Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac8dbe03b2 | ||
|
|
6c2aef7a6d | ||
|
|
e1eb68e226 | ||
|
|
4ae02a8764 | ||
|
|
cc59342e80 | ||
|
|
97099f4d69 | ||
|
|
c50c290c3e | ||
|
|
4775e4a36a | ||
|
|
08cb834b4d | ||
|
|
46cff26d92 | ||
|
|
b668e39296 | ||
|
|
97ee16eb4e | ||
|
|
41fd190d38 | ||
|
|
2b0facb24c | ||
|
|
94a29efea5 | ||
|
|
827156485c | ||
|
|
6b4e6857de | ||
|
|
5b7e814166 | ||
|
|
f99b40c2f3 | ||
|
|
ae00e83c9d | ||
|
|
d4f2f12924 |
@@ -61,7 +61,7 @@ class Loader(object):
|
||||
self.log = CPLog(__name__)
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S')
|
||||
hdlr = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10)
|
||||
hdlr = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10, encoding = 'utf-8')
|
||||
hdlr.setLevel(logging.CRITICAL)
|
||||
hdlr.setFormatter(formatter)
|
||||
self.log.logger.addHandler(hdlr)
|
||||
|
||||
@@ -3,7 +3,7 @@ from threading import Thread
|
||||
import json
|
||||
import threading
|
||||
import traceback
|
||||
import urllib
|
||||
from six.moves import urllib
|
||||
|
||||
from couchpotato.core.helpers.request import getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -102,7 +102,7 @@ class ApiHandler(RequestHandler):
|
||||
|
||||
kwargs = {}
|
||||
for x in self.request.arguments:
|
||||
kwargs[x] = urllib.unquote(self.get_argument(x))
|
||||
kwargs[x] = urllib.parse.unquote(self.get_argument(x))
|
||||
|
||||
# Split array arguments
|
||||
kwargs = getParams(kwargs)
|
||||
|
||||
@@ -181,13 +181,13 @@ class Core(Plugin):
|
||||
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
|
||||
def version(self):
|
||||
ver = fireEvent('updater.info', single = True) or {'version': {}}
|
||||
ver = fireEvent('updater.info', single = True)
|
||||
|
||||
if os.name == 'nt': platf = 'windows'
|
||||
elif 'Darwin' in platform.platform(): platf = 'osx'
|
||||
else: platf = 'linux'
|
||||
|
||||
return '%s - %s-%s - v2' % (platf, ver.get('version').get('type') or 'unknown', ver.get('version').get('hash') or 'unknown')
|
||||
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
||||
|
||||
def versionView(self, **kwargs):
|
||||
return {
|
||||
@@ -290,7 +290,7 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'permission_file',
|
||||
'default': '0644',
|
||||
'default': '0755',
|
||||
'label': 'File CHMOD',
|
||||
'description': 'See Folder CHMOD description, but for files',
|
||||
},
|
||||
|
||||
@@ -205,28 +205,19 @@ class GitUpdater(BaseUpdater):
|
||||
def getVersion(self):
|
||||
|
||||
if not self.version:
|
||||
|
||||
hash = None
|
||||
date = None
|
||||
branch = self.branch
|
||||
|
||||
try:
|
||||
output = self.repo.getHead() # Yes, please
|
||||
log.debug('Git version output: %s', output.hash)
|
||||
|
||||
hash = output.hash[:8]
|
||||
date = output.getDate()
|
||||
branch = self.repo.getCurrentBranch().name
|
||||
self.version = {
|
||||
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.repo.getCurrentBranch().name or self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
|
||||
'hash': output.hash[:8],
|
||||
'date': output.getDate(),
|
||||
'type': 'git',
|
||||
'branch': self.repo.getCurrentBranch().name
|
||||
}
|
||||
except Exception as e:
|
||||
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
|
||||
|
||||
self.version = {
|
||||
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, branch, hash or 'unknown_hash', datetime.fromtimestamp(date) if date else 'unknown_date'),
|
||||
'hash': hash,
|
||||
'date': date,
|
||||
'type': 'git',
|
||||
'branch': branch
|
||||
}
|
||||
return 'No GIT'
|
||||
|
||||
return self.version
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import os
|
||||
import time
|
||||
import traceback
|
||||
from sqlite3 import OperationalError
|
||||
from CodernityDB3.index import Index
|
||||
|
||||
from CodernityDB.database import RecordNotFound
|
||||
from CodernityDB.index import IndexException, IndexNotFoundException, IndexConflict
|
||||
from couchpotato import CPLog
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.database import IndexException, IndexNotFoundException, IndexConflict, RecordNotFound
|
||||
from couchpotato.core.helpers.encoding import toUnicode, sp
|
||||
from couchpotato.core.helpers.variable import getImdb, tryInt, randomString
|
||||
|
||||
@@ -621,8 +621,6 @@ class Database(object):
|
||||
|
||||
except OperationalError:
|
||||
log.error('Migrating from faulty database, probably a (too) old version: %s', traceback.format_exc())
|
||||
|
||||
rename_old = True
|
||||
except:
|
||||
log.error('Migration failed: %s', traceback.format_exc())
|
||||
|
||||
|
||||
@@ -27,11 +27,6 @@ class Deluge(DownloaderBase):
|
||||
def connect(self, reconnect = False):
|
||||
# Load host from config and split out port.
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
|
||||
# Force host assignment
|
||||
if len(host) == 1:
|
||||
host.append(80)
|
||||
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from urlparse import urlparse
|
||||
from six.moves import urllib
|
||||
import os
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
@@ -62,7 +62,7 @@ class rTorrent(DownloaderBase):
|
||||
if self.conf('ssl') and url.startswith('httprpc://'):
|
||||
url = url.replace('httprpc://', 'httprpc+https://')
|
||||
|
||||
parsed = urlparse(url)
|
||||
parsed = urllib.urlparse(url)
|
||||
|
||||
# rpc_url is only used on http/https scgi pass-through
|
||||
if parsed.scheme in ['http', 'https']:
|
||||
|
||||
@@ -78,14 +78,12 @@ class Transmission(DownloaderBase):
|
||||
log.error('Failed sending torrent to Transmission')
|
||||
return False
|
||||
|
||||
data = remote_torrent.get('torrent-added') or remote_torrent.get('torrent-duplicate')
|
||||
|
||||
# Change settings of added torrents
|
||||
if torrent_params:
|
||||
self.trpc.set_torrent(data['hashString'], torrent_params)
|
||||
self.trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||
|
||||
log.info('Torrent sent to Transmission successfully.')
|
||||
return self.downloadReturnId(data['hashString'])
|
||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||
|
||||
def test(self):
|
||||
if self.connect() and self.trpc.get_session():
|
||||
|
||||
@@ -102,7 +102,7 @@ def fireEvent(name, *args, **kwargs):
|
||||
# Fire
|
||||
result = e(*args, **kwargs)
|
||||
|
||||
result_keys = result.keys()
|
||||
result_keys = list(result.keys())
|
||||
result_keys.sort(key = natsortKey)
|
||||
|
||||
if options['single'] and not options['merge']:
|
||||
|
||||
26
couchpotato/core/helpers/database.py
Normal file
26
couchpotato/core/helpers/database.py
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
from six import PY2
|
||||
|
||||
|
||||
if PY2:
|
||||
from CodernityDB.database_super_thread_safe import SuperThreadSafeDatabase
|
||||
from CodernityDB.index import IndexException, IndexConflict, IndexNotFoundException
|
||||
from CodernityDB.database import RecordNotFound, RecordDeleted
|
||||
from CodernityDB.hash_index import HashIndex
|
||||
from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex
|
||||
else:
|
||||
from CodernityDB3.database_super_thread_safe import SuperThreadSafeDatabase
|
||||
from CodernityDB3.index import IndexException, IndexConflict, IndexNotFoundException
|
||||
from CodernityDB3.database import RecordNotFound, RecordDeleted
|
||||
from CodernityDB3.hash_index import HashIndex
|
||||
from CodernityDB3.tree_index import MultiTreeBasedIndex, TreeBasedIndex
|
||||
|
||||
SuperThreadSafeDatabase = SuperThreadSafeDatabase
|
||||
IndexException = IndexException
|
||||
IndexNotFoundException = IndexNotFoundException
|
||||
IndexConflict = IndexConflict
|
||||
RecordNotFound = RecordNotFound
|
||||
HashIndex = HashIndex
|
||||
MultiTreeBasedIndex = MultiTreeBasedIndex
|
||||
TreeBasedIndex = TreeBasedIndex
|
||||
RecordDeleted = RecordDeleted
|
||||
@@ -1,11 +1,11 @@
|
||||
from string import ascii_letters, digits
|
||||
from urllib import quote_plus
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
import unicodedata
|
||||
|
||||
from chardet import detect
|
||||
from six.moves import urllib
|
||||
from couchpotato.core.logger import CPLog
|
||||
import six
|
||||
|
||||
@@ -16,7 +16,7 @@ log = CPLog(__name__)
|
||||
def toSafeString(original):
|
||||
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
||||
cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||
valid_string = ''.join(c for c in cleaned_filename if c in valid_chars)
|
||||
valid_string = ''.join(list(six.unichr(c) for c in cleaned_filename if six.unichr(c) in valid_chars))
|
||||
return ' '.join(valid_string.split())
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def simplifyString(original):
|
||||
|
||||
def toUnicode(original, *args):
|
||||
try:
|
||||
if isinstance(original, unicode):
|
||||
if isinstance(original, six.text_type):
|
||||
return original
|
||||
else:
|
||||
try:
|
||||
@@ -47,16 +47,32 @@ def toUnicode(original, *args):
|
||||
ascii_text = str(original).encode('string_escape')
|
||||
return toUnicode(ascii_text)
|
||||
|
||||
def toUTF8(original):
|
||||
try:
|
||||
if isinstance(original, six.binary_type) and len(original) > 0:
|
||||
# Try to detect
|
||||
detected = detect(original)
|
||||
return original.decode(detected.get('encoding')).encode('utf-8')
|
||||
else:
|
||||
return original
|
||||
except:
|
||||
#log.error('Failed encoding to UTF8: %s', traceback.format_exc())
|
||||
raise
|
||||
|
||||
def ss(original, *args):
|
||||
|
||||
u_original = toUnicode(original, *args)
|
||||
try:
|
||||
from couchpotato.environment import Env
|
||||
return u_original.encode(Env.get('encoding'))
|
||||
if isinstance(u_original, six.text_type):
|
||||
u_original = u_original.encode('unicode_escape')
|
||||
else:
|
||||
u_original = u_original
|
||||
|
||||
return six.u(u_original)
|
||||
except Exception as e:
|
||||
log.debug('Failed ss encoding char, force UTF8: %s', e)
|
||||
try:
|
||||
from couchpotato.environment import Env
|
||||
return u_original.encode(Env.get('encoding'), 'replace')
|
||||
except:
|
||||
return u_original.encode('utf-8', 'replace')
|
||||
@@ -72,7 +88,7 @@ def sp(path, *args):
|
||||
if os.path.sep == '/' and '\\' in path:
|
||||
path = '/' + path.replace(':', '').replace('\\', '/')
|
||||
|
||||
path = os.path.normpath(ss(path, *args))
|
||||
path = os.path.normpath(path)
|
||||
|
||||
# Remove any trailing path separators
|
||||
if path != os.path.sep:
|
||||
@@ -114,14 +130,15 @@ def stripAccents(s):
|
||||
def tryUrlencode(s):
|
||||
new = six.u('')
|
||||
if isinstance(s, dict):
|
||||
for key, value in s.items():
|
||||
for key, value in list(s.items()):
|
||||
new += six.u('&%s=%s') % (key, tryUrlencode(value))
|
||||
|
||||
return new[1:]
|
||||
else:
|
||||
for letter in ss(s):
|
||||
letter = six.unichr(letter)
|
||||
try:
|
||||
new += quote_plus(letter)
|
||||
new += urllib.parse.quote_plus(letter)
|
||||
except:
|
||||
new += letter
|
||||
|
||||
|
||||
51
couchpotato/core/helpers/path.py
Normal file
51
couchpotato/core/helpers/path.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import os
|
||||
from chardet import detect
|
||||
from couchpotato import Env
|
||||
|
||||
fs_enc = Env.get('fs_encoding')
|
||||
|
||||
|
||||
def list_dir(path, full_path = True):
|
||||
"""
|
||||
List directory don't error when it doesn't exist
|
||||
"""
|
||||
|
||||
path = unicode_path(path)
|
||||
|
||||
if os.path.isdir(path):
|
||||
for f in os.listdir(path):
|
||||
if full_path:
|
||||
yield join(path, f)
|
||||
else:
|
||||
yield f
|
||||
|
||||
|
||||
def join(*args):
|
||||
"""
|
||||
Join path, encode properly before joining
|
||||
"""
|
||||
|
||||
return os.path.join(*[safe(x) for x in args])
|
||||
|
||||
|
||||
def unicode_path(path):
|
||||
"""
|
||||
Convert back to unicode
|
||||
:param path: path string
|
||||
"""
|
||||
|
||||
if isinstance(path, str):
|
||||
detected = detect(path)
|
||||
print detected
|
||||
path = path.decode(detected.get('encoding'))
|
||||
path = path.decode('unicode_escape')
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def safe(path):
|
||||
|
||||
if isinstance(path, unicode):
|
||||
return path.encode('unicode_escape')
|
||||
|
||||
return path
|
||||
2
couchpotato/core/helpers/py3.py
Normal file
2
couchpotato/core/helpers/py3.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class NotSupported(Exception):
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
from urllib import unquote
|
||||
import re
|
||||
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from six.moves import urllib
|
||||
from couchpotato.core.helpers.variable import natsortKey
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ def getParams(params):
|
||||
reg = re.compile('^[a-z0-9_\.]+$')
|
||||
|
||||
# Sort keys
|
||||
param_keys = params.keys()
|
||||
param_keys = list(params.keys())
|
||||
param_keys.sort(key = natsortKey)
|
||||
|
||||
temp = {}
|
||||
@@ -28,7 +28,7 @@ def getParams(params):
|
||||
|
||||
for item in nested:
|
||||
if item is nested[-1]:
|
||||
current[item] = toUnicode(unquote(value))
|
||||
current[item] = toUnicode(urllib.parse.unquote(value))
|
||||
else:
|
||||
try:
|
||||
current[item]
|
||||
@@ -37,7 +37,7 @@ def getParams(params):
|
||||
|
||||
current = current[item]
|
||||
else:
|
||||
temp[param] = toUnicode(unquote(value))
|
||||
temp[param] = toUnicode(urllib.parse.unquote(value))
|
||||
if temp[param].lower() in ['true', 'false']:
|
||||
temp[param] = temp[param].lower() != 'false'
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.py3 import NotSupported
|
||||
from couchpotato.core.logger import CPLog
|
||||
from importhelper import import_module
|
||||
import six
|
||||
@@ -131,7 +132,7 @@ class Loader(object):
|
||||
return False
|
||||
try:
|
||||
# Load single file plugin
|
||||
if isinstance(module.autoload, (str, unicode)):
|
||||
if isinstance(module.autoload, (six.string_types, six.text_type)):
|
||||
getattr(module, module.autoload)()
|
||||
# Load folder plugin
|
||||
else:
|
||||
@@ -162,6 +163,8 @@ class Loader(object):
|
||||
def loadModule(self, name):
|
||||
try:
|
||||
return import_module(name)
|
||||
except NotSupported:
|
||||
log.error('Module "%s" is not supported in Python 3', name)
|
||||
except ImportError:
|
||||
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
||||
return None
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import re
|
||||
import traceback
|
||||
|
||||
|
||||
class CPLog(object):
|
||||
@@ -54,19 +55,19 @@ class CPLog(object):
|
||||
|
||||
def safeMessage(self, msg, replace_tuple = ()):
|
||||
|
||||
from couchpotato.core.helpers.encoding import ss, toUnicode
|
||||
from couchpotato.core.helpers.encoding import ss, toUTF8
|
||||
|
||||
msg = ss(msg)
|
||||
msg = toUTF8(msg)
|
||||
|
||||
try:
|
||||
if isinstance(replace_tuple, tuple):
|
||||
msg = msg % tuple([ss(x) if not isinstance(x, (int, float)) else x for x in list(replace_tuple)])
|
||||
msg = msg % tuple([toUTF8(x) for x in list(replace_tuple)])
|
||||
elif isinstance(replace_tuple, dict):
|
||||
msg = msg % dict((k, ss(v) if not isinstance(v, (int, float)) else v) for k, v in replace_tuple.iteritems())
|
||||
msg = msg % dict((k, toUTF8(v)) for k, v in replace_tuple.iteritems())
|
||||
else:
|
||||
msg = msg % ss(replace_tuple)
|
||||
except Exception as e:
|
||||
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
msg = msg % toUTF8(replace_tuple)
|
||||
except:
|
||||
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, traceback.format_exc()))
|
||||
|
||||
self.setup()
|
||||
if not self.is_develop:
|
||||
@@ -83,4 +84,4 @@ class CPLog(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
return toUnicode(msg)
|
||||
return toUTF8(msg)
|
||||
|
||||
@@ -89,7 +89,7 @@ class MediaBase(Plugin):
|
||||
|
||||
# Loop over type
|
||||
for image in image_urls.get(image_type, []):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
if not isinstance(image, six.string_types):
|
||||
continue
|
||||
|
||||
if file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from string import ascii_letters
|
||||
from hashlib import md5
|
||||
from couchpotato.core.helpers.database import MultiTreeBasedIndex, TreeBasedIndex
|
||||
|
||||
from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
|
||||
|
||||
class MediaIndex(MultiTreeBasedIndex):
|
||||
_version = 3
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
|
||||
custom_header = """from couchpotato.core.helpers.database import MultiTreeBasedIndex"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
@@ -62,10 +62,11 @@ class MediaTypeIndex(TreeBasedIndex):
|
||||
|
||||
|
||||
class TitleSearchIndex(MultiTreeBasedIndex):
|
||||
_version = 1
|
||||
_version = 2
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex
|
||||
from itertools import izip
|
||||
custom_header = """from couchpotato.core.helpers.database import MultiTreeBasedIndex
|
||||
try: from itertools import izip
|
||||
except: izip = zip
|
||||
from couchpotato.core.helpers.encoding import simplifyString"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -101,7 +102,7 @@ from couchpotato.core.helpers.encoding import simplifyString"""
|
||||
class TitleIndex(TreeBasedIndex):
|
||||
_version = 4
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
|
||||
custom_header = """from couchpotato.core.helpers.database import TreeBasedIndex
|
||||
from string import ascii_letters
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
|
||||
|
||||
@@ -134,7 +135,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
|
||||
class StartsWithIndex(TreeBasedIndex):
|
||||
_version = 3
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
|
||||
custom_header = """from couchpotato.core.helpers.database import TreeBasedIndex
|
||||
from string import ascii_letters
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
|
||||
|
||||
@@ -180,7 +181,7 @@ class MediaChildrenIndex(TreeBasedIndex):
|
||||
class MediaTagIndex(MultiTreeBasedIndex):
|
||||
_version = 2
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
|
||||
custom_header = """from couchpotato.core.helpers.database import MultiTreeBasedIndex"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
|
||||
@@ -3,15 +3,16 @@ import time
|
||||
import traceback
|
||||
from string import ascii_lowercase
|
||||
|
||||
from CodernityDB.database import RecordNotFound, RecordDeleted
|
||||
from couchpotato import tryInt, get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.database import RecordNotFound, RecordDeleted
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString, getImdb, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media import MediaBase
|
||||
from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex, MediaTagIndex
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -273,10 +274,14 @@ class MediaPlugin(MediaBase):
|
||||
for x in filter_by:
|
||||
media_ids = [n for n in media_ids if n in filter_by[x]]
|
||||
|
||||
total_count = len(media_ids)
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
offset = 0
|
||||
limit = -1
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, six.string_types) else limit_offset
|
||||
limit = tryInt(splt[0])
|
||||
offset = tryInt(0 if len(splt) is 1 else splt[1])
|
||||
|
||||
@@ -302,30 +307,11 @@ class MediaPlugin(MediaBase):
|
||||
media_ids.remove(media_id)
|
||||
if len(media_ids) == 0 or len(medias) == limit: break
|
||||
|
||||
# Sort media by type and return result
|
||||
result = {}
|
||||
|
||||
# Create keys for media types we are listing
|
||||
if types:
|
||||
for media_type in types:
|
||||
result['%ss' % media_type] = []
|
||||
else:
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
result['%ss' % media_type] = []
|
||||
|
||||
total_count = len(medias)
|
||||
|
||||
if total_count == 0:
|
||||
return 0, result
|
||||
|
||||
for kind in medias:
|
||||
result['%ss' % kind['type']].append(kind)
|
||||
|
||||
return total_count, result
|
||||
return total_count, medias
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
total_count, result = self.list(
|
||||
total_movies, movies = self.list(
|
||||
types = splitString(kwargs.get('type')),
|
||||
status = splitString(kwargs.get('status')),
|
||||
release_status = splitString(kwargs.get('release_status')),
|
||||
@@ -336,12 +322,12 @@ class MediaPlugin(MediaBase):
|
||||
search = kwargs.get('search')
|
||||
)
|
||||
|
||||
results = result
|
||||
results['success'] = True
|
||||
results['empty'] = len(result) == 0
|
||||
results['total'] = total_count
|
||||
|
||||
return results
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def addSingleListView(self):
|
||||
|
||||
@@ -471,11 +457,6 @@ class MediaPlugin(MediaBase):
|
||||
deleted = True
|
||||
elif new_media_status:
|
||||
media['status'] = new_media_status
|
||||
|
||||
# Remove profile (no use for in manage)
|
||||
if new_media_status == 'done':
|
||||
media['profile_id'] = None
|
||||
|
||||
db.update(media)
|
||||
|
||||
fireEvent('media.untag', media['_id'], 'recent', single = True)
|
||||
@@ -511,7 +492,7 @@ class MediaPlugin(MediaBase):
|
||||
}
|
||||
})
|
||||
|
||||
def restatus(self, media_id, tag_recent = True, allowed_restatus = None):
|
||||
def restatus(self, media_id, tag_recent = True):
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
@@ -546,7 +527,7 @@ class MediaPlugin(MediaBase):
|
||||
m['status'] = previous_status
|
||||
|
||||
# Only update when status has changed
|
||||
if previous_status != m['status'] and (not allowed_restatus or m['status'] in allowed_restatus):
|
||||
if previous_status != m['status']:
|
||||
db.update(m)
|
||||
|
||||
# Tag media as recent
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from urlparse import urlparse
|
||||
from six.moves import urllib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
@@ -50,7 +50,7 @@ class Provider(Plugin):
|
||||
if Env.get('dev'): return True
|
||||
|
||||
now = time.time()
|
||||
host = urlparse(test_url).hostname
|
||||
host = urllib.urlparse(test_url).hostname
|
||||
|
||||
if self.last_available_check.get(host) < now - 900:
|
||||
self.last_available_check[host] = now
|
||||
@@ -219,7 +219,7 @@ class YarrProvider(Provider):
|
||||
if provider and provider == self.getName():
|
||||
return self
|
||||
|
||||
hostname = urlparse(url).hostname
|
||||
hostname = urllib.urlparse(url).hostname
|
||||
if host and hostname in host:
|
||||
return self
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from urlparse import urlparse
|
||||
from six.moves import urllib
|
||||
import time
|
||||
import traceback
|
||||
import re
|
||||
@@ -97,7 +97,7 @@ class Base(NZBProvider, RSS):
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
||||
'provider_extra': urllib.urlparse(host['host']).hostname or host['host'],
|
||||
'name': toUnicode(name),
|
||||
'name_extra': name_extra,
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
@@ -175,7 +175,7 @@ class Base(NZBProvider, RSS):
|
||||
return '&apikey=%s' % host['api_key']
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
host = urlparse(url).hostname
|
||||
host = urllib.urlparse(url).hostname
|
||||
|
||||
if self.limits_reached.get(host):
|
||||
# Try again in 3 hours
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from urlparse import urlparse, parse_qs
|
||||
from six.moves import urllib
|
||||
import time
|
||||
|
||||
from couchpotato.core.event import fireEvent
|
||||
@@ -52,7 +52,7 @@ class Base(NZBProvider, RSS):
|
||||
for nzb in nzbs:
|
||||
|
||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||
nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
|
||||
nzb_id = urllib.parse_qs(urllib.urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import htmlentitydefs
|
||||
from six.moves import html_entities
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
@@ -145,15 +145,15 @@ class Base(TorrentProvider):
|
||||
# character reference
|
||||
try:
|
||||
if txt[:3] == "&#x":
|
||||
return unichr(int(txt[3:-1], 16))
|
||||
return six.unichr(int(txt[3:-1], 16))
|
||||
else:
|
||||
return unichr(int(txt[2:-1]))
|
||||
return six.unichr(int(txt[2:-1]))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
# named entity
|
||||
try:
|
||||
txt = unichr(htmlentitydefs.name2codepoint[txt[1:-1]])
|
||||
txt = six.unichr(html_entities.name2codepoint[txt[1:-1]])
|
||||
except KeyError:
|
||||
pass
|
||||
return txt # leave as is
|
||||
|
||||
126
couchpotato/core/media/_base/providers/torrent/torrentleech.py
Normal file
126
couchpotato/core/media/_base/providers/torrent/torrentleech.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.torrentleech.org/',
|
||||
'login': 'https://www.torrentleech.org/user/account/login/',
|
||||
'login_check': 'https://torrentleech.org/user/messages',
|
||||
'detail': 'https://www.torrentleech.org/torrent/%s',
|
||||
'search': 'https://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||
'download': 'https://www.torrentleech.org%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def _searchOnTitle(self, title, media, quality, results):
|
||||
|
||||
url = self.urls['search'] % self.buildUrl(title, media, quality)
|
||||
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'id': 'torrenttable'})
|
||||
if not result_table:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
for result in entries[1:]:
|
||||
|
||||
link = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||
url = result.find('td', attrs = {'class': 'quickdownload'}).find('a')
|
||||
details = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||
|
||||
results.append({
|
||||
'id': link['href'].replace('/torrent/', ''),
|
||||
'name': six.text_type(link.string),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % details['href'],
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string),
|
||||
'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'remember_me': 'on',
|
||||
'login': 'submit',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'torrentleech',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentLeech',
|
||||
'description': '<a href="http://torrentleech.org">TorrentLeech</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,4 +1,4 @@
|
||||
from urlparse import urlparse
|
||||
from six.moves import urllib
|
||||
import re
|
||||
import traceback
|
||||
|
||||
@@ -45,7 +45,7 @@ class Base(TorrentProvider):
|
||||
results.append({
|
||||
'id': torrent.get('torrent_id'),
|
||||
'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet',
|
||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
||||
'provider_extra': urllib.urlparse(host['host']).hostname or host['host'],
|
||||
'name': toUnicode(torrent.get('release_name')),
|
||||
'url': torrent.get('download_url'),
|
||||
'detail_url': torrent.get('details_url'),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from urlparse import urlparse
|
||||
from six.moves import urllib
|
||||
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
@@ -34,7 +34,7 @@ class UserscriptBase(Plugin):
|
||||
|
||||
def belongsTo(self, url):
|
||||
|
||||
host = urlparse(url).hostname
|
||||
host = urllib.urlparse(url).hostname
|
||||
host_split = host.split('.')
|
||||
if len(host_split) > 2:
|
||||
host = host[len(host_split[0]):]
|
||||
|
||||
@@ -3,6 +3,7 @@ from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts, getImdb
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import six
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -30,7 +31,7 @@ class Search(Plugin):
|
||||
def search(self, q = '', types = None, **kwargs):
|
||||
|
||||
# Make sure types is the correct instance
|
||||
if isinstance(types, (str, unicode)):
|
||||
if isinstance(types, six.string_types):
|
||||
types = [types]
|
||||
elif isinstance(types, (list, tuple, set)):
|
||||
types = list(types)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import traceback
|
||||
import time
|
||||
|
||||
from CodernityDB.database import RecordNotFound
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.database import RecordNotFound
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle, getImdb, getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
|
||||
@@ -264,11 +264,3 @@
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.toggle_menu h2 {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
from CodernityDB.database import RecordNotFound
|
||||
from couchpotato import get_db
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.database import RecordNotFound
|
||||
from couchpotato.core.helpers.variable import mergeDicts, randomString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
@@ -4,7 +4,6 @@ from couchpotato import tryInt
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie.providers.base import MovieProvider
|
||||
from requests import HTTPError
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -33,14 +32,12 @@ class FanartTV(MovieProvider):
|
||||
|
||||
try:
|
||||
url = self.urls['api'] % identifier
|
||||
fanart_data = self.getJsonData(url, show_error = False)
|
||||
fanart_data = self.getJsonData(url)
|
||||
|
||||
if fanart_data:
|
||||
log.debug('Found images for %s', fanart_data.get('name'))
|
||||
images = self._parseMovie(fanart_data)
|
||||
except HTTPError as e:
|
||||
log.debug('Failed getting extra art for %s: %s',
|
||||
(identifier, e))
|
||||
|
||||
except:
|
||||
log.error('Failed getting extra art for %s: %s',
|
||||
(identifier, traceback.format_exc()))
|
||||
|
||||
@@ -7,6 +7,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie.providers.base import MovieProvider
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -72,7 +73,7 @@ class OMDBAPI(MovieProvider):
|
||||
try:
|
||||
|
||||
try:
|
||||
if isinstance(movie, (str, unicode)):
|
||||
if isinstance(movie, six.string_types):
|
||||
movie = json.loads(movie)
|
||||
except ValueError:
|
||||
log.info('No proper json to decode')
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import traceback
|
||||
import time
|
||||
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss, tryUrlencode
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie.providers.base import MovieProvider
|
||||
@@ -13,7 +14,7 @@ autoload = 'TheMovieDb'
|
||||
|
||||
class TheMovieDb(MovieProvider):
|
||||
|
||||
http_time_between_calls = .35
|
||||
http_time_between_calls = .3
|
||||
|
||||
configuration = {
|
||||
'images': {
|
||||
@@ -22,8 +23,6 @@ class TheMovieDb(MovieProvider):
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('info.search', self.search, priority = 3)
|
||||
addEvent('movie.search', self.search, priority = 3)
|
||||
addEvent('movie.info', self.getInfo, priority = 3)
|
||||
addEvent('movie.info_by_tmdb', self.getInfo)
|
||||
addEvent('app.load', self.config)
|
||||
@@ -33,45 +32,49 @@ class TheMovieDb(MovieProvider):
|
||||
if configuration:
|
||||
self.configuration = configuration
|
||||
|
||||
def search(self, q, limit = 3):
|
||||
def search(self, q, limit = 12):
|
||||
""" Find movie by name """
|
||||
|
||||
if self.isDisabled():
|
||||
return False
|
||||
|
||||
log.debug('Searching for movie: %s', q)
|
||||
search_string = simplifyString(q)
|
||||
cache_key = 'tmdb.cache.%s.%s' % (search_string, limit)
|
||||
results = None #self.getCache(cache_key)
|
||||
|
||||
raw = None
|
||||
try:
|
||||
name_year = fireEvent('scanner.name_year', q, single = True)
|
||||
raw = self.request('search/movie', {
|
||||
'query': name_year.get('name', q),
|
||||
'year': name_year.get('year'),
|
||||
'search_type': 'ngram' if limit > 1 else 'phrase'
|
||||
}, return_key = 'results')
|
||||
except:
|
||||
log.error('Failed searching TMDB for "%s": %s', (q, traceback.format_exc()))
|
||||
if not results:
|
||||
log.debug('Searching for movie: %s', q)
|
||||
|
||||
results = []
|
||||
if raw:
|
||||
raw = None
|
||||
try:
|
||||
nr = 0
|
||||
|
||||
for movie in raw:
|
||||
parsed_movie = self.parseMovie(movie, extended = False)
|
||||
if parsed_movie:
|
||||
results.append(parsed_movie)
|
||||
#name_year = fireEvent('scanner.name_year', q, single = True)
|
||||
|
||||
nr += 1
|
||||
if nr == limit:
|
||||
break
|
||||
raw = self.request('search/movie', {
|
||||
'query': q
|
||||
}, return_key = 'results')
|
||||
except:
|
||||
log.error('Failed searching TMDB for "%s": %s', (search_string, traceback.format_exc()))
|
||||
|
||||
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
|
||||
results = []
|
||||
if raw:
|
||||
try:
|
||||
nr = 0
|
||||
|
||||
return results
|
||||
except SyntaxError as e:
|
||||
log.error('Failed to parse XML response: %s', e)
|
||||
return False
|
||||
for movie in raw:
|
||||
results.append(self.parseMovie(movie, extended = False))
|
||||
|
||||
nr += 1
|
||||
if nr == limit:
|
||||
break
|
||||
|
||||
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
|
||||
|
||||
self.setCache(cache_key, results)
|
||||
return results
|
||||
except SyntaxError as e:
|
||||
log.error('Failed to parse XML response: %s', e)
|
||||
return False
|
||||
|
||||
return results
|
||||
|
||||
@@ -84,87 +87,93 @@ class TheMovieDb(MovieProvider):
|
||||
'id': identifier
|
||||
}, extended = extended)
|
||||
|
||||
return result or {}
|
||||
return result
|
||||
|
||||
def parseMovie(self, movie, extended = True):
|
||||
|
||||
# Do request, append other items
|
||||
movie = self.request('movie/%s' % movie.get('id'), {
|
||||
'append_to_response': 'alternative_titles' + (',images,casts' if extended else '')
|
||||
})
|
||||
if not movie:
|
||||
return
|
||||
cache_key = 'tmdb.cache.%s%s' % (movie.get('id'), '.ex' if extended else '')
|
||||
movie_data = None #self.getCache(cache_key)
|
||||
|
||||
# Images
|
||||
poster = self.getImage(movie, type = 'poster', size = 'w154')
|
||||
poster_original = self.getImage(movie, type = 'poster', size = 'original')
|
||||
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
|
||||
extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original') if extended else []
|
||||
|
||||
images = {
|
||||
'poster': [poster] if poster else [],
|
||||
#'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [poster_original] if poster_original else [],
|
||||
'backdrop_original': [backdrop_original] if backdrop_original else [],
|
||||
'actors': {},
|
||||
'extra_thumbs': extra_thumbs
|
||||
}
|
||||
|
||||
# Genres
|
||||
try:
|
||||
genres = [genre.get('name') for genre in movie.get('genres', [])]
|
||||
except:
|
||||
genres = []
|
||||
|
||||
# 1900 is the same as None
|
||||
year = str(movie.get('release_date') or '')[:4]
|
||||
if not movie.get('release_date') or year == '1900' or year.lower() == 'none':
|
||||
year = None
|
||||
|
||||
# Gather actors data
|
||||
actors = {}
|
||||
if extended:
|
||||
if not movie_data:
|
||||
|
||||
# Full data
|
||||
cast = movie.get('casts', {}).get('cast', [])
|
||||
movie = self.request('movie/%s' % movie.get('id'))
|
||||
|
||||
for cast_item in cast:
|
||||
try:
|
||||
actors[toUnicode(cast_item.get('name'))] = toUnicode(cast_item.get('character'))
|
||||
images['actors'][toUnicode(cast_item.get('name'))] = self.getImage(cast_item, type = 'profile', size = 'original')
|
||||
except:
|
||||
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
|
||||
# Images
|
||||
poster = self.getImage(movie, type = 'poster', size = 'w154')
|
||||
poster_original = self.getImage(movie, type = 'poster', size = 'original')
|
||||
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
|
||||
extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original')
|
||||
|
||||
movie_data = {
|
||||
'type': 'movie',
|
||||
'via_tmdb': True,
|
||||
'tmdb_id': movie.get('id'),
|
||||
'titles': [toUnicode(movie.get('title'))],
|
||||
'original_title': movie.get('original_title'),
|
||||
'images': images,
|
||||
'imdb': movie.get('imdb_id'),
|
||||
'runtime': movie.get('runtime'),
|
||||
'released': str(movie.get('release_date')),
|
||||
'year': tryInt(year, None),
|
||||
'plot': movie.get('overview'),
|
||||
'genres': genres,
|
||||
'collection': getattr(movie.get('belongs_to_collection'), 'name', None),
|
||||
'actor_roles': actors
|
||||
}
|
||||
images = {
|
||||
'poster': [poster] if poster else [],
|
||||
#'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [poster_original] if poster_original else [],
|
||||
'backdrop_original': [backdrop_original] if backdrop_original else [],
|
||||
'actors': {},
|
||||
'extra_thumbs': extra_thumbs
|
||||
}
|
||||
|
||||
movie_data = dict((k, v) for k, v in movie_data.items() if v)
|
||||
# Genres
|
||||
try:
|
||||
genres = [genre.get('name') for genre in movie.get('genres', [])]
|
||||
except:
|
||||
genres = []
|
||||
|
||||
# Add alternative names
|
||||
if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']:
|
||||
movie_data['titles'].append(movie_data['original_title'])
|
||||
# 1900 is the same as None
|
||||
year = str(movie.get('release_date') or '')[:4]
|
||||
if not movie.get('release_date') or year == '1900' or year.lower() == 'none':
|
||||
year = None
|
||||
|
||||
# Add alternative titles
|
||||
alternate_titles = movie.get('alternative_titles', {}).get('titles', [])
|
||||
# Gather actors data
|
||||
actors = {}
|
||||
if extended:
|
||||
|
||||
for alt in alternate_titles:
|
||||
alt_name = alt.get('title')
|
||||
if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None:
|
||||
movie_data['titles'].append(alt_name)
|
||||
# Full data
|
||||
cast = self.request('movie/%s/casts' % movie.get('id'), return_key = 'cast')
|
||||
|
||||
for cast_item in cast:
|
||||
try:
|
||||
actors[toUnicode(cast_item.get('name'))] = toUnicode(cast_item.get('character'))
|
||||
images['actors'][toUnicode(cast_item.get('name'))] = self.getImage(cast_item, type = 'profile', size = 'original')
|
||||
except:
|
||||
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
|
||||
|
||||
movie_data = {
|
||||
'type': 'movie',
|
||||
'via_tmdb': True,
|
||||
'tmdb_id': movie.get('id'),
|
||||
'titles': [toUnicode(movie.get('title'))],
|
||||
'original_title': movie.get('original_title'),
|
||||
'images': images,
|
||||
'imdb': movie.get('imdb_id'),
|
||||
'runtime': movie.get('runtime'),
|
||||
'released': str(movie.get('release_date')),
|
||||
'year': tryInt(year, None),
|
||||
'plot': movie.get('overview'),
|
||||
'genres': genres,
|
||||
'collection': getattr(movie.get('belongs_to_collection'), 'name', None),
|
||||
'actor_roles': actors
|
||||
}
|
||||
|
||||
movie_data = dict((k, v) for k, v in movie_data.items() if v)
|
||||
|
||||
# Add alternative names
|
||||
if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']:
|
||||
movie_data['titles'].append(movie_data['original_title'])
|
||||
|
||||
if extended:
|
||||
|
||||
# Full data
|
||||
alternate_titles = self.request('movie/%s/alternative_titles' % movie.get('id'), return_key = 'titles')
|
||||
|
||||
for alt in alternate_titles:
|
||||
alt_name = alt.get('title')
|
||||
if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None:
|
||||
movie_data['titles'].append(alt_name)
|
||||
|
||||
# Cache movie parsed
|
||||
self.setCache(cache_key, movie_data)
|
||||
|
||||
return movie_data
|
||||
|
||||
@@ -183,26 +192,23 @@ class TheMovieDb(MovieProvider):
|
||||
|
||||
image_urls = []
|
||||
try:
|
||||
for image in movie.get('images', {}).get(type, [])[1:5]:
|
||||
|
||||
# Full data
|
||||
images = self.request('movie/%s/images' % movie.get('id'), return_key = type)
|
||||
for image in images[1:5]:
|
||||
image_urls.append(self.getImage(image, 'file', size))
|
||||
|
||||
except:
|
||||
log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie))))
|
||||
|
||||
return image_urls
|
||||
|
||||
def request(self, call = '', params = {}, return_key = None):
|
||||
|
||||
params = dict((k, v) for k, v in params.items() if v)
|
||||
params = tryUrlencode(params)
|
||||
url = 'http://api.themoviedb.org/3/%s?api_key=%s%s' % (call, self.conf('api_key'), '&%s' % params if params else '')
|
||||
data = self.getJsonData(url, cache_timeout = 0)
|
||||
|
||||
try:
|
||||
url = 'http://api.themoviedb.org/3/%s?api_key=%s%s' % (call, self.conf('api_key'), '&%s' % params if params else '')
|
||||
data = self.getJsonData(url, show_error = False)
|
||||
except:
|
||||
log.debug('Movie not found: %s, %s', (call, params))
|
||||
data = None
|
||||
|
||||
if data and return_key and return_key in data:
|
||||
if data and return_key and data.get(return_key):
|
||||
data = data.get(return_key)
|
||||
|
||||
return data
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.torrentleech import Base
|
||||
from couchpotato.core.media.movie.providers.base import MovieProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TorrentLeech'
|
||||
|
||||
|
||||
class TorrentLeech(MovieProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([13], ['720p', '1080p', 'bd50']),
|
||||
([8], ['cam']),
|
||||
([9], ['ts', 'tc']),
|
||||
([10], ['r5', 'scr']),
|
||||
([11], ['dvdrip']),
|
||||
([14], ['brrip']),
|
||||
([12], ['dvdr']),
|
||||
]
|
||||
|
||||
def buildUrl(self, title, media, quality):
|
||||
return (
|
||||
tryUrlencode(title.replace(':', '')),
|
||||
self.getCatId(quality)[0]
|
||||
)
|
||||
@@ -166,8 +166,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
'quality': q_identifier,
|
||||
'finish': profile['finish'][index],
|
||||
'wait_for': tryInt(profile['wait_for'][index]),
|
||||
'3d': profile['3d'][index] if profile.get('3d') else False,
|
||||
'minimum_score': profile.get('minimum_score', 1),
|
||||
'3d': profile['3d'][index] if profile.get('3d') else False
|
||||
}
|
||||
|
||||
could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year'])
|
||||
@@ -203,14 +202,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
quality['custom'] = quality_custom
|
||||
|
||||
results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
|
||||
|
||||
# Check if movie isn't deleted while searching
|
||||
if not fireEvent('media.get', movie.get('_id'), single = True):
|
||||
break
|
||||
|
||||
# Add them to this movie releases list
|
||||
found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True)
|
||||
results_count = len(found_releases)
|
||||
results_count = len(results)
|
||||
total_result_count += results_count
|
||||
if results_count == 0:
|
||||
log.debug('Nothing found for %s in %s', (default_title, quality['label']))
|
||||
@@ -218,9 +210,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
# Keep track of releases found outside ETA window
|
||||
outside_eta_results += results_count if could_not_be_released else 0
|
||||
|
||||
# Check if movie isn't deleted while searching
|
||||
if not fireEvent('media.get', movie.get('_id'), single = True):
|
||||
break
|
||||
|
||||
# Add them to this movie releases list
|
||||
found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True)
|
||||
|
||||
# Don't trigger download, but notify user of available releases
|
||||
if could_not_be_released and results_count > 0:
|
||||
log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title))
|
||||
if could_not_be_released:
|
||||
if results_count > 0:
|
||||
log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title))
|
||||
|
||||
# Try find a valid result and download it
|
||||
if (force_download or not could_not_be_released or always_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True):
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
class ShowTypeBase(MediaBase):
|
||||
_type = 'show'
|
||||
|
||||
def getType(self):
|
||||
if hasattr(self, 'type') and self.type != self._type:
|
||||
return '%s.%s' % (self._type, self.type)
|
||||
|
||||
return self._type
|
||||
@@ -1,4 +0,0 @@
|
||||
from .main import ShowBase
|
||||
|
||||
def autoload():
|
||||
return ShowBase()
|
||||
@@ -1,109 +0,0 @@
|
||||
from couchpotato import get_db
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Episode'
|
||||
|
||||
|
||||
class Episode(MediaBase):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('show.episode.add', self.add)
|
||||
addEvent('show.episode.update', self.update)
|
||||
addEvent('show.episode.update_extras', self.updateExtras)
|
||||
|
||||
def add(self, parent_id, info = None, update_after = True, status = None):
|
||||
if not info: info = {}
|
||||
|
||||
identifiers = info.pop('identifiers', None)
|
||||
|
||||
if not identifiers:
|
||||
log.warning('Unable to add episode, missing identifiers (info provider mismatch?)')
|
||||
return
|
||||
|
||||
# Add Season
|
||||
episode_info = {
|
||||
'_t': 'media',
|
||||
'type': 'show.episode',
|
||||
'identifiers': identifiers,
|
||||
'status': status if status else 'active',
|
||||
'parent_id': parent_id,
|
||||
'info': info, # Returned dict by providers
|
||||
}
|
||||
|
||||
# Check if season already exists
|
||||
existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
|
||||
|
||||
db = get_db()
|
||||
|
||||
if existing_episode:
|
||||
s = existing_episode['doc']
|
||||
s.update(episode_info)
|
||||
|
||||
episode = db.update(s)
|
||||
else:
|
||||
episode = db.insert(episode_info)
|
||||
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('show.episode.update_extras', episode, info, store = True, single = True)
|
||||
|
||||
return episode
|
||||
|
||||
def update(self, media_id = None, identifiers = None, info = None):
|
||||
if not info: info = {}
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_db()
|
||||
|
||||
episode = db.get('id', media_id)
|
||||
|
||||
# Get new info
|
||||
if not info:
|
||||
season = db.get('id', episode['parent_id'])
|
||||
show = db.get('id', season['parent_id'])
|
||||
|
||||
info = fireEvent(
|
||||
'episode.info', show.get('identifiers'), {
|
||||
'season_identifiers': season.get('identifiers'),
|
||||
'season_number': season.get('info', {}).get('number'),
|
||||
|
||||
'episode_identifiers': episode.get('identifiers'),
|
||||
'episode_number': episode.get('info', {}).get('number'),
|
||||
|
||||
'absolute_number': episode.get('info', {}).get('absolute_number')
|
||||
},
|
||||
merge = True
|
||||
)
|
||||
|
||||
info['season_number'] = season.get('info', {}).get('number')
|
||||
|
||||
identifiers = info.pop('identifiers', None) or identifiers
|
||||
|
||||
# Update/create media
|
||||
episode['identifiers'].update(identifiers)
|
||||
episode.update({'info': info})
|
||||
|
||||
self.updateExtras(episode, info)
|
||||
|
||||
db.update(episode)
|
||||
return episode
|
||||
|
||||
def updateExtras(self, episode, info, store=False):
|
||||
db = get_db()
|
||||
|
||||
# Get images
|
||||
image_urls = info.get('images', [])
|
||||
existing_files = episode.get('files', {})
|
||||
self.getPoster(image_urls, existing_files)
|
||||
|
||||
if store:
|
||||
db.update(episode)
|
||||
@@ -1,291 +0,0 @@
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.variable import getTitle, find
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ShowBase(MediaBase):
|
||||
|
||||
_type = 'show'
|
||||
|
||||
def __init__(self):
|
||||
super(ShowBase, self).__init__()
|
||||
self.initType()
|
||||
|
||||
addApiView('show.add', self.addView, docs = {
|
||||
'desc': 'Add new show to the wanted list',
|
||||
'params': {
|
||||
'identifier': {'desc': 'IMDB id of the show your want to add.'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the add the show in. If empty will use the default profile.'},
|
||||
'category_id': {'desc': 'ID of category you want the add the show in.'},
|
||||
'title': {'desc': 'Title of the show to use for search and renaming'},
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('show.add', self.add)
|
||||
addEvent('show.update', self.update)
|
||||
addEvent('show.update_extras', self.updateExtras)
|
||||
|
||||
def addView(self, **kwargs):
|
||||
add_dict = self.add(params = kwargs)
|
||||
|
||||
return {
|
||||
'success': True if add_dict else False,
|
||||
'show': add_dict,
|
||||
}
|
||||
|
||||
def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
|
||||
if not params: params = {}
|
||||
|
||||
# Identifiers
|
||||
if not params.get('identifiers'):
|
||||
msg = 'Can\'t add show without at least 1 identifier.'
|
||||
log.error(msg)
|
||||
fireEvent('notify.frontend', type = 'show.no_identifier', message = msg)
|
||||
return False
|
||||
|
||||
info = params.get('info')
|
||||
if not info or (info and len(info.get('titles', [])) == 0):
|
||||
info = fireEvent('show.info', merge = True, identifiers = params.get('identifiers'))
|
||||
|
||||
# Add Show
|
||||
try:
|
||||
m, added = self.create(info, params, force_readd, search_after, update_after)
|
||||
|
||||
result = fireEvent('media.get', m['_id'], single = True)
|
||||
|
||||
if added and notify_after:
|
||||
if params.get('title'):
|
||||
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')
|
||||
else:
|
||||
title = getTitle(m)
|
||||
if title:
|
||||
message = 'Successfully added "%s" to your wanted list.' % title
|
||||
else:
|
||||
message = 'Successfully added to your wanted list.'
|
||||
|
||||
fireEvent('notify.frontend', type = 'show.added', data = result, message = message)
|
||||
|
||||
return result
|
||||
except:
|
||||
log.error('Failed adding media: %s', traceback.format_exc())
|
||||
|
||||
def create(self, info, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
|
||||
# Set default title
|
||||
def_title = self.getDefaultTitle(info)
|
||||
|
||||
# Default profile and category
|
||||
default_profile = {}
|
||||
if not params.get('profile_id'):
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
|
||||
cat_id = params.get('category_id')
|
||||
|
||||
media = {
|
||||
'_t': 'media',
|
||||
'type': 'show',
|
||||
'title': def_title,
|
||||
'identifiers': info.get('identifiers'),
|
||||
'status': status if status else 'active',
|
||||
'profile_id': params.get('profile_id', default_profile.get('_id')),
|
||||
'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None
|
||||
}
|
||||
|
||||
identifiers = info.pop('identifiers', {})
|
||||
seasons = info.pop('seasons', {})
|
||||
|
||||
# Update media with info
|
||||
self.updateInfo(media, info)
|
||||
|
||||
existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True)
|
||||
|
||||
db = get_db()
|
||||
|
||||
if existing_show:
|
||||
s = existing_show['doc']
|
||||
s.update(media)
|
||||
|
||||
show = db.update(s)
|
||||
else:
|
||||
show = db.insert(media)
|
||||
|
||||
# Update dict to be usable
|
||||
show.update(media)
|
||||
|
||||
added = True
|
||||
do_search = False
|
||||
search_after = search_after and self.conf('search_on_add', section = 'showsearcher')
|
||||
onComplete = None
|
||||
|
||||
if existing_show:
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(show['_id'])
|
||||
|
||||
search_after = False
|
||||
elif force_readd:
|
||||
# Clean snatched history
|
||||
for release in fireEvent('release.for_media', show['_id'], single = True):
|
||||
if release.get('status') in ['downloaded', 'snatched', 'done']:
|
||||
if params.get('ignore_previous', False):
|
||||
release['status'] = 'ignored'
|
||||
db.update(release)
|
||||
else:
|
||||
fireEvent('release.delete', release['_id'], single = True)
|
||||
|
||||
show['profile_id'] = params.get('profile_id', default_profile.get('id'))
|
||||
show['category_id'] = media.get('category_id')
|
||||
show['last_edit'] = int(time.time())
|
||||
|
||||
do_search = True
|
||||
db.update(show)
|
||||
else:
|
||||
params.pop('info', None)
|
||||
log.debug('Show already exists, not updating: %s', params)
|
||||
added = False
|
||||
|
||||
# Create episodes
|
||||
self.createEpisodes(show, seasons)
|
||||
|
||||
# Trigger update info
|
||||
if added and update_after:
|
||||
# Do full update to get images etc
|
||||
fireEventAsync('show.update_extras', show.copy(), info, store = True, on_complete = onComplete)
|
||||
|
||||
# Remove releases
|
||||
for rel in fireEvent('release.for_media', show['_id'], single = True):
|
||||
if rel['status'] is 'available':
|
||||
db.delete(rel)
|
||||
|
||||
if do_search and search_after:
|
||||
onComplete = self.createOnComplete(show['_id'])
|
||||
onComplete()
|
||||
|
||||
return show, added
|
||||
|
||||
def createEpisodes(self, m, seasons_info):
|
||||
# Add Seasons
|
||||
for season_nr in seasons_info:
|
||||
season_info = seasons_info[season_nr]
|
||||
episodes = season_info.get('episodes', {})
|
||||
|
||||
season = fireEvent('show.season.add', m.get('_id'), season_info, update_after = False, single = True)
|
||||
|
||||
# Add Episodes
|
||||
for episode_nr in episodes:
|
||||
episode_info = episodes[episode_nr]
|
||||
episode_info['season_number'] = season_nr
|
||||
|
||||
fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True)
|
||||
|
||||
def update(self, media_id = None, media = None, identifiers = None, info = None):
|
||||
"""
|
||||
Update movie information inside media['doc']['info']
|
||||
|
||||
@param media_id: document id
|
||||
@param identifiers: identifiers from multiple providers
|
||||
{
|
||||
'thetvdb': 123,
|
||||
'imdb': 'tt123123',
|
||||
..
|
||||
}
|
||||
@param extended: update with extended info (parses more info, actors, images from some info providers)
|
||||
@return: dict, with media
|
||||
"""
|
||||
|
||||
if not info: info = {}
|
||||
if not identifiers: identifiers = {}
|
||||
|
||||
db = get_db()
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
if media is None and media_id:
|
||||
media = db.get('id', media_id)
|
||||
else:
|
||||
log.error('missing "media" and "media_id" parameters, unable to update')
|
||||
return
|
||||
|
||||
if not info:
|
||||
info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True)
|
||||
|
||||
try:
|
||||
identifiers = info.pop('identifiers', {})
|
||||
seasons = info.pop('seasons', {})
|
||||
|
||||
self.updateInfo(media, info)
|
||||
self.updateEpisodes(media, seasons)
|
||||
self.updateExtras(media, info)
|
||||
|
||||
db.update(media)
|
||||
return media
|
||||
except:
|
||||
log.error('Failed update media: %s', traceback.format_exc())
|
||||
|
||||
return {}
|
||||
|
||||
def updateInfo(self, media, info):
|
||||
db = get_db()
|
||||
|
||||
# Remove season info for later use (save separately)
|
||||
info.pop('in_wanted', None)
|
||||
info.pop('in_library', None)
|
||||
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no show info to work with: %s', media.get('identifier'))
|
||||
return False
|
||||
|
||||
# Update basic info
|
||||
media['info'] = info
|
||||
|
||||
def updateEpisodes(self, media, seasons):
|
||||
# Fetch current season/episode tree
|
||||
show_tree = fireEvent('library.tree', media_id = media['_id'], single = True)
|
||||
|
||||
# Update seasons
|
||||
for season_num in seasons:
|
||||
season_info = seasons[season_num]
|
||||
episodes = season_info.get('episodes', {})
|
||||
|
||||
# Find season that matches number
|
||||
season = find(lambda s: s.get('info', {}).get('number', 0) == season_num, show_tree.get('seasons', []))
|
||||
|
||||
if not season:
|
||||
log.warning('Unable to find season "%s"', season_num)
|
||||
continue
|
||||
|
||||
# Update season
|
||||
fireEvent('show.season.update', season['_id'], info = season_info, single = True)
|
||||
|
||||
# Update episodes
|
||||
for episode_num in episodes:
|
||||
episode_info = episodes[episode_num]
|
||||
episode_info['season_number'] = season_num
|
||||
|
||||
# Find episode that matches number
|
||||
episode = find(lambda s: s.get('info', {}).get('number', 0) == episode_num, season.get('episodes', []))
|
||||
|
||||
if not episode:
|
||||
log.debug('Creating new episode %s in season %s', (episode_num, season_num))
|
||||
fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True)
|
||||
continue
|
||||
|
||||
fireEvent('show.episode.update', episode['_id'], info = episode_info, single = True)
|
||||
|
||||
def updateExtras(self, media, info, store=False):
|
||||
db = get_db()
|
||||
|
||||
# Update image file
|
||||
image_urls = info.get('images', [])
|
||||
self.getPoster(media, image_urls)
|
||||
|
||||
if store:
|
||||
db.update(media)
|
||||
@@ -1,94 +0,0 @@
|
||||
from couchpotato import get_db
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Season'
|
||||
|
||||
|
||||
class Season(MediaBase):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('show.season.add', self.add)
|
||||
addEvent('show.season.update', self.update)
|
||||
addEvent('show.season.update_extras', self.updateExtras)
|
||||
|
||||
def add(self, parent_id, info = None, update_after = True, status = None):
|
||||
if not info: info = {}
|
||||
|
||||
identifiers = info.pop('identifiers', None)
|
||||
info.pop('episodes', None)
|
||||
|
||||
# Add Season
|
||||
season_info = {
|
||||
'_t': 'media',
|
||||
'type': 'show.season',
|
||||
'identifiers': identifiers,
|
||||
'status': status if status else 'active',
|
||||
'parent_id': parent_id,
|
||||
'info': info, # Returned dict by providers
|
||||
}
|
||||
|
||||
# Check if season already exists
|
||||
existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
|
||||
|
||||
db = get_db()
|
||||
|
||||
if existing_season:
|
||||
s = existing_season['doc']
|
||||
s.update(season_info)
|
||||
|
||||
season = db.update(s)
|
||||
else:
|
||||
season = db.insert(season_info)
|
||||
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('show.season.update_extras', season, info, store = True, single = True)
|
||||
|
||||
return season
|
||||
|
||||
def update(self, media_id = None, identifiers = None, info = None):
|
||||
if not info: info = {}
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_db()
|
||||
|
||||
season = db.get('id', media_id)
|
||||
show = db.get('id', season['parent_id'])
|
||||
|
||||
# Get new info
|
||||
if not info:
|
||||
info = fireEvent('season.info', show.get('identifiers'), {
|
||||
'season_number': season.get('info', {}).get('number', 0)
|
||||
}, merge = True)
|
||||
|
||||
identifiers = info.pop('identifiers', None) or identifiers
|
||||
info.pop('episodes', None)
|
||||
|
||||
# Update/create media
|
||||
season['identifiers'].update(identifiers)
|
||||
season.update({'info': info})
|
||||
|
||||
self.updateExtras(season, info)
|
||||
|
||||
db.update(season)
|
||||
return season
|
||||
|
||||
def updateExtras(self, season, info, store=False):
|
||||
db = get_db()
|
||||
|
||||
# Get images
|
||||
image_urls = info.get('images', [])
|
||||
existing_files = season.get('files', {})
|
||||
self.getPoster(image_urls, existing_files)
|
||||
|
||||
if store:
|
||||
db.update(season)
|
||||
@@ -1,28 +0,0 @@
|
||||
Page.Shows = new Class({
|
||||
|
||||
Extends: PageBase,
|
||||
|
||||
name: 'shows',
|
||||
title: 'Gimmy gimmy gimmy!',
|
||||
folder_browser: null,
|
||||
|
||||
indexAction: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.wanted){
|
||||
|
||||
// Wanted movies
|
||||
self.wanted = new ShowList({
|
||||
'identifier': 'wanted',
|
||||
'status': 'active',
|
||||
'type': 'show',
|
||||
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete],
|
||||
'add_new': true,
|
||||
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
|
||||
});
|
||||
$(self.wanted).inject(self.el);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,474 +0,0 @@
|
||||
var EpisodeAction = new Class({
|
||||
|
||||
Implements: [Options],
|
||||
|
||||
class_name: 'item-action icon2',
|
||||
|
||||
initialize: function(episode, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.show = episode.show;
|
||||
self.episode = episode;
|
||||
|
||||
self.create();
|
||||
if(self.el)
|
||||
self.el.addClass(self.class_name)
|
||||
},
|
||||
|
||||
create: function(){},
|
||||
|
||||
disable: function(){
|
||||
if(this.el)
|
||||
this.el.addClass('disable')
|
||||
},
|
||||
|
||||
enable: function(){
|
||||
if(this.el)
|
||||
this.el.removeClass('disable')
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.show.getTitle();
|
||||
}
|
||||
catch(e){
|
||||
try {
|
||||
return self.show.original_title ? self.show.original_title : self.show.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
var self = this;
|
||||
try {
|
||||
return self.show.get(key)
|
||||
}
|
||||
catch(e){
|
||||
return self.show[key]
|
||||
}
|
||||
},
|
||||
|
||||
createMask: function(){
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
'styles': {
|
||||
'z-index': '1'
|
||||
}
|
||||
}).inject(self.show, 'top').fade('hide');
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el || null
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var EA = {};
|
||||
|
||||
EA.IMDB = new Class({
|
||||
|
||||
Extends: EpisodeAction,
|
||||
id: null,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.show.getIdentifier ? self.show.getIdentifier() : self.get('imdb');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'title': 'Go to the IMDB page of ' + self.getTitle(),
|
||||
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||
'target': '_blank'
|
||||
});
|
||||
|
||||
if(!self.id) self.disable();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
EA.Release = new Class({
|
||||
|
||||
Extends: EpisodeAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.download', {
|
||||
'title': 'Show the releases that are available for ' + self.getTitle(),
|
||||
'events': {
|
||||
'click': self.toggle.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
self.options = new Element('div.episode-options').inject(self.episode.el);
|
||||
|
||||
if(!self.episode.data.releases || self.episode.data.releases.length == 0)
|
||||
self.el.hide();
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
App.on('show.searcher.ended', function(notification){
|
||||
if(self.show.data._id != notification.data._id) return;
|
||||
|
||||
self.releases = null;
|
||||
if(self.options_container){
|
||||
self.options_container.destroy();
|
||||
self.options_container = null;
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
toggle: function(e){
|
||||
var self = this;
|
||||
|
||||
if(self.options && self.options.hasClass('expanded')) {
|
||||
self.close();
|
||||
} else {
|
||||
self.open();
|
||||
}
|
||||
},
|
||||
|
||||
open: function(e){
|
||||
var self = this;
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
|
||||
},
|
||||
|
||||
close: function(e) {
|
||||
var self = this;
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.options.setStyle('height', 0)
|
||||
.removeClass('expanded');
|
||||
},
|
||||
|
||||
createReleases: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.releases_table){
|
||||
self.options.adopt(
|
||||
self.releases_table = new Element('div.releases.table')
|
||||
);
|
||||
|
||||
// Header
|
||||
new Element('div.item.head').adopt(
|
||||
new Element('span.name', {'text': 'Release name'}),
|
||||
new Element('span.status', {'text': 'Status'}),
|
||||
new Element('span.quality', {'text': 'Quality'}),
|
||||
new Element('span.size', {'text': 'Size'}),
|
||||
new Element('span.age', {'text': 'Age'}),
|
||||
new Element('span.score', {'text': 'Score'}),
|
||||
new Element('span.provider', {'text': 'Provider'})
|
||||
).inject(self.releases_table);
|
||||
|
||||
if(self.episode.data.releases)
|
||||
self.episode.data.releases.each(function(release){
|
||||
|
||||
var quality = Quality.getQuality(release.quality) || {},
|
||||
info = release.info || {},
|
||||
provider = self.get(release, 'provider') + (info['provider_extra'] ? self.get(release, 'provider_extra') : '');
|
||||
|
||||
var release_name = self.get(release, 'name');
|
||||
if(release.files && release.files.length > 0){
|
||||
try {
|
||||
var movie_file = release.files.filter(function(file){
|
||||
var type = File.Type.get(file.type_id);
|
||||
return type && type.identifier == 'movie'
|
||||
}).pick();
|
||||
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
|
||||
}
|
||||
catch(e){}
|
||||
}
|
||||
|
||||
// Create release
|
||||
release['el'] = new Element('div', {
|
||||
'class': 'item '+release.status,
|
||||
'id': 'release_'+release._id
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': release_name, 'title': release_name}),
|
||||
new Element('span.status', {'text': release.status, 'class': 'status '+release.status}),
|
||||
new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}),
|
||||
new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
if(!this.hasClass('completed'))
|
||||
self.download(release);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.releases_table);
|
||||
|
||||
if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){
|
||||
if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched'))
|
||||
self.last_release = release;
|
||||
}
|
||||
else if(!self.next_release && release.status == 'available'){
|
||||
self.next_release = release;
|
||||
}
|
||||
|
||||
var update_handle = function(notification) {
|
||||
if(notification.data._id != release._id) return;
|
||||
|
||||
var q = self.show.quality.getElement('.q_' + release.quality),
|
||||
new_status = notification.data.status;
|
||||
|
||||
release.el.set('class', 'item ' + new_status);
|
||||
|
||||
var status_el = release.el.getElement('.release_status');
|
||||
status_el.set('class', 'release_status ' + new_status);
|
||||
status_el.set('text', new_status);
|
||||
|
||||
if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done'))
|
||||
q = self.addQuality(release.quality_id);
|
||||
|
||||
if(q && !q.hasClass(new_status)) {
|
||||
q.removeClass(release.status).addClass(new_status);
|
||||
q.set('title', q.get('title').replace(release.status, new_status));
|
||||
}
|
||||
};
|
||||
|
||||
App.on('release.update_status', update_handle);
|
||||
|
||||
});
|
||||
|
||||
if(self.last_release)
|
||||
self.releases_table.getElements('#release_'+self.last_release._id).addClass('last_release');
|
||||
|
||||
if(self.next_release)
|
||||
self.releases_table.getElements('#release_'+self.next_release._id).addClass('next_release');
|
||||
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.releases_table, 'top');
|
||||
|
||||
var nr = self.next_release,
|
||||
lr = self.last_release;
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
'text': 'If anything went wrong, download'
|
||||
}),
|
||||
lr ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.download(lr);
|
||||
}
|
||||
}
|
||||
}) : null,
|
||||
nr && lr ? new Element('span.or', {
|
||||
'text': ','
|
||||
}) : null,
|
||||
nr ? [new Element('a.button.green', {
|
||||
'text': lr ? 'another release' : 'the best release',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.download(nr);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('span.or', {
|
||||
'text': 'or pick one below'
|
||||
})] : null
|
||||
)
|
||||
}
|
||||
|
||||
self.last_release = null;
|
||||
self.next_release = null;
|
||||
|
||||
self.episode.el.addEvent('outerClick', function(){
|
||||
self.close();
|
||||
});
|
||||
}
|
||||
|
||||
self.options.setStyle('height', self.releases_table.getSize().y)
|
||||
.addClass('expanded');
|
||||
|
||||
},
|
||||
|
||||
showHelper: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
var has_available = false,
|
||||
has_snatched = false;
|
||||
|
||||
if(self.episode.data.releases)
|
||||
self.episode.data.releases.each(function(release){
|
||||
if(has_available && has_snatched) return;
|
||||
|
||||
if(['snatched', 'downloaded', 'seeding'].contains(release.status))
|
||||
has_snatched = true;
|
||||
|
||||
if(['available'].contains(release.status))
|
||||
has_available = true;
|
||||
|
||||
});
|
||||
|
||||
if(has_available || has_snatched){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.trynext').inject(self.show.info_container);
|
||||
|
||||
self.trynext_container.adopt(
|
||||
has_available ? [new Element('a.icon2.readd', {
|
||||
'text': has_snatched ? 'Download another release' : 'Download the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('a.icon2.download', {
|
||||
'text': 'pick one yourself',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.show.quality.fireEvent('click');
|
||||
}
|
||||
}
|
||||
})] : null,
|
||||
new Element('a.icon2.completed', {
|
||||
'text': 'mark this movie done',
|
||||
'events': {
|
||||
'click': self.markMovieDone.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
get: function(release, type){
|
||||
return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a'
|
||||
},
|
||||
|
||||
download: function(release){
|
||||
var self = this;
|
||||
|
||||
var release_el = self.releases_table.getElement('#release_'+release._id),
|
||||
icon = release_el.getElement('.download.icon2');
|
||||
|
||||
if(icon)
|
||||
icon.addClass('icon spinner').removeClass('download');
|
||||
|
||||
Api.request('release.manual_download', {
|
||||
'data': {
|
||||
'id': release._id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(icon)
|
||||
icon.removeClass('icon spinner');
|
||||
|
||||
if(json.success){
|
||||
if(icon)
|
||||
icon.addClass('completed');
|
||||
release_el.getElement('.release_status').set('text', 'snatched');
|
||||
}
|
||||
else
|
||||
if(icon)
|
||||
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
ignore: function(release){
|
||||
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release._id
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
markMovieDone: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': self.show.get('_id'),
|
||||
'delete_from': 'wanted'
|
||||
},
|
||||
'onComplete': function(){
|
||||
var movie = $(self.show);
|
||||
movie.set('tween', {
|
||||
'duration': 300,
|
||||
'onComplete': function(){
|
||||
self.show.destroy()
|
||||
}
|
||||
});
|
||||
movie.tween('height', 0);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
tryNextRelease: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.searcher.try_next', {
|
||||
'data': {
|
||||
'media_id': self.show.get('_id')
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
EA.Refresh = new Class({
|
||||
|
||||
Extends: EpisodeAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.refresh', {
|
||||
'title': 'Refresh the movie info and do a forced search',
|
||||
'events': {
|
||||
'click': self.doRefresh.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
doRefresh: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
Api.request('media.refresh', {
|
||||
'data': {
|
||||
'id': self.episode.get('_id')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
var Episode = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
action: {},
|
||||
|
||||
initialize: function(show, options, data){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.show = show;
|
||||
self.options = options;
|
||||
self.data = data;
|
||||
|
||||
self.profile = self.show.profile;
|
||||
|
||||
self.el = new Element('div.item.episode').adopt(
|
||||
self.detail = new Element('div.item.data')
|
||||
);
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.detail.set('id', 'episode_'+self.data._id);
|
||||
|
||||
self.detail.adopt(
|
||||
new Element('span.episode', {'text': (self.data.info.number || 0)}),
|
||||
new Element('span.name', {'text': self.getTitle()}),
|
||||
new Element('span.firstaired', {'text': self.data.info.firstaired}),
|
||||
|
||||
self.quality = new Element('span.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.detail.getElement('.item-actions .releases');
|
||||
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.actions = new Element('div.item-actions')
|
||||
);
|
||||
|
||||
// Add profile
|
||||
if(self.profile.data) {
|
||||
self.profile.getTypes().each(function(type){
|
||||
var q = self.addQuality(type.get('quality'), type.get('3d'));
|
||||
|
||||
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
|
||||
q.addClass('finish');
|
||||
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add releases
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
|
||||
if(action.el)
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
|
||||
status = release.status;
|
||||
|
||||
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
|
||||
q = self.addQuality(release.quality, release.is_3d || false);
|
||||
|
||||
if (q && !q.hasClass(status)){
|
||||
q.addClass(status);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality, is_3d){
|
||||
var self = this,
|
||||
q = Quality.getQuality(quality);
|
||||
|
||||
return new Element('span', {
|
||||
'text': q.label + (is_3d ? ' 3D' : ''),
|
||||
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
|
||||
'title': ''
|
||||
}).inject(self.quality);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
var title = '';
|
||||
|
||||
if(self.data.info.titles && self.data.info.titles.length > 0) {
|
||||
title = self.data.info.titles[0];
|
||||
} else {
|
||||
title = 'Episode ' + self.data.info.number;
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
getIdentifier: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.get('identifiers').imdb;
|
||||
}
|
||||
catch (e){ }
|
||||
|
||||
return self.get('imdb');
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.info[attr]
|
||||
}
|
||||
});
|
||||
@@ -1,636 +0,0 @@
|
||||
var ShowList = new Class({
|
||||
|
||||
Implements: [Events, Options],
|
||||
|
||||
options: {
|
||||
navigation: true,
|
||||
limit: 50,
|
||||
load_more: true,
|
||||
loader: true,
|
||||
menu: [],
|
||||
add_new: false,
|
||||
force_view: false
|
||||
},
|
||||
|
||||
movies: [],
|
||||
movies_added: {},
|
||||
total_movies: 0,
|
||||
letters: {},
|
||||
filter: null,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.offset = 0;
|
||||
self.filter = self.options.filter || {
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
};
|
||||
|
||||
self.el = new Element('div.shows').adopt(
|
||||
self.title = self.options.title ? new Element('h2', {
|
||||
'text': self.options.title,
|
||||
'styles': {'display': 'none'}
|
||||
}) : null,
|
||||
self.description = self.options.description ? new Element('div.description', {
|
||||
'html': self.options.description,
|
||||
'styles': {'display': 'none'}
|
||||
}) : null,
|
||||
self.movie_list = new Element('div.list'),
|
||||
self.load_more = self.options.load_more ? new Element('a.load_more', {
|
||||
'events': {
|
||||
'click': self.loadMore.bind(self)
|
||||
}
|
||||
}) : null
|
||||
);
|
||||
|
||||
if($(window).getSize().x <= 480 && !self.options.force_view)
|
||||
self.changeView('list');
|
||||
else
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
|
||||
self.getMovies();
|
||||
|
||||
App.on('movie.added', self.movieAdded.bind(self));
|
||||
App.on('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.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.checkIfEmpty();
|
||||
},
|
||||
|
||||
movieAdded: function(notification){
|
||||
var self = this;
|
||||
|
||||
self.fireEvent('movieAdded', notification);
|
||||
if(self.options.add_new && !self.movies_added[notification.data._id] && notification.data.status == self.options.status){
|
||||
window.scroll(0,0);
|
||||
self.createShow(notification.data, 'top');
|
||||
self.setCounter(self.counter_count+1);
|
||||
|
||||
self.checkIfEmpty();
|
||||
}
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
// Create the alphabet nav
|
||||
if(self.options.navigation)
|
||||
self.createNavigation();
|
||||
|
||||
if(self.options.load_more)
|
||||
self.scrollspy = new ScrollSpy({
|
||||
min: function(){
|
||||
var c = self.load_more.getCoordinates();
|
||||
return c.top - window.document.getSize().y - 300
|
||||
},
|
||||
onEnter: self.loadMore.bind(self)
|
||||
});
|
||||
|
||||
self.created = true;
|
||||
},
|
||||
|
||||
addMovies: function(movies, total){
|
||||
var self = this;
|
||||
|
||||
if(!self.created) self.create();
|
||||
|
||||
// do scrollspy
|
||||
if(movies.length < self.options.limit && self.scrollspy){
|
||||
self.load_more.hide();
|
||||
self.scrollspy.stop();
|
||||
}
|
||||
|
||||
Object.each(movies, function(movie){
|
||||
self.createShow(movie);
|
||||
});
|
||||
|
||||
self.total_movies += total;
|
||||
self.setCounter(total);
|
||||
|
||||
},
|
||||
|
||||
setCounter: function(count){
|
||||
var self = this;
|
||||
|
||||
if(!self.navigation_counter) return;
|
||||
|
||||
self.counter_count = count;
|
||||
self.navigation_counter.set('text', (count || 0) + ' shows');
|
||||
|
||||
if (self.empty_message) {
|
||||
self.empty_message.destroy();
|
||||
self.empty_message = null;
|
||||
}
|
||||
|
||||
if(self.total_movies && count == 0 && !self.empty_message){
|
||||
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
|
||||
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
|
||||
|
||||
self.empty_message = new Element('.message', {
|
||||
'html': 'No shows found ' + message + '.<br/>'
|
||||
}).grab(
|
||||
new Element('a', {
|
||||
'text': 'Reset filter',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.filter = {
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
};
|
||||
self.navigation_search_input.set('value', '');
|
||||
self.reset();
|
||||
self.activateLetter();
|
||||
self.getMovies(true);
|
||||
self.last_search_value = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.movie_list);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
createShow: function(show, inject_at){
|
||||
var self = this;
|
||||
var m = new Show(self, {
|
||||
'actions': self.options.actions,
|
||||
'view': self.current_view,
|
||||
'onSelect': self.calculateSelected.bind(self)
|
||||
}, show);
|
||||
|
||||
$(m).inject(self.movie_list, inject_at || 'bottom');
|
||||
|
||||
m.fireEvent('injected');
|
||||
|
||||
self.movies.include(m);
|
||||
self.movies_added[show._id] = true;
|
||||
},
|
||||
|
||||
createNavigation: function(){
|
||||
var self = this;
|
||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
self.el.addClass('with_navigation');
|
||||
|
||||
self.navigation = new Element('div.alph_nav').adopt(
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': self.massEditToggleAll.bind(self)
|
||||
}
|
||||
}),
|
||||
self.mass_edit_selected = new Element('span.count', {'text': 0}),
|
||||
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
|
||||
),
|
||||
new Element('div.quality').adopt(
|
||||
self.mass_edit_quality = new Element('select'),
|
||||
new Element('a.button.orange', {
|
||||
'text': 'Change quality',
|
||||
'events': {
|
||||
'click': self.changeQualitySelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.delete').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.red', {
|
||||
'text': 'Delete',
|
||||
'events': {
|
||||
'click': self.deleteSelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.refresh').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.green', {
|
||||
'text': 'Refresh',
|
||||
'events': {
|
||||
'click': self.refreshSelected.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
new Element('div.menus').adopt(
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.filter_menu = new Block.Menu(self, {
|
||||
'class': 'filter'
|
||||
}),
|
||||
self.navigation_actions = new Element('ul.actions', {
|
||||
'events': {
|
||||
'click:relay(li)': function(e, el){
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(el.get('data-view'));
|
||||
this.addClass(a);
|
||||
|
||||
el.inject(el.getParent(), 'top');
|
||||
el.getSiblings().hide();
|
||||
setTimeout(function(){
|
||||
el.getSiblings().setStyle('display', null);
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.navigation_menu = new Block.Menu(self, {
|
||||
'class': 'extra'
|
||||
})
|
||||
)
|
||||
).inject(self.el, 'top');
|
||||
|
||||
// Mass edit
|
||||
self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
|
||||
Quality.getActiveProfiles().each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.get('_id'),
|
||||
'text': profile.get('label')
|
||||
}).inject(self.mass_edit_quality)
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_search_input = new Element('input', {
|
||||
'title': 'Search through ' + self.options.identifier,
|
||||
'placeholder': 'Search through ' + self.options.identifier,
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
})
|
||||
).addClass('search');
|
||||
|
||||
var available_chars;
|
||||
self.filter_menu.addEvent('open', function(){
|
||||
self.navigation_search_input.focus();
|
||||
|
||||
// Get available chars and highlight
|
||||
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
|
||||
Api.request('media.available_chars', {
|
||||
'data': Object.merge({
|
||||
'type': 'show',
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
available_chars = json.chars;
|
||||
|
||||
available_chars.each(function(c){
|
||||
self.letters[c.capitalize()].addClass('available')
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
'events': {
|
||||
'click:relay(li.available)': function(e, el){
|
||||
self.activateLetter(el.get('data-letter'));
|
||||
self.getMovies(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Actions
|
||||
['mass_edit', 'details', 'list'].each(function(view){
|
||||
var current = self.current_view == view;
|
||||
new Element('li', {
|
||||
'class': 'icon2 ' + view + (current ? ' active ' : ''),
|
||||
'data-view': view
|
||||
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
|
||||
});
|
||||
|
||||
// All
|
||||
self.letters['all'] = new Element('li.letter_all.available.active', {
|
||||
'text': 'ALL'
|
||||
}).inject(self.navigation_alpha);
|
||||
|
||||
// Chars
|
||||
chars.split('').each(function(c){
|
||||
self.letters[c] = new Element('li', {
|
||||
'text': c,
|
||||
'class': 'letter_'+c,
|
||||
'data-letter': c
|
||||
}).inject(self.navigation_alpha);
|
||||
});
|
||||
|
||||
// Add menu or hide
|
||||
if (self.options.menu.length > 0)
|
||||
self.options.menu.each(function(menu_item){
|
||||
self.navigation_menu.addLink(menu_item);
|
||||
});
|
||||
else
|
||||
self.navigation_menu.hide();
|
||||
|
||||
},
|
||||
|
||||
calculateSelected: function(){
|
||||
var self = this;
|
||||
|
||||
var selected = 0,
|
||||
movies = self.movies.length;
|
||||
self.movies.each(function(movie){
|
||||
selected += movie.isSelected() ? 1 : 0
|
||||
});
|
||||
|
||||
var indeterminate = selected > 0 && selected < movies,
|
||||
checked = selected == movies && selected > 0;
|
||||
|
||||
self.mass_edit_select.set('indeterminate', indeterminate);
|
||||
|
||||
self.mass_edit_select_class[checked ? 'check' : 'uncheck']();
|
||||
self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate');
|
||||
|
||||
self.mass_edit_selected.set('text', selected);
|
||||
},
|
||||
|
||||
deleteSelected: function(){
|
||||
var self = this,
|
||||
ids = self.getSelectedMovies(),
|
||||
help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
|
||||
|
||||
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{
|
||||
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
|
||||
'class': 'delete',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
this.set('text', 'Deleting..');
|
||||
Api.request('media.delete', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'delete_from': self.options.identifier
|
||||
},
|
||||
'onSuccess': function(){
|
||||
qObj.close();
|
||||
|
||||
var erase_movies = [];
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected()){
|
||||
$(movie).destroy();
|
||||
erase_movies.include(movie);
|
||||
}
|
||||
});
|
||||
|
||||
erase_movies.each(function(movie){
|
||||
self.movies.erase(movie);
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}, {
|
||||
'text': 'Cancel',
|
||||
'cancel': true
|
||||
}]);
|
||||
|
||||
},
|
||||
|
||||
changeQualitySelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies();
|
||||
|
||||
Api.request('movie.edit', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'profile_id': self.mass_edit_quality.get('value')
|
||||
},
|
||||
'onSuccess': self.search.bind(self)
|
||||
});
|
||||
},
|
||||
|
||||
refreshSelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies();
|
||||
|
||||
Api.request('media.refresh', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(',')
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getSelectedMovies: function(){
|
||||
var self = this;
|
||||
|
||||
var ids = [];
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected())
|
||||
ids.include(movie.get('_id'))
|
||||
});
|
||||
|
||||
return ids
|
||||
},
|
||||
|
||||
massEditToggleAll: function(){
|
||||
var self = this;
|
||||
|
||||
var select = self.mass_edit_select.get('checked');
|
||||
|
||||
self.movies.each(function(movie){
|
||||
movie.select(select)
|
||||
});
|
||||
|
||||
self.calculateSelected()
|
||||
},
|
||||
|
||||
reset: function(){
|
||||
var self = this;
|
||||
|
||||
self.movies = [];
|
||||
if(self.mass_edit_select)
|
||||
self.calculateSelected();
|
||||
if(self.navigation_alpha)
|
||||
self.navigation_alpha.getElements('.active').removeClass('active');
|
||||
|
||||
self.offset = 0;
|
||||
if(self.scrollspy){
|
||||
//self.load_more.show();
|
||||
self.scrollspy.start();
|
||||
}
|
||||
},
|
||||
|
||||
activateLetter: function(letter){
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
|
||||
self.letters[letter || 'all'].addClass('active');
|
||||
self.filter.starts_with = letter;
|
||||
|
||||
},
|
||||
|
||||
changeView: function(new_view){
|
||||
var self = this;
|
||||
|
||||
self.el
|
||||
.removeClass(self.current_view+'_list')
|
||||
.addClass(new_view+'_list');
|
||||
|
||||
self.current_view = new_view;
|
||||
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
|
||||
},
|
||||
|
||||
getSavedView: function(){
|
||||
var self = this;
|
||||
return Cookie.read(self.options.identifier+'_view2');
|
||||
},
|
||||
|
||||
search: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.search_timer) clearTimeout(self.search_timer);
|
||||
self.search_timer = (function(){
|
||||
var search_value = self.navigation_search_input.get('value');
|
||||
if (search_value == self.last_search_value) return;
|
||||
|
||||
self.reset();
|
||||
|
||||
self.activateLetter();
|
||||
self.filter.search = search_value;
|
||||
|
||||
self.getMovies(true);
|
||||
|
||||
self.last_search_value = search_value;
|
||||
|
||||
}).delay(250);
|
||||
|
||||
},
|
||||
|
||||
update: function(){
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
self.getMovies(true);
|
||||
},
|
||||
|
||||
getMovies: function(reset){
|
||||
var self = this;
|
||||
|
||||
if(self.scrollspy){
|
||||
self.scrollspy.stop();
|
||||
self.load_more.set('text', 'loading...');
|
||||
}
|
||||
|
||||
if(self.movies.length == 0 && self.options.loader){
|
||||
|
||||
self.loader_first = new Element('div.loading').adopt(
|
||||
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
|
||||
).inject(self.el, 'top');
|
||||
|
||||
createSpinner(self.loader_first, {
|
||||
radius: 4,
|
||||
length: 4,
|
||||
width: 1
|
||||
});
|
||||
|
||||
self.el.setStyle('min-height', 93);
|
||||
|
||||
}
|
||||
|
||||
Api.request(self.options.api_call || 'media.list', {
|
||||
'data': Object.merge({
|
||||
'type': self.options.type || 'movie',
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
|
||||
if(reset)
|
||||
self.movie_list.empty();
|
||||
|
||||
if(self.loader_first){
|
||||
var lf = self.loader_first;
|
||||
self.loader_first.addClass('hide');
|
||||
self.loader_first = null;
|
||||
setTimeout(function(){
|
||||
lf.destroy();
|
||||
}, 20000);
|
||||
self.el.setStyle('min-height', null);
|
||||
}
|
||||
|
||||
self.store(json.shows);
|
||||
self.addMovies(json.shows, json.total || json.shows.length);
|
||||
if(self.scrollspy) {
|
||||
self.load_more.set('text', 'load more movies');
|
||||
self.scrollspy.start();
|
||||
}
|
||||
|
||||
self.checkIfEmpty();
|
||||
self.fireEvent('loaded');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMore: function(){
|
||||
var self = this;
|
||||
if(self.offset >= self.options.limit)
|
||||
self.getMovies()
|
||||
},
|
||||
|
||||
store: function(movies){
|
||||
var self = this;
|
||||
|
||||
self.offset += movies.length;
|
||||
|
||||
},
|
||||
|
||||
checkIfEmpty: function(){
|
||||
var self = this;
|
||||
|
||||
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
|
||||
|
||||
if(self.title)
|
||||
self.title[is_empty ? 'hide' : 'show']();
|
||||
|
||||
if(self.description)
|
||||
self.description.setStyle('display', [is_empty ? 'none' : '']);
|
||||
|
||||
if(is_empty && self.options.on_empty_element){
|
||||
self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
|
||||
|
||||
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(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,230 +0,0 @@
|
||||
Block.Search.ShowItem = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
initialize: function(info, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.info = info;
|
||||
self.alternative_titles = [];
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
self.el = new Element('div.media_result', {
|
||||
'id': info.id
|
||||
}).adopt(
|
||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': info.images.poster[0],
|
||||
'height': null,
|
||||
'width': null
|
||||
}) : null,
|
||||
self.options_el = new Element('div.options.inlay'),
|
||||
self.data_container = new Element('div.data', {
|
||||
'events': {
|
||||
'click': self.showOptions.bind(self)
|
||||
}
|
||||
}).adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('h2').adopt(
|
||||
self.title = new Element('span.title', {
|
||||
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||
}),
|
||||
self.year = info.year ? new Element('span.year', {
|
||||
'text': info.year
|
||||
}) : null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if(info.titles)
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
alternativeTitle: function(alternative){
|
||||
var self = this;
|
||||
|
||||
self.alternative_titles.include(alternative);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
try {
|
||||
return self.info.original_title ? self.info.original_title : self.info.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
return this.info[key]
|
||||
},
|
||||
|
||||
showOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.createOptions();
|
||||
|
||||
self.data_container.addClass('open');
|
||||
self.el.addEvent('outerClick', self.closeOptions.bind(self))
|
||||
|
||||
},
|
||||
|
||||
closeOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.data_container.removeClass('open');
|
||||
self.el.removeEvents('outerClick')
|
||||
},
|
||||
|
||||
add: function(e){
|
||||
var self = this;
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.loadingMask();
|
||||
|
||||
Api.request('show.add', {
|
||||
'data': {
|
||||
'identifiers': self.info.identifiers,
|
||||
'type': self.info.type,
|
||||
'title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value'),
|
||||
'category_id': self.category_select.get('value')
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': json.success ? 'Show successfully added.' : 'Show didn\'t add properly. Check logs'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
|
||||
self.fireEvent('added');
|
||||
},
|
||||
'onFailure': function(){
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': 'Something went wrong, check the logs for more info.'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createOptions: function(){
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
if(!self.options_el.hasClass('set')){
|
||||
|
||||
if(self.info.in_library){
|
||||
var in_library = [];
|
||||
self.info.in_library.releases.each(function(release){
|
||||
in_library.include(release.quality.label)
|
||||
});
|
||||
}
|
||||
|
||||
self.options_el.grab(
|
||||
new Element('div', {
|
||||
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
|
||||
}).adopt(
|
||||
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
|
||||
}) : (in_library ? new Element('span.in_library', {
|
||||
'text': 'Already in library: ' + in_library.join(', ')
|
||||
}) : null),
|
||||
self.title_select = new Element('select', {
|
||||
'name': 'title'
|
||||
}),
|
||||
self.profile_select = new Element('select', {
|
||||
'name': 'profile'
|
||||
}),
|
||||
self.category_select = new Element('select', {
|
||||
'name': 'category'
|
||||
}).grab(
|
||||
new Element('option', {'value': -1, 'text': 'None'})
|
||||
),
|
||||
self.add_button = new Element('a.button', {
|
||||
'text': 'Add',
|
||||
'events': {
|
||||
'click': self.add.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
Array.each(self.alternative_titles, function(alt){
|
||||
new Element('option', {
|
||||
'text': alt.title
|
||||
}).inject(self.title_select)
|
||||
})
|
||||
|
||||
|
||||
// Fill categories
|
||||
var categories = CategoryList.getAll();
|
||||
|
||||
if(categories.length == 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.show();
|
||||
categories.each(function(category){
|
||||
new Element('option', {
|
||||
'value': category.data._id,
|
||||
'text': category.data.label
|
||||
}).inject(self.category_select);
|
||||
});
|
||||
}
|
||||
|
||||
// Fill profiles
|
||||
var profiles = Quality.getActiveProfiles();
|
||||
if(profiles.length == 1)
|
||||
self.profile_select.hide();
|
||||
|
||||
profiles.each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.get('_id'),
|
||||
'text': profile.get('label')
|
||||
}).inject(self.profile_select)
|
||||
});
|
||||
|
||||
self.options_el.addClass('set');
|
||||
|
||||
if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
|
||||
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
|
||||
self.add();
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
loadingMask: function(){
|
||||
var self = this;
|
||||
|
||||
self.mask = new Element('div.mask').inject(self.el).fade('hide')
|
||||
|
||||
createSpinner(self.mask)
|
||||
self.mask.fade('in')
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
var Season = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
action: {},
|
||||
|
||||
initialize: function(show, options, data){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.show = show;
|
||||
self.options = options;
|
||||
self.data = data;
|
||||
|
||||
self.profile = self.show.profile;
|
||||
|
||||
self.el = new Element('div.item.season').adopt(
|
||||
self.detail = new Element('div.item.data')
|
||||
);
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.detail.set('id', 'season_'+self.data._id);
|
||||
|
||||
self.detail.adopt(
|
||||
new Element('span.name', {'text': self.getTitle()}),
|
||||
|
||||
self.quality = new Element('span.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.detail.getElement('.item-actions .releases');
|
||||
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.actions = new Element('div.item-actions')
|
||||
);
|
||||
|
||||
// Add profile
|
||||
if(self.profile.data) {
|
||||
self.profile.getTypes().each(function(type){
|
||||
var q = self.addQuality(type.get('quality'), type.get('3d'));
|
||||
|
||||
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
|
||||
q.addClass('finish');
|
||||
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add releases
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
|
||||
if(action.el)
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
|
||||
status = release.status;
|
||||
|
||||
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
|
||||
q = self.addQuality(release.quality, release.is_3d || false);
|
||||
|
||||
if (q && !q.hasClass(status)){
|
||||
q.addClass(status);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality, is_3d){
|
||||
var self = this,
|
||||
q = Quality.getQuality(quality);
|
||||
|
||||
return new Element('span', {
|
||||
'text': q.label + (is_3d ? ' 3D' : ''),
|
||||
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
|
||||
'title': ''
|
||||
}).inject(self.quality);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
var title = '';
|
||||
|
||||
if(self.data.info.number) {
|
||||
title = 'Season ' + self.data.info.number;
|
||||
} else {
|
||||
// Season 0 / Specials
|
||||
title = 'Specials';
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
getIdentifier: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.get('identifiers').imdb;
|
||||
}
|
||||
catch (e){ }
|
||||
|
||||
return self.get('imdb');
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.info[attr]
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
||||
var Episodes = new Class({
|
||||
initialize: function(show, options) {
|
||||
var self = this;
|
||||
|
||||
self.show = show;
|
||||
self.options = options;
|
||||
},
|
||||
|
||||
open: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.container){
|
||||
self.container = new Element('div.options').grab(
|
||||
self.episodes_container = new Element('div.episodes.table')
|
||||
);
|
||||
|
||||
self.container.inject(self.show, 'top');
|
||||
|
||||
Api.request('library.tree', {
|
||||
'data': {
|
||||
'media_id': self.show.data._id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.data = json.result;
|
||||
|
||||
self.createEpisodes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.show.slide('in', self.container, true);
|
||||
},
|
||||
|
||||
createEpisodes: function() {
|
||||
var self = this;
|
||||
|
||||
self.data.seasons.sort(self.sortSeasons);
|
||||
self.data.seasons.each(function(season) {
|
||||
self.createSeason(season);
|
||||
|
||||
season.episodes.sort(self.sortEpisodes);
|
||||
season.episodes.each(function(episode) {
|
||||
self.createEpisode(episode);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
createSeason: function(season) {
|
||||
var self = this,
|
||||
s = new Season(self.show, self.options, season);
|
||||
|
||||
$(s).inject(self.episodes_container);
|
||||
},
|
||||
|
||||
createEpisode: function(episode){
|
||||
var self = this,
|
||||
e = new Episode(self.show, self.options, episode);
|
||||
|
||||
$(e).inject(self.episodes_container);
|
||||
},
|
||||
|
||||
sortSeasons: function(a, b) {
|
||||
// Move "Specials" to the bottom of the list
|
||||
if(!a.info.number) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!b.info.number) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Order seasons descending
|
||||
if(a.info.number < b.info.number)
|
||||
return -1;
|
||||
|
||||
if(a.info.number > b.info.number)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
sortEpisodes: function(a, b) {
|
||||
// Order episodes descending
|
||||
if(a.info.number < b.info.number)
|
||||
return -1;
|
||||
|
||||
if(a.info.number > b.info.number)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
@@ -1,370 +0,0 @@
|
||||
var Show = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
action: {},
|
||||
|
||||
initialize: function(list, options, data){
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
self.view = options.view || 'details';
|
||||
self.list = list;
|
||||
|
||||
self.el = new Element('div.show');
|
||||
|
||||
self.episodes = new Episodes(self, {
|
||||
'actions': [EA.IMDB, EA.Release, EA.Refresh]
|
||||
});
|
||||
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
self.category = CategoryList.getCategory(data.category_id) || {};
|
||||
self.parent(self, options);
|
||||
|
||||
self.addEvents();
|
||||
},
|
||||
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
self.global_events = {};
|
||||
|
||||
// Do refresh with new data
|
||||
self.global_events['movie.update'] = function(notification){
|
||||
if(self.data._id != notification.data._id) return;
|
||||
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
};
|
||||
App.on('movie.update', self.global_events['movie.update']);
|
||||
|
||||
// Add spinner on load / search
|
||||
['media.busy', 'movie.searcher.started'].each(function(listener){
|
||||
self.global_events[listener] = function(notification){
|
||||
if(notification.data && (self.data._id == notification.data._id || (typeOf(notification.data._id) == 'array' && notification.data._id.indexOf(self.data._id) > -1)))
|
||||
self.busy(true);
|
||||
};
|
||||
App.on(listener, self.global_events[listener]);
|
||||
});
|
||||
|
||||
// Remove spinner
|
||||
self.global_events['movie.searcher.ended'] = function(notification){
|
||||
if(notification.data && self.data._id == notification.data._id)
|
||||
self.busy(false)
|
||||
};
|
||||
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
|
||||
|
||||
// Reload when releases have updated
|
||||
self.global_events['release.update_status'] = function(notification){
|
||||
var data = notification.data;
|
||||
if(data && self.data._id == data.movie_id){
|
||||
|
||||
if(!self.data.releases)
|
||||
self.data.releases = [];
|
||||
|
||||
self.data.releases.push({'quality': data.quality, 'status': data.status});
|
||||
self.updateReleases();
|
||||
}
|
||||
};
|
||||
|
||||
App.on('release.update_status', self.global_events['release.update_status']);
|
||||
|
||||
},
|
||||
|
||||
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
|
||||
Object.each(self.global_events, function(handle, listener){
|
||||
App.off(listener, handle);
|
||||
});
|
||||
},
|
||||
|
||||
busy: function(set_busy, timeout){
|
||||
var self = this;
|
||||
|
||||
if(!set_busy){
|
||||
setTimeout(function(){
|
||||
if(self.spinner){
|
||||
self.mask.fade('out');
|
||||
setTimeout(function(){
|
||||
if(self.mask)
|
||||
self.mask.destroy();
|
||||
if(self.spinner)
|
||||
self.spinner.el.destroy();
|
||||
self.spinner = null;
|
||||
self.mask = null;
|
||||
}, timeout || 400);
|
||||
}
|
||||
}, timeout || 1000)
|
||||
}
|
||||
else if(!self.spinner) {
|
||||
self.createMask();
|
||||
self.spinner = createSpinner(self.mask);
|
||||
self.mask.fade('in');
|
||||
}
|
||||
},
|
||||
|
||||
createMask: function(){
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
'styles': {
|
||||
'z-index': 4
|
||||
}
|
||||
}).inject(self.el, 'top').fade('hide');
|
||||
},
|
||||
|
||||
update: function(notification){
|
||||
var self = this;
|
||||
|
||||
self.data = notification.data;
|
||||
self.el.empty();
|
||||
self.removeView();
|
||||
|
||||
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||
self.category = CategoryList.getCategory(self.data.category_id) || {};
|
||||
self.create();
|
||||
|
||||
self.busy(false);
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.addClass('status_'+self.get('status'));
|
||||
|
||||
var eta = null,
|
||||
eta_date = null,
|
||||
now = Math.round(+new Date()/1000);
|
||||
|
||||
if(self.data.info.release_date)
|
||||
[self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){
|
||||
if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now)))
|
||||
eta = timestamp;
|
||||
});
|
||||
|
||||
if(eta){
|
||||
eta_date = new Date(eta * 1000);
|
||||
eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear();
|
||||
}
|
||||
|
||||
self.el.adopt(
|
||||
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': function(){
|
||||
self.fireEvent('select')
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', {
|
||||
'class': 'type_image poster',
|
||||
'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop()
|
||||
}): null,
|
||||
self.data_container = new Element('div.data.inlay.light').adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('div.title').adopt(
|
||||
self.title = new Element('a', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
self.episodes.open();
|
||||
}
|
||||
},
|
||||
'text': self.getTitle() || 'n/a'
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'text': self.data.info.year || 'n/a'
|
||||
})
|
||||
),
|
||||
self.description = new Element('div.description.tiny_scroll', {
|
||||
'text': self.data.info.plot
|
||||
}),
|
||||
self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', {
|
||||
'text': eta_date,
|
||||
'title': 'ETA'
|
||||
}) : null,
|
||||
self.quality = new Element('div.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
self.actions = new Element('div.actions')
|
||||
)
|
||||
);
|
||||
|
||||
if(!self.thumbnail)
|
||||
self.el.addClass('no_thumbnail');
|
||||
|
||||
//self.changeView(self.view);
|
||||
self.select_checkbox_class = new Form.Check(self.select_checkbox);
|
||||
|
||||
// Add profile
|
||||
if(self.profile.data)
|
||||
self.profile.getTypes().each(function(type){
|
||||
|
||||
var q = self.addQuality(type.get('quality'), type.get('3d'));
|
||||
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
|
||||
q.addClass('finish');
|
||||
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Add releases
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
|
||||
if(action.el)
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
|
||||
status = release.status;
|
||||
|
||||
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
|
||||
q = self.addQuality(release.quality, release.is_3d || false);
|
||||
|
||||
if (q && !q.hasClass(status)){
|
||||
q.addClass(status);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality, is_3d){
|
||||
var self = this;
|
||||
|
||||
var q = Quality.getQuality(quality);
|
||||
return new Element('span', {
|
||||
'text': q.label + (is_3d ? ' 3D' : ''),
|
||||
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
|
||||
'title': ''
|
||||
}).inject(self.quality);
|
||||
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.data.title)
|
||||
return self.getUnprefixedTitle(self.data.title);
|
||||
else if(self.data.info.titles.length > 0)
|
||||
return self.getUnprefixedTitle(self.data.info.titles[0]);
|
||||
|
||||
return 'Unknown movie'
|
||||
},
|
||||
|
||||
getUnprefixedTitle: function(t){
|
||||
if(t.substr(0, 4).toLowerCase() == 'the ')
|
||||
t = t.substr(4) + ', The';
|
||||
else if(t.substr(0, 3).toLowerCase() == 'an ')
|
||||
t = t.substr(3) + ', An';
|
||||
else if(t.substr(0, 2).toLowerCase() == 'a ')
|
||||
t = t.substr(2) + ', A';
|
||||
return t;
|
||||
},
|
||||
|
||||
slide: function(direction, el, expand){
|
||||
var self = this;
|
||||
|
||||
if(direction == 'in'){
|
||||
self.temp_view = self.view;
|
||||
self.changeView('details');
|
||||
|
||||
self.el.addEvent('outerClick', function(){
|
||||
self.removeView();
|
||||
self.slide('out')
|
||||
});
|
||||
el.show();
|
||||
|
||||
|
||||
if(expand === true) {
|
||||
self.el.addClass('expanded');
|
||||
self.el.getElements('.table').addClass('expanded');
|
||||
}
|
||||
|
||||
self.data_container.addClass('hide_right');
|
||||
}
|
||||
else {
|
||||
self.el.removeEvents('outerClick');
|
||||
|
||||
setTimeout(function(){
|
||||
if(self.el)
|
||||
{
|
||||
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
|
||||
self.el.getElements('.table').removeClass('expanded');
|
||||
}
|
||||
}, 600);
|
||||
|
||||
self.el.removeClass('expanded');
|
||||
self.data_container.removeClass('hide_right');
|
||||
}
|
||||
},
|
||||
|
||||
changeView: function(new_view){
|
||||
var self = this;
|
||||
|
||||
if(self.el)
|
||||
self.el
|
||||
.removeClass(self.view+'_view')
|
||||
.addClass(new_view+'_view');
|
||||
|
||||
self.view = new_view;
|
||||
},
|
||||
|
||||
removeView: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.removeClass(self.view+'_view')
|
||||
},
|
||||
|
||||
getIdentifier: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.get('identifiers').imdb;
|
||||
}
|
||||
catch (e){ }
|
||||
|
||||
return self.get('imdb');
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.info[attr]
|
||||
},
|
||||
|
||||
select: function(bool){
|
||||
var self = this;
|
||||
self.select_checkbox_class[bool ? 'check' : 'uncheck']()
|
||||
},
|
||||
|
||||
isSelected: function(){
|
||||
return this.select_checkbox.get('checked');
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'EpisodeLibraryPlugin'
|
||||
|
||||
|
||||
class EpisodeLibraryPlugin(LibraryBase):
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
addEvent('library.identifier', self.identifier)
|
||||
|
||||
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
|
||||
if media.get('type') != 'show.episode':
|
||||
return
|
||||
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
|
||||
# Get season titles
|
||||
titles = fireEvent(
|
||||
'library.query', related['season'],
|
||||
|
||||
first = False,
|
||||
include_identifier = include_identifier,
|
||||
condense = condense,
|
||||
|
||||
single = True
|
||||
)
|
||||
|
||||
# Add episode identifier to titles
|
||||
if include_identifier:
|
||||
identifier = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
if identifier and identifier.get('episode'):
|
||||
titles = [title + ('E%02d' % identifier['episode']) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
|
||||
def identifier(self, media):
|
||||
if media.get('type') != 'show.episode':
|
||||
return
|
||||
|
||||
identifier = {
|
||||
'season': None,
|
||||
'episode': None
|
||||
}
|
||||
|
||||
# TODO identifier mapping
|
||||
# scene_map = media['info'].get('map_episode', {}).get('scene')
|
||||
|
||||
# if scene_map:
|
||||
# # Use scene mappings if they are available
|
||||
# identifier['season'] = scene_map.get('season_nr')
|
||||
# identifier['episode'] = scene_map.get('episode_nr')
|
||||
# else:
|
||||
# Fallback to normal season/episode numbers
|
||||
identifier['season'] = media['info'].get('season_number')
|
||||
identifier['episode'] = media['info'].get('number')
|
||||
|
||||
# Cast identifiers to integers
|
||||
# TODO this will need changing to support identifiers with trailing 'a', 'b' characters
|
||||
identifier['season'] = tryInt(identifier['season'], None)
|
||||
identifier['episode'] = tryInt(identifier['episode'], None)
|
||||
|
||||
return identifier
|
||||
@@ -1,52 +0,0 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'SeasonLibraryPlugin'
|
||||
|
||||
|
||||
class SeasonLibraryPlugin(LibraryBase):
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
addEvent('library.identifier', self.identifier)
|
||||
|
||||
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
|
||||
if media.get('type') != 'show.season':
|
||||
return
|
||||
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
|
||||
# Get show titles
|
||||
titles = fireEvent(
|
||||
'library.query', related['show'],
|
||||
|
||||
first = False,
|
||||
condense = condense,
|
||||
|
||||
single = True
|
||||
)
|
||||
|
||||
# TODO map_names
|
||||
|
||||
# Add season identifier to titles
|
||||
if include_identifier:
|
||||
identifier = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
if identifier and identifier.get('season') is not None:
|
||||
titles = [title + (' S%02d' % identifier['season']) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
|
||||
def identifier(self, media):
|
||||
if media.get('type') != 'show.season':
|
||||
return
|
||||
|
||||
return {
|
||||
'season': tryInt(media['info']['number'], None)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
from qcond import QueryCondenser
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'ShowLibraryPlugin'
|
||||
|
||||
|
||||
class ShowLibraryPlugin(LibraryBase):
|
||||
query_condenser = QueryCondenser()
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
|
||||
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
|
||||
if media.get('type') != 'show':
|
||||
return
|
||||
|
||||
titles = media['info']['titles']
|
||||
|
||||
if condense:
|
||||
# Use QueryCondenser to build a list of optimal search titles
|
||||
condensed_titles = self.query_condenser.distinct(titles)
|
||||
|
||||
if condensed_titles:
|
||||
# Use condensed titles if we got a valid result
|
||||
titles = condensed_titles
|
||||
else:
|
||||
# Fallback to simplifying titles
|
||||
titles = [simplifyString(title) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
@@ -1,7 +0,0 @@
|
||||
from .main import ShowMatcher
|
||||
|
||||
|
||||
def autoload():
|
||||
return ShowMatcher()
|
||||
|
||||
config = []
|
||||
@@ -1,72 +0,0 @@
|
||||
from couchpotato import fireEvent, CPLog, tryInt
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.media._base.matcher.base import MatcherBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(MatcherBase):
|
||||
|
||||
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
|
||||
quality_map = {
|
||||
'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']},
|
||||
'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']},
|
||||
|
||||
'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']},
|
||||
'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']},
|
||||
|
||||
'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']},
|
||||
'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']},
|
||||
|
||||
'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
|
||||
'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']},
|
||||
'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super(Base, self).__init__()
|
||||
|
||||
addEvent('%s.matcher.correct_identifier' % self.type, self.correctIdentifier)
|
||||
|
||||
def correct(self, chain, release, media, quality):
|
||||
log.info("Checking if '%s' is valid", release['name'])
|
||||
log.info2('Release parsed as: %s', chain.info)
|
||||
|
||||
if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True):
|
||||
log.info('Wrong: %s, quality does not match', release['name'])
|
||||
return False
|
||||
|
||||
if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media):
|
||||
log.info('Wrong: %s, identifier does not match', release['name'])
|
||||
return False
|
||||
|
||||
if not fireEvent('matcher.correct_title', chain, media):
|
||||
log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name'])))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
raise NotImplementedError()
|
||||
|
||||
def getChainIdentifier(self, chain):
|
||||
if 'identifier' not in chain.info:
|
||||
return None
|
||||
|
||||
identifier = self.flattenInfo(chain.info['identifier'])
|
||||
|
||||
# Try cast values to integers
|
||||
for key, value in identifier.items():
|
||||
if isinstance(value, list):
|
||||
if len(value) <= 1:
|
||||
value = value[0]
|
||||
else:
|
||||
log.warning('Wrong: identifier contains multiple season or episode values, unsupported')
|
||||
return None
|
||||
|
||||
identifier[key] = tryInt(value, value)
|
||||
|
||||
return identifier
|
||||
@@ -1,30 +0,0 @@
|
||||
from couchpotato import fireEvent, CPLog
|
||||
from couchpotato.core.media.show.matcher.base import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Episode(Base):
|
||||
type = 'show.episode'
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
identifier = self.getChainIdentifier(chain)
|
||||
if not identifier:
|
||||
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
|
||||
return False
|
||||
|
||||
# TODO - Parse episode ranges from identifier to determine if they are multi-part episodes
|
||||
if any([x in identifier for x in ['episode_from', 'episode_to']]):
|
||||
log.info2('Wrong: releases with identifier ranges are not supported yet')
|
||||
return False
|
||||
|
||||
required = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
# TODO - Support air by date episodes
|
||||
# TODO - Support episode parts
|
||||
|
||||
if identifier != required:
|
||||
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,9 +0,0 @@
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.matcher.episode import Episode
|
||||
from couchpotato.core.media.show.matcher.season import Season
|
||||
|
||||
|
||||
class ShowMatcher(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
@@ -1,27 +0,0 @@
|
||||
from couchpotato import fireEvent, CPLog
|
||||
from couchpotato.core.media.show.matcher.base import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Season(Base):
|
||||
type = 'show.season'
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
identifier = self.getChainIdentifier(chain)
|
||||
if not identifier:
|
||||
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
|
||||
return False
|
||||
|
||||
# TODO - Parse episode ranges from identifier to determine if they are season packs
|
||||
if any([x in identifier for x in ['episode_from', 'episode_to']]):
|
||||
log.info2('Wrong: releases with identifier ranges are not supported yet')
|
||||
return False
|
||||
|
||||
required = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
if identifier != required:
|
||||
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,13 +0,0 @@
|
||||
from couchpotato.core.media._base.providers.info.base import BaseInfoProvider
|
||||
|
||||
|
||||
class ShowProvider(BaseInfoProvider):
|
||||
type = 'show'
|
||||
|
||||
|
||||
class SeasonProvider(BaseInfoProvider):
|
||||
type = 'show.season'
|
||||
|
||||
|
||||
class EpisodeProvider(BaseInfoProvider):
|
||||
type = 'show.episode'
|
||||
@@ -1,372 +0,0 @@
|
||||
from datetime import datetime
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from couchpotato import Env
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.show.providers.base import ShowProvider
|
||||
from tvdb_api import tvdb_exceptions
|
||||
from tvdb_api.tvdb_api import Tvdb, Show
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TheTVDb'
|
||||
|
||||
|
||||
class TheTVDb(ShowProvider):
|
||||
|
||||
# TODO: Consider grabbing zips to put less strain on tvdb
|
||||
# TODO: Unicode stuff (check)
|
||||
# TODO: Notigy frontend on error (tvdb down at monent)
|
||||
# TODO: Expose apikey in setting so it can be changed by user
|
||||
|
||||
def __init__(self):
|
||||
addEvent('show.info', self.getShowInfo, priority = 1)
|
||||
addEvent('season.info', self.getSeasonInfo, priority = 1)
|
||||
addEvent('episode.info', self.getEpisodeInfo, priority = 1)
|
||||
|
||||
self.tvdb_api_parms = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'banners': True,
|
||||
'language': 'en',
|
||||
'cache': os.path.join(Env.get('cache_dir'), 'thetvdb_api'),
|
||||
}
|
||||
self._setup()
|
||||
|
||||
def _setup(self):
|
||||
self.tvdb = Tvdb(**self.tvdb_api_parms)
|
||||
self.valid_languages = self.tvdb.config['valid_languages']
|
||||
|
||||
def getShow(self, identifier = None):
|
||||
show = None
|
||||
try:
|
||||
log.debug('Getting show: %s', identifier)
|
||||
show = self.tvdb[int(identifier)]
|
||||
except (tvdb_exceptions.tvdb_error, IOError), e:
|
||||
log.error('Failed to getShowInfo for show id "%s": %s', (identifier, traceback.format_exc()))
|
||||
return None
|
||||
|
||||
return show
|
||||
|
||||
def getShowInfo(self, identifiers = None):
|
||||
"""
|
||||
|
||||
@param identifiers: dict with identifiers per provider
|
||||
@return: Full show info including season and episode info
|
||||
"""
|
||||
|
||||
if not identifiers or not identifiers.get('thetvdb'):
|
||||
return None
|
||||
|
||||
identifier = tryInt(identifiers.get('thetvdb'))
|
||||
|
||||
cache_key = 'thetvdb.cache.show.%s' % identifier
|
||||
result = None #self.getCache(cache_key)
|
||||
if result:
|
||||
return result
|
||||
|
||||
show = self.getShow(identifier = identifier)
|
||||
if show:
|
||||
result = self._parseShow(show)
|
||||
self.setCache(cache_key, result)
|
||||
|
||||
return result or {}
|
||||
|
||||
def getSeasonInfo(self, identifiers = None, params = {}):
|
||||
"""Either return a list of all seasons or a single season by number.
|
||||
identifier is the show 'id'
|
||||
"""
|
||||
if not identifiers or not identifiers.get('thetvdb'):
|
||||
return None
|
||||
|
||||
season_number = params.get('season_number', None)
|
||||
identifier = tryInt(identifiers.get('thetvdb'))
|
||||
|
||||
cache_key = 'thetvdb.cache.%s.%s' % (identifier, season_number)
|
||||
log.debug('Getting SeasonInfo: %s', cache_key)
|
||||
result = self.getCache(cache_key) or {}
|
||||
if result:
|
||||
return result
|
||||
|
||||
try:
|
||||
show = self.tvdb[int(identifier)]
|
||||
except (tvdb_exceptions.tvdb_error, IOError), e:
|
||||
log.error('Failed parsing TheTVDB SeasonInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
result = []
|
||||
for number, season in show.items():
|
||||
if season_number is not None and number == season_number:
|
||||
result = self._parseSeason(show, number, season)
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
else:
|
||||
result.append(self._parseSeason(show, number, season))
|
||||
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
|
||||
def getEpisodeInfo(self, identifier = None, params = {}):
|
||||
"""Either return a list of all episodes or a single episode.
|
||||
If episode_identifer contains an episode number to search for
|
||||
"""
|
||||
season_number = self.getIdentifier(params.get('season_number', None))
|
||||
episode_identifier = self.getIdentifier(params.get('episode_identifiers', None))
|
||||
identifier = self.getIdentifier(identifier)
|
||||
|
||||
if not identifier and season_number is None:
|
||||
return False
|
||||
|
||||
# season_identifier must contain the 'show id : season number' since there is no tvdb id
|
||||
# for season and we need a reference to both the show id and season number
|
||||
if not identifier and season_number:
|
||||
try:
|
||||
identifier, season_number = season_number.split(':')
|
||||
season_number = int(season_number)
|
||||
except: return None
|
||||
|
||||
cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_number)
|
||||
log.debug('Getting EpisodeInfo: %s', cache_key)
|
||||
result = self.getCache(cache_key) or {}
|
||||
if result:
|
||||
return result
|
||||
|
||||
try:
|
||||
show = self.tvdb[int(identifier)]
|
||||
except (tvdb_exceptions.tvdb_error, IOError), e:
|
||||
log.error('Failed parsing TheTVDB EpisodeInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
result = []
|
||||
for number, season in show.items():
|
||||
if season_number is not None and number != season_number:
|
||||
continue
|
||||
|
||||
for episode in season.values():
|
||||
if episode_identifier is not None and episode['id'] == toUnicode(episode_identifier):
|
||||
result = self._parseEpisode(episode)
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
else:
|
||||
result.append(self._parseEpisode(episode))
|
||||
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
|
||||
def getIdentifier(self, value):
|
||||
if type(value) is dict:
|
||||
return value.get('thetvdb')
|
||||
|
||||
return value
|
||||
|
||||
def _parseShow(self, show):
|
||||
|
||||
#
|
||||
# NOTE: show object only allows direct access via
|
||||
# show['id'], not show.get('id')
|
||||
#
|
||||
def get(name):
|
||||
return show.get(name) if not hasattr(show, 'search') else show[name]
|
||||
|
||||
## Images
|
||||
poster = get('poster')
|
||||
backdrop = get('fanart')
|
||||
|
||||
genres = splitString(get('genre'), '|')
|
||||
if get('firstaired') is not None:
|
||||
try: year = datetime.strptime(get('firstaired'), '%Y-%m-%d').year
|
||||
except: year = None
|
||||
else:
|
||||
year = None
|
||||
|
||||
show_data = {
|
||||
'identifiers': {
|
||||
'thetvdb': tryInt(get('id')),
|
||||
'imdb': get('imdb_id'),
|
||||
'zap2it': get('zap2it_id'),
|
||||
},
|
||||
'type': 'show',
|
||||
'titles': [get('seriesname')],
|
||||
'images': {
|
||||
'poster': [poster] if poster else [],
|
||||
'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [],
|
||||
'backdrop_original': [],
|
||||
},
|
||||
'year': year,
|
||||
'genres': genres,
|
||||
'network': get('network'),
|
||||
'plot': get('overview'),
|
||||
'networkid': get('networkid'),
|
||||
'air_day': (get('airs_dayofweek') or '').lower(),
|
||||
'air_time': self.parseTime(get('airs_time')),
|
||||
'firstaired': get('firstaired'),
|
||||
'runtime': tryInt(get('runtime')),
|
||||
'contentrating': get('contentrating'),
|
||||
'rating': {},
|
||||
'actors': splitString(get('actors'), '|'),
|
||||
'status': get('status'),
|
||||
'language': get('language'),
|
||||
}
|
||||
|
||||
if tryFloat(get('rating')):
|
||||
show_data['rating']['thetvdb'] = [tryFloat(get('rating')), tryInt(get('ratingcount'))],
|
||||
|
||||
show_data = dict((k, v) for k, v in show_data.iteritems() if v)
|
||||
|
||||
# Only load season info when available
|
||||
if type(show) == Show:
|
||||
|
||||
# Parse season and episode data
|
||||
show_data['seasons'] = {}
|
||||
|
||||
for season_nr in show:
|
||||
season = self._parseSeason(show, season_nr, show[season_nr])
|
||||
season['episodes'] = {}
|
||||
|
||||
for episode_nr in show[season_nr]:
|
||||
season['episodes'][episode_nr] = self._parseEpisode(show[season_nr][episode_nr])
|
||||
|
||||
show_data['seasons'][season_nr] = season
|
||||
|
||||
# Add alternative titles
|
||||
# try:
|
||||
# raw = self.tvdb.search(show['seriesname'])
|
||||
# if raw:
|
||||
# for show_info in raw:
|
||||
# print show_info
|
||||
# if show_info['id'] == show_data['id'] and show_info.get('aliasnames', None):
|
||||
# for alt_name in show_info['aliasnames'].split('|'):
|
||||
# show_data['titles'].append(toUnicode(alt_name))
|
||||
# except (tvdb_exceptions.tvdb_error, IOError), e:
|
||||
# log.error('Failed searching TheTVDB for "%s": %s', (show['seriesname'], traceback.format_exc()))
|
||||
|
||||
return show_data
|
||||
|
||||
def _parseSeason(self, show, number, season):
|
||||
"""
|
||||
contains no data
|
||||
"""
|
||||
|
||||
poster = []
|
||||
try:
|
||||
temp_poster = {}
|
||||
for id, data in show.data['_banners']['season']['season'].items():
|
||||
if data.get('season') == str(number) and data.get('language') == self.tvdb_api_parms['language']:
|
||||
temp_poster[tryFloat(data.get('rating')) * tryInt(data.get('ratingcount'))] = data.get('_bannerpath')
|
||||
#break
|
||||
poster.append(temp_poster[sorted(temp_poster, reverse = True)[0]])
|
||||
except:
|
||||
pass
|
||||
|
||||
season_data = {
|
||||
'identifiers': {
|
||||
'thetvdb': show['id'] if show.get('id') else show[number][1]['seasonid']
|
||||
},
|
||||
'number': tryInt(number),
|
||||
'images': {
|
||||
'poster': poster,
|
||||
},
|
||||
}
|
||||
|
||||
season_data = dict((k, v) for k, v in season_data.iteritems() if v)
|
||||
return season_data
|
||||
|
||||
def _parseEpisode(self, episode):
|
||||
"""
|
||||
('episodenumber', u'1'),
|
||||
('thumb_added', None),
|
||||
('rating', u'7.7'),
|
||||
('overview',
|
||||
u'Experienced waitress Max Black meets her new co-worker, former rich-girl Caroline Channing, and puts her skills to the test at an old but re-emerging Brooklyn diner. Despite her initial distaste for Caroline, Max eventually softens and the two team up for a new business venture.'),
|
||||
('dvd_episodenumber', None),
|
||||
('dvd_discid', None),
|
||||
('combined_episodenumber', u'1'),
|
||||
('epimgflag', u'7'),
|
||||
('id', u'4099506'),
|
||||
('seasonid', u'465948'),
|
||||
('thumb_height', u'225'),
|
||||
('tms_export', u'1374789754'),
|
||||
('seasonnumber', u'1'),
|
||||
('writer', u'|Michael Patrick King|Whitney Cummings|'),
|
||||
('lastupdated', u'1371420338'),
|
||||
('filename', u'http://thetvdb.com/banners/episodes/248741/4099506.jpg'),
|
||||
('absolute_number', u'1'),
|
||||
('ratingcount', u'102'),
|
||||
('combined_season', u'1'),
|
||||
('thumb_width', u'400'),
|
||||
('imdb_id', u'tt1980319'),
|
||||
('director', u'James Burrows'),
|
||||
('dvd_chapter', None),
|
||||
('dvd_season', None),
|
||||
('gueststars',
|
||||
u'|Brooke Lyons|Noah Mills|Shoshana Bush|Cale Hartmann|Adam Korson|Alex Enriquez|Matt Cook|Bill Parks|Eugene Shaw|Sergey Brusilovsky|Greg Lewis|Cocoa Brown|Nick Jameson|'),
|
||||
('seriesid', u'248741'),
|
||||
('language', u'en'),
|
||||
('productioncode', u'296793'),
|
||||
('firstaired', u'2011-09-19'),
|
||||
('episodename', u'Pilot')]
|
||||
"""
|
||||
|
||||
def get(name, default = None):
|
||||
return episode.get(name, default)
|
||||
|
||||
poster = get('filename', [])
|
||||
|
||||
episode_data = {
|
||||
'number': tryInt(get('episodenumber')),
|
||||
'absolute_number': tryInt(get('absolute_number')),
|
||||
'identifiers': {
|
||||
'thetvdb': tryInt(episode['id'])
|
||||
},
|
||||
'type': 'episode',
|
||||
'titles': [get('episodename')] if get('episodename') else [],
|
||||
'images': {
|
||||
'poster': [poster] if poster else [],
|
||||
},
|
||||
'released': get('firstaired'),
|
||||
'plot': get('overview'),
|
||||
'firstaired': get('firstaired'),
|
||||
'language': get('language'),
|
||||
}
|
||||
|
||||
if get('imdb_id'):
|
||||
episode_data['identifiers']['imdb'] = get('imdb_id')
|
||||
|
||||
episode_data = dict((k, v) for k, v in episode_data.iteritems() if v)
|
||||
return episode_data
|
||||
|
||||
def parseTime(self, time):
|
||||
return time
|
||||
|
||||
def isDisabled(self):
|
||||
if self.conf('api_key') == '':
|
||||
log.error('No API key provided.')
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'thetvdb',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'name': 'tmdb',
|
||||
'label': 'TheTVDB',
|
||||
'hidden': True,
|
||||
'description': 'Used for all calls to TheTVDB.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': '7966C02F860586D2',
|
||||
'label': 'Api Key',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,86 +0,0 @@
|
||||
import urllib
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.show.providers.base import ShowProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Trakt'
|
||||
|
||||
|
||||
class Trakt(ShowProvider):
|
||||
api_key = 'c043de5ada9d180028c10229d2a3ea5b'
|
||||
base_url = 'http://api.trakt.tv/%%s.json/%s' % api_key
|
||||
|
||||
def __init__(self):
|
||||
addEvent('info.search', self.search, priority = 1)
|
||||
addEvent('show.search', self.search, priority = 1)
|
||||
|
||||
def search(self, q, limit = 12):
|
||||
if self.isDisabled():
|
||||
return False
|
||||
|
||||
# Check for cached result
|
||||
cache_key = 'trakt.cache.search.%s.%s' % (q, limit)
|
||||
results = self.getCache(cache_key) or []
|
||||
|
||||
if results:
|
||||
return results
|
||||
|
||||
# Search
|
||||
log.debug('Searching for show: "%s"', q)
|
||||
response = self._request('search/shows', query=q, limit=limit)
|
||||
|
||||
if not response:
|
||||
return []
|
||||
|
||||
# Parse search results
|
||||
for show in response:
|
||||
results.append(self._parseShow(show))
|
||||
|
||||
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
|
||||
|
||||
self.setCache(cache_key, results)
|
||||
return results
|
||||
|
||||
def _request(self, action, **kwargs):
|
||||
url = self.base_url % action
|
||||
|
||||
if kwargs:
|
||||
url += '?' + urllib.urlencode(kwargs)
|
||||
|
||||
return self.getJsonData(url)
|
||||
|
||||
def _parseShow(self, show):
|
||||
# Images
|
||||
images = show.get('images', {})
|
||||
|
||||
poster = images.get('poster')
|
||||
backdrop = images.get('backdrop')
|
||||
|
||||
# Rating
|
||||
rating = show.get('ratings', {}).get('percentage')
|
||||
|
||||
# Build show dict
|
||||
show_data = {
|
||||
'identifiers': {
|
||||
'thetvdb': show.get('tvdb_id'),
|
||||
'imdb': show.get('imdb_id'),
|
||||
'tvrage': show.get('tvrage_id'),
|
||||
},
|
||||
'type': 'show',
|
||||
'titles': [show.get('title')],
|
||||
'images': {
|
||||
'poster': [poster] if poster else [],
|
||||
'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [],
|
||||
'backdrop_original': [],
|
||||
},
|
||||
'year': show.get('year'),
|
||||
'rating': {
|
||||
'trakt': float(rating) / 10
|
||||
},
|
||||
}
|
||||
|
||||
return dict((k, v) for k, v in show_data.iteritems() if v)
|
||||
@@ -1,216 +0,0 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.media.show.providers.base import ShowProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Xem'
|
||||
|
||||
|
||||
class Xem(ShowProvider):
|
||||
'''
|
||||
Mapping Information
|
||||
===================
|
||||
|
||||
Single
|
||||
------
|
||||
You will need the id / identifier of the show e.g. tvdb-id for American Dad! is 73141
|
||||
the origin is the name of the site/entity the episode, season (and/or absolute) numbers are based on
|
||||
|
||||
http://thexem.de/map/single?id=&origin=&episode=&season=&absolute=
|
||||
|
||||
episode, season and absolute are all optional but it wont work if you don't provide either episode and season OR absolute in
|
||||
addition you can provide destination as the name of the wished destination, if not provided it will output all available
|
||||
|
||||
When a destination has two or more addresses another entry will be added as _ ... for now the second address gets the index "2"
|
||||
(the first index is omitted) and so on
|
||||
|
||||
http://thexem.de/map/single?id=7529&origin=anidb&season=1&episode=2&destination=trakt
|
||||
{
|
||||
"result":"success",
|
||||
"data":{
|
||||
"trakt": {"season":1,"episode":3,"absolute":3},
|
||||
"trakt_2":{"season":1,"episode":4,"absolute":4}
|
||||
},
|
||||
"message":"single mapping for 7529 on anidb."
|
||||
}
|
||||
|
||||
All
|
||||
---
|
||||
Basically same as "single" just a little easier
|
||||
The origin address is added into the output too!!
|
||||
|
||||
http://thexem.de/map/all?id=7529&origin=anidb
|
||||
|
||||
All Names
|
||||
---------
|
||||
Get all names xem has to offer
|
||||
non optional params: origin(an entity string like 'tvdb')
|
||||
optional params: season, language
|
||||
- season: a season number or a list like: 1,3,5 or a compare operator like ne,gt,ge,lt,le,eq and a season number. default would
|
||||
return all
|
||||
- language: a language string like 'us' or 'jp' default is all
|
||||
- defaultNames: 1(yes) or 0(no) should the default names be added to the list ? default is 0(no)
|
||||
|
||||
http://thexem.de/map/allNames?origin=tvdb&season=le1
|
||||
|
||||
{
|
||||
"result": "success",
|
||||
"data": {
|
||||
"248812": ["Dont Trust the Bitch in Apartment 23", "Don't Trust the Bitch in Apartment 23"],
|
||||
"257571": ["Nazo no Kanojo X"],
|
||||
"257875": ["Lupin III - Mine Fujiko to Iu Onna", "Lupin III Fujiko to Iu Onna", "Lupin the Third - Mine Fujiko to Iu Onna"]
|
||||
},
|
||||
"message": ""
|
||||
}
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
addEvent('show.info', self.getShowInfo, priority = 5)
|
||||
addEvent('episode.info', self.getEpisodeInfo, priority = 5)
|
||||
|
||||
self.config = {}
|
||||
self.config['base_url'] = "http://thexem.de"
|
||||
self.config['url_single'] = u"%(base_url)s/map/single?" % self.config
|
||||
self.config['url_all'] = u"%(base_url)s/map/all?" % self.config
|
||||
self.config['url_names'] = u"%(base_url)s/map/names?" % self.config
|
||||
self.config['url_all_names'] = u"%(base_url)s/map/allNames?" % self.config
|
||||
|
||||
def getShowInfo(self, identifiers = None):
|
||||
if self.isDisabled():
|
||||
return {}
|
||||
|
||||
identifier = identifiers.get('thetvdb')
|
||||
|
||||
if not identifier:
|
||||
return {}
|
||||
|
||||
cache_key = 'xem.cache.%s' % identifier
|
||||
log.debug('Getting showInfo: %s', cache_key)
|
||||
result = self.getCache(cache_key) or {}
|
||||
if result:
|
||||
return result
|
||||
|
||||
result['seasons'] = {}
|
||||
|
||||
# Create season/episode and absolute mappings
|
||||
url = self.config['url_all'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
|
||||
response = self.getJsonData(url)
|
||||
|
||||
if response and response.get('result') == 'success':
|
||||
data = response.get('data', None)
|
||||
self.parseMaps(result, data)
|
||||
|
||||
# Create name alias mappings
|
||||
url = self.config['url_names'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
|
||||
response = self.getJsonData(url)
|
||||
|
||||
if response and response.get('result') == 'success':
|
||||
data = response.get('data', None)
|
||||
self.parseNames(result, data)
|
||||
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
|
||||
def getEpisodeInfo(self, identifiers = None, params = {}):
|
||||
episode_num = params.get('episode_number', None)
|
||||
if episode_num is None:
|
||||
return False
|
||||
|
||||
season_num = params.get('season_number', None)
|
||||
if season_num is None:
|
||||
return False
|
||||
|
||||
result = self.getShowInfo(identifiers)
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
# Find season
|
||||
if season_num not in result['seasons']:
|
||||
return False
|
||||
|
||||
season = result['seasons'][season_num]
|
||||
|
||||
# Find episode
|
||||
if episode_num not in season['episodes']:
|
||||
return False
|
||||
|
||||
return season['episodes'][episode_num]
|
||||
|
||||
def parseMaps(self, result, data, master = 'tvdb'):
|
||||
'''parses xem map and returns a custom formatted dict map
|
||||
|
||||
To retreive map for scene:
|
||||
if 'scene' in map['map_episode'][1][1]:
|
||||
print map['map_episode'][1][1]['scene']['season']
|
||||
'''
|
||||
if not isinstance(data, list):
|
||||
return
|
||||
|
||||
for episode_map in data:
|
||||
origin = episode_map.pop(master, None)
|
||||
if origin is None:
|
||||
continue # No master origin to map to
|
||||
|
||||
o_season = origin['season']
|
||||
o_episode = origin['episode']
|
||||
|
||||
# Create season info
|
||||
if o_season not in result['seasons']:
|
||||
result['seasons'][o_season] = {}
|
||||
|
||||
season = result['seasons'][o_season]
|
||||
|
||||
if 'episodes' not in season:
|
||||
season['episodes'] = {}
|
||||
|
||||
# Create episode info
|
||||
if o_episode not in season['episodes']:
|
||||
season['episodes'][o_episode] = {}
|
||||
|
||||
episode = season['episodes'][o_episode]
|
||||
episode['episode_map'] = episode_map
|
||||
|
||||
def parseNames(self, result, data):
|
||||
result['title_map'] = data.pop('all', None)
|
||||
|
||||
for season, title_map in data.items():
|
||||
season = int(season)
|
||||
|
||||
# Create season info
|
||||
if season not in result['seasons']:
|
||||
result['seasons'][season] = {}
|
||||
|
||||
season = result['seasons'][season]
|
||||
season['title_map'] = title_map
|
||||
|
||||
def isDisabled(self):
|
||||
if __name__ == '__main__':
|
||||
return False
|
||||
if self.conf('enabled'):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'xem',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'name': 'xem',
|
||||
'label': 'TheXem',
|
||||
'hidden': True,
|
||||
'description': 'Used for all calls to TheXem.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': True,
|
||||
'label': 'Enabled',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,51 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media._base.providers.nzb.binsearch import Base
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.environment import Env
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'BinSearch'
|
||||
|
||||
|
||||
class BinSearch(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
query = tryUrlencode({
|
||||
'q': fireEvent('media.search_query', media, single = True),
|
||||
'm': 'n',
|
||||
'max': 400,
|
||||
'adv_age': Env.setting('retention', 'nzb'),
|
||||
'adv_sort': 'date',
|
||||
'adv_col': 'on',
|
||||
'adv_nfo': 'on',
|
||||
'minsize': quality.get('size_min'),
|
||||
'maxsize': quality.get('size_max'),
|
||||
})
|
||||
return query
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
query = tryUrlencode({
|
||||
'q': fireEvent('media.search_query', media, single = True),
|
||||
'm': 'n',
|
||||
'max': 400,
|
||||
'adv_age': Env.setting('retention', 'nzb'),
|
||||
'adv_sort': 'date',
|
||||
'adv_col': 'on',
|
||||
'adv_nfo': 'on',
|
||||
'minsize': quality.get('size_min'),
|
||||
'maxsize': quality.get('size_max'),
|
||||
})
|
||||
return query
|
||||
@@ -1,49 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media._base.providers.nzb.newznab import Base
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Newznab'
|
||||
|
||||
|
||||
class Newznab(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
def buildUrl(self, media, host):
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
identifier = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
query = tryUrlencode({
|
||||
't': 'tvsearch',
|
||||
'apikey': host['api_key'],
|
||||
'q': related['show']['title'],
|
||||
'season': identifier['season'],
|
||||
'extended': 1
|
||||
})
|
||||
return query
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
def buildUrl(self, media, host):
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
identifier = fireEvent('library.identifier', media, single = True)
|
||||
query = tryUrlencode({
|
||||
't': 'tvsearch',
|
||||
'apikey': host['api_key'],
|
||||
'q': related['show']['title'],
|
||||
'season': identifier['season'],
|
||||
'ep': identifier['episode'],
|
||||
'extended': 1
|
||||
})
|
||||
|
||||
return query
|
||||
@@ -1,52 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.nzb.nzbclub import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'NZBClub'
|
||||
|
||||
|
||||
class NZBClub(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
def buildUrl(self, media):
|
||||
|
||||
q = tryUrlencode({
|
||||
'q': fireEvent('media.search_query', media, single = True),
|
||||
})
|
||||
|
||||
query = tryUrlencode({
|
||||
'ig': 1,
|
||||
'rpp': 200,
|
||||
'st': 5,
|
||||
'sp': 1,
|
||||
'ns': 1,
|
||||
})
|
||||
return '%s&%s' % (q, query)
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
def buildUrl(self, media):
|
||||
|
||||
q = tryUrlencode({
|
||||
'q': fireEvent('media.search_query', media, single = True),
|
||||
})
|
||||
|
||||
query = tryUrlencode({
|
||||
'ig': 1,
|
||||
'rpp': 200,
|
||||
'st': 5,
|
||||
'sp': 1,
|
||||
'ns': 1,
|
||||
})
|
||||
return '%s&%s' % (q, query)
|
||||
@@ -1,51 +0,0 @@
|
||||
from couchpotato import Env
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.nzb.nzbindex import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'NzbIndex'
|
||||
|
||||
|
||||
class NzbIndex(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
query = tryUrlencode({
|
||||
'q': fireEvent('media.search_query', media, single = True),
|
||||
'age': Env.setting('retention', 'nzb'),
|
||||
'sort': 'agedesc',
|
||||
'minsize': quality.get('size_min'),
|
||||
'maxsize': quality.get('size_max'),
|
||||
'rating': 1,
|
||||
'max': 250,
|
||||
'more': 1,
|
||||
'complete': 1,
|
||||
})
|
||||
return query
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
query = tryUrlencode({
|
||||
'q': fireEvent('media.search_query', media, single = True),
|
||||
'age': Env.setting('retention', 'nzb'),
|
||||
'sort': 'agedesc',
|
||||
'minsize': quality.get('size_min'),
|
||||
'maxsize': quality.get('size_max'),
|
||||
'rating': 1,
|
||||
'max': 250,
|
||||
'more': 1,
|
||||
'complete': 1,
|
||||
})
|
||||
return query
|
||||
@@ -1,36 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.bithdtv import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'BiTHDTV'
|
||||
|
||||
|
||||
class BiTHDTV(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
def buildUrl(self, media):
|
||||
query = tryUrlencode({
|
||||
'search': fireEvent('media.search_query', media, single = True),
|
||||
'cat': 12 # Season cat
|
||||
})
|
||||
return query
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
def buildUrl(self, media):
|
||||
query = tryUrlencode({
|
||||
'search': fireEvent('media.search_query', media, single = True),
|
||||
'cat': 10 # Episode cat
|
||||
})
|
||||
return query
|
||||
@@ -1,41 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.bitsoup import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Bitsoup'
|
||||
|
||||
|
||||
class Bitsoup(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
# For season bundles, bitsoup currently only has one category
|
||||
def buildUrl(self, media, quality):
|
||||
query = tryUrlencode({
|
||||
'search': fireEvent('media.search_query', media, single = True),
|
||||
'cat': 45 # TV-Packs Category
|
||||
})
|
||||
return query
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
cat_ids = [
|
||||
([42], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
|
||||
([49], ['hdtv_sd', 'webdl_480p'])
|
||||
]
|
||||
cat_backup_id = 0
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
query = tryUrlencode({
|
||||
'search': fireEvent('media.search_query', media, single = True),
|
||||
'cat': self.getCatId(quality['identifier'])[0],
|
||||
})
|
||||
return query
|
||||
@@ -1,37 +0,0 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.iptorrents import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'IPTorrents'
|
||||
|
||||
|
||||
class IPTorrents(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
# TODO come back to this later, a better quality system needs to be created
|
||||
cat_ids = [
|
||||
([65], [
|
||||
'bluray_1080p', 'bluray_720p',
|
||||
'bdrip_1080p', 'bdrip_720p',
|
||||
'brrip_1080p', 'brrip_720p',
|
||||
'webdl_1080p', 'webdl_720p', 'webdl_480p',
|
||||
'hdtv_720p', 'hdtv_sd'
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
# TODO come back to this later, a better quality system needs to be created
|
||||
cat_ids = [
|
||||
([5], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
([4, 78, 79], ['hdtv_sd'])
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.publichd import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'PublicHD'
|
||||
|
||||
|
||||
class PublicHD(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
def buildUrl(self, media):
|
||||
return fireEvent('media.search_query', media, single = True)
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
def buildUrl(self, media):
|
||||
return fireEvent('media.search_query', media, single = True)
|
||||
@@ -1,60 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.sceneaccess import Base
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'SceneAccess'
|
||||
|
||||
|
||||
class SceneAccess(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([26], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
]
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
url = self.urls['archive'] % (
|
||||
self.getCatId(quality['identifier'])[0],
|
||||
self.getCatId(quality['identifier'])[0]
|
||||
)
|
||||
|
||||
arguments = tryUrlencode({
|
||||
'search': fireEvent('media.search_query', media, single = True),
|
||||
'method': 3,
|
||||
})
|
||||
query = "%s&%s" % (url, arguments)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([27], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
([17, 11], ['hdtv_sd'])
|
||||
]
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
url = self.urls['search'] % (
|
||||
self.getCatId(quality['identifier'])[0],
|
||||
self.getCatId(quality['identifier'])[0]
|
||||
)
|
||||
|
||||
arguments = tryUrlencode({
|
||||
'search': fireEvent('media.search_query', media, single = True),
|
||||
'method': 3,
|
||||
})
|
||||
query = "%s&%s" % (url, arguments)
|
||||
|
||||
return query
|
||||
@@ -1,46 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.thepiratebay import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'ThePirateBay'
|
||||
|
||||
|
||||
class ThePirateBay(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([208], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
([205], ['hdtv_sd'])
|
||||
]
|
||||
|
||||
def buildUrl(self, media, page, cats):
|
||||
return (
|
||||
tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)),
|
||||
page,
|
||||
','.join(str(x) for x in cats)
|
||||
)
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([208], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
([205], ['hdtv_sd'])
|
||||
]
|
||||
|
||||
def buildUrl(self, media, page, cats):
|
||||
return (
|
||||
tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)),
|
||||
page,
|
||||
','.join(str(x) for x in cats)
|
||||
)
|
||||
@@ -1,34 +0,0 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.torrentday import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TorrentDay'
|
||||
|
||||
|
||||
class TorrentDay(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([14], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
]
|
||||
def buildUrl(self, media):
|
||||
return fireEvent('media.search_query', media, single = True)
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
cat_ids = [
|
||||
([7], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
([2], [24], [26], ['hdtv_sd'])
|
||||
]
|
||||
def buildUrl(self, media):
|
||||
return fireEvent('media.search_query', media, single = True)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
from couchpotato import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.torrentleech import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TorrentLeech'
|
||||
|
||||
|
||||
class TorrentLeech(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([27], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
]
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
return (
|
||||
tryUrlencode(fireEvent('media.search_query', media, single = True)),
|
||||
self.getCatId(quality['identifier'])[0]
|
||||
)
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([32], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
|
||||
([26], ['hdtv_sd'])
|
||||
]
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
return (
|
||||
tryUrlencode(fireEvent('media.search_query', media, single = True)),
|
||||
self.getCatId(quality['identifier'])[0]
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.torrentpotato import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TorrentPotato'
|
||||
|
||||
|
||||
class TorrentPotato(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
|
||||
def buildUrl(self, media, host):
|
||||
arguments = tryUrlencode({
|
||||
'user': host['name'],
|
||||
'passkey': host['pass_key'],
|
||||
'search': fireEvent('media.search_query', media, single = True)
|
||||
})
|
||||
return '%s?%s' % (host['host'], arguments)
|
||||
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
|
||||
def buildUrl(self, media, host):
|
||||
arguments = tryUrlencode({
|
||||
'user': host['name'],
|
||||
'passkey': host['pass_key'],
|
||||
'search': fireEvent('media.search_query', media, single = True)
|
||||
})
|
||||
return '%s?%s' % (host['host'], arguments)
|
||||
@@ -1,52 +0,0 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.providers.base import SeasonProvider, EpisodeProvider
|
||||
from couchpotato.core.media._base.providers.torrent.torrentshack import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TorrentShack'
|
||||
|
||||
|
||||
class TorrentShack(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Season(SeasonProvider, Base):
|
||||
# TorrentShack tv season search categories
|
||||
# TV-SD Pack - 980
|
||||
# TV-HD Pack - 981
|
||||
# Full Blu-ray - 970
|
||||
cat_ids = [
|
||||
([980], ['hdtv_sd']),
|
||||
([981], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
|
||||
([970], ['bluray_1080p', 'bluray_720p']),
|
||||
]
|
||||
cat_backup_id = 980
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
query = (tryUrlencode(fireEvent('media.search_query', media, single = True)),
|
||||
self.getCatId(quality['identifier'])[0],
|
||||
self.getSceneOnly())
|
||||
return query
|
||||
|
||||
class Episode(EpisodeProvider, Base):
|
||||
# TorrentShack tv episode search categories
|
||||
# TV/x264-HD - 600
|
||||
# TV/x264-SD - 620
|
||||
# TV/DVDrip - 700
|
||||
cat_ids = [
|
||||
([600], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
|
||||
([620], ['hdtv_sd'])
|
||||
]
|
||||
cat_backup_id = 620
|
||||
|
||||
def buildUrl(self, media, quality):
|
||||
query = (tryUrlencode(fireEvent('media.search_query', media, single = True)),
|
||||
self.getCatId(quality['identifier'])[0],
|
||||
self.getSceneOnly())
|
||||
return query
|
||||
@@ -1,152 +0,0 @@
|
||||
from couchpotato import fireEvent, get_db, Env
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEventAsync
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
from couchpotato.core.media._base.searcher.main import SearchSetupError
|
||||
from couchpotato.core.media.show import ShowTypeBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'EpisodeSearcher'
|
||||
|
||||
|
||||
class EpisodeSearcher(SearcherBase, ShowTypeBase):
|
||||
type = 'episode'
|
||||
|
||||
in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
super(EpisodeSearcher, self).__init__()
|
||||
|
||||
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
|
||||
addEvent('%s.searcher.single' % self.getType(), self.single)
|
||||
addEvent('searcher.correct_release', self.correctRelease)
|
||||
|
||||
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
|
||||
'desc': 'Starts a full search for all wanted shows',
|
||||
})
|
||||
|
||||
addApiView('%s.searcher.single' % self.getType(), self.singleView)
|
||||
|
||||
def searchAllView(self, **kwargs):
|
||||
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
|
||||
|
||||
return {
|
||||
'success': not self.in_progress
|
||||
}
|
||||
|
||||
def searchAll(self, manual = False):
|
||||
pass
|
||||
|
||||
def singleView(self, media_id, **kwargs):
|
||||
db = get_db()
|
||||
media = db.get('id', media_id)
|
||||
|
||||
return {
|
||||
'result': fireEvent('%s.searcher.single' % self.getType(), media, single = True)
|
||||
}
|
||||
|
||||
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
|
||||
db = get_db()
|
||||
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
|
||||
# TODO search_protocols, profile, quality_order can be moved to a base method
|
||||
# Find out search type
|
||||
try:
|
||||
if not search_protocols:
|
||||
search_protocols = fireEvent('searcher.protocols', single = True)
|
||||
except SearchSetupError:
|
||||
return
|
||||
|
||||
if not profile and related['show']['profile_id']:
|
||||
profile = db.get('id', related['show']['profile_id'])
|
||||
|
||||
if not quality_order:
|
||||
quality_order = fireEvent('quality.order', single = True)
|
||||
|
||||
# TODO: check episode status
|
||||
# TODO: check air date
|
||||
#if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
|
||||
# too_early_to_search.append(quality_type['quality']['identifier'])
|
||||
# return
|
||||
|
||||
ret = False
|
||||
has_better_quality = None
|
||||
found_releases = []
|
||||
too_early_to_search = []
|
||||
|
||||
releases = fireEvent('release.for_media', media['_id'], single = True)
|
||||
query = fireEvent('library.query', media, condense = False, single = True)
|
||||
|
||||
index = 0
|
||||
for q_identifier in profile.get('qualities'):
|
||||
quality_custom = {
|
||||
'quality': q_identifier,
|
||||
'finish': profile['finish'][index],
|
||||
'wait_for': profile['wait_for'][index],
|
||||
'3d': profile['3d'][index] if profile.get('3d') else False
|
||||
}
|
||||
|
||||
has_better_quality = 0
|
||||
|
||||
# See if better quality is available
|
||||
for release in releases:
|
||||
if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']:
|
||||
has_better_quality += 1
|
||||
|
||||
# Don't search for quality lower then already available.
|
||||
if has_better_quality is 0:
|
||||
|
||||
log.info('Searching for %s in %s', (query, q_identifier))
|
||||
quality = fireEvent('quality.single', identifier = q_identifier, single = True)
|
||||
quality['custom'] = quality_custom
|
||||
|
||||
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
|
||||
if len(results) == 0:
|
||||
log.debug('Nothing found for %s in %s', (query, q_identifier))
|
||||
|
||||
# Add them to this movie releases list
|
||||
found_releases += fireEvent('release.create_from_search', results, media, quality, single = True)
|
||||
|
||||
# Try find a valid result and download it
|
||||
if fireEvent('release.try_download_result', results, media, quality, single = True):
|
||||
ret = True
|
||||
|
||||
# Remove releases that aren't found anymore
|
||||
for release in releases:
|
||||
if release.get('status') == 'available' and release.get('identifier') not in found_releases:
|
||||
fireEvent('release.delete', release.get('_id'), single = True)
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query))
|
||||
fireEvent('media.restatus', media['_id'])
|
||||
break
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown() or ret:
|
||||
break
|
||||
|
||||
if len(too_early_to_search) > 0:
|
||||
log.info2('Too early to search for %s, %s', (too_early_to_search, query))
|
||||
|
||||
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
|
||||
if media.get('type') != 'show.episode':
|
||||
return
|
||||
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
|
||||
if release.get('seeders') is None and 0 < retention < release.get('age', 0):
|
||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name']))
|
||||
return False
|
||||
|
||||
# Check for required and ignored words
|
||||
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
|
||||
return False
|
||||
|
||||
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
|
||||
match = fireEvent('matcher.match', release, media, quality, single = True)
|
||||
if match:
|
||||
return match.weight
|
||||
|
||||
return False
|
||||
@@ -1,172 +0,0 @@
|
||||
from couchpotato import get_db, Env
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
from couchpotato.core.media.movie.searcher import SearchSetupError
|
||||
from couchpotato.core.media.show import ShowTypeBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'SeasonSearcher'
|
||||
|
||||
|
||||
class SeasonSearcher(SearcherBase, ShowTypeBase):
|
||||
type = 'season'
|
||||
|
||||
in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
super(SeasonSearcher, self).__init__()
|
||||
|
||||
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
|
||||
addEvent('%s.searcher.single' % self.getType(), self.single)
|
||||
addEvent('searcher.correct_release', self.correctRelease)
|
||||
|
||||
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
|
||||
'desc': 'Starts a full search for all wanted seasons',
|
||||
})
|
||||
|
||||
def searchAllView(self, **kwargs):
|
||||
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
|
||||
|
||||
return {
|
||||
'success': not self.in_progress
|
||||
}
|
||||
|
||||
def searchAll(self, manual = False):
|
||||
pass
|
||||
|
||||
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
|
||||
db = get_db()
|
||||
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
|
||||
# TODO search_protocols, profile, quality_order can be moved to a base method
|
||||
# Find out search type
|
||||
try:
|
||||
if not search_protocols:
|
||||
search_protocols = fireEvent('searcher.protocols', single = True)
|
||||
except SearchSetupError:
|
||||
return
|
||||
|
||||
if not profile and related['show']['profile_id']:
|
||||
profile = db.get('id', related['show']['profile_id'])
|
||||
|
||||
if not quality_order:
|
||||
quality_order = fireEvent('quality.order', single = True)
|
||||
|
||||
# Find 'active' episodes
|
||||
episodes = related['episodes']
|
||||
episodes_active = []
|
||||
|
||||
for episode in episodes:
|
||||
if episode.get('status') != 'active':
|
||||
continue
|
||||
|
||||
episodes_active.append(episode)
|
||||
|
||||
if len(episodes_active) == len(episodes):
|
||||
# All episodes are 'active', try and search for full season
|
||||
if self.search(media, profile, quality_order, search_protocols):
|
||||
# Success, end season search
|
||||
return True
|
||||
else:
|
||||
log.info('Unable to find season pack, searching for individual episodes...')
|
||||
|
||||
# Search for each episode individually
|
||||
for episode in episodes_active:
|
||||
fireEvent('show.episode.searcher.single', episode, profile, quality_order, search_protocols, manual)
|
||||
|
||||
# TODO (testing) only grab one episode
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def search(self, media, profile, quality_order, search_protocols):
|
||||
# TODO: check episode status
|
||||
# TODO: check air date
|
||||
#if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
|
||||
# too_early_to_search.append(quality_type['quality']['identifier'])
|
||||
# return
|
||||
|
||||
ret = False
|
||||
has_better_quality = None
|
||||
found_releases = []
|
||||
too_early_to_search = []
|
||||
|
||||
releases = fireEvent('release.for_media', media['_id'], single = True)
|
||||
query = fireEvent('library.query', media, condense = False, single = True)
|
||||
|
||||
index = 0
|
||||
for q_identifier in profile.get('qualities'):
|
||||
quality_custom = {
|
||||
'quality': q_identifier,
|
||||
'finish': profile['finish'][index],
|
||||
'wait_for': profile['wait_for'][index],
|
||||
'3d': profile['3d'][index] if profile.get('3d') else False
|
||||
}
|
||||
|
||||
has_better_quality = 0
|
||||
|
||||
# See if better quality is available
|
||||
for release in releases:
|
||||
if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']:
|
||||
has_better_quality += 1
|
||||
|
||||
# Don't search for quality lower then already available.
|
||||
if has_better_quality is 0:
|
||||
|
||||
log.info('Searching for %s in %s', (query, q_identifier))
|
||||
quality = fireEvent('quality.single', identifier = q_identifier, single = True)
|
||||
quality['custom'] = quality_custom
|
||||
|
||||
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
|
||||
if len(results) == 0:
|
||||
log.debug('Nothing found for %s in %s', (query, q_identifier))
|
||||
|
||||
# Add them to this movie releases list
|
||||
found_releases += fireEvent('release.create_from_search', results, media, quality, single = True)
|
||||
|
||||
# Try find a valid result and download it
|
||||
if fireEvent('release.try_download_result', results, media, quality, single = True):
|
||||
ret = True
|
||||
|
||||
# Remove releases that aren't found anymore
|
||||
for release in releases:
|
||||
if release.get('status') == 'available' and release.get('identifier') not in found_releases:
|
||||
fireEvent('release.delete', release.get('_id'), single = True)
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query))
|
||||
fireEvent('media.restatus', media['_id'])
|
||||
break
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown() or ret:
|
||||
break
|
||||
|
||||
if len(too_early_to_search) > 0:
|
||||
log.info2('Too early to search for %s, %s', (too_early_to_search, query))
|
||||
|
||||
return len(found_releases) > 0
|
||||
|
||||
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
|
||||
if media.get('type') != 'show.season':
|
||||
return
|
||||
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
|
||||
if release.get('seeders') is None and 0 < retention < release.get('age', 0):
|
||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name']))
|
||||
return False
|
||||
|
||||
# Check for required and ignored words
|
||||
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
|
||||
return False
|
||||
|
||||
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
|
||||
match = fireEvent('matcher.match', release, media, quality, single = True)
|
||||
if match:
|
||||
return match.weight
|
||||
|
||||
return False
|
||||
@@ -1,88 +0,0 @@
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
from couchpotato.core.media._base.searcher.main import SearchSetupError
|
||||
from couchpotato.core.media.show import ShowTypeBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'ShowSearcher'
|
||||
|
||||
|
||||
class ShowSearcher(SearcherBase, ShowTypeBase):
|
||||
type = 'show'
|
||||
|
||||
in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
super(ShowSearcher, self).__init__()
|
||||
|
||||
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
|
||||
addEvent('%s.searcher.single' % self.getType(), self.single)
|
||||
addEvent('searcher.get_search_title', self.getSearchTitle)
|
||||
|
||||
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
|
||||
'desc': 'Starts a full search for all wanted episodes',
|
||||
})
|
||||
|
||||
def searchAllView(self, **kwargs):
|
||||
fireEventAsync('%s.searcher.all' % self.getType(), manual = True)
|
||||
|
||||
return {
|
||||
'success': not self.in_progress
|
||||
}
|
||||
|
||||
def searchAll(self, manual = False):
|
||||
pass
|
||||
|
||||
def single(self, media, search_protocols = None, manual = False):
|
||||
# Find out search type
|
||||
try:
|
||||
if not search_protocols:
|
||||
search_protocols = fireEvent('searcher.protocols', single = True)
|
||||
except SearchSetupError:
|
||||
return
|
||||
|
||||
if not media['profile_id'] or media['status'] == 'done':
|
||||
log.debug('Show doesn\'t have a profile or already done, assuming in manage tab.')
|
||||
return
|
||||
|
||||
show_title = fireEvent('media.search_query', media, condense = False, single = True)
|
||||
|
||||
fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title)
|
||||
|
||||
show_tree = fireEvent('library.tree', media, single = True)
|
||||
|
||||
db = get_db()
|
||||
|
||||
profile = db.get('id', media['profile_id'])
|
||||
quality_order = fireEvent('quality.order', single = True)
|
||||
|
||||
for season in show_tree.get('seasons', []):
|
||||
if not season.get('info'):
|
||||
continue
|
||||
|
||||
# Skip specials (and seasons missing 'number') for now
|
||||
# TODO: set status for specials to skipped by default
|
||||
if not season['info'].get('number'):
|
||||
continue
|
||||
|
||||
# Check if full season can be downloaded
|
||||
fireEvent('show.season.searcher.single', season, profile, quality_order, search_protocols, manual)
|
||||
|
||||
# TODO (testing) only snatch one season
|
||||
return
|
||||
|
||||
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True)
|
||||
|
||||
def getSearchTitle(self, media):
|
||||
if media.get('type') != 'show':
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
show = related['show']
|
||||
else:
|
||||
show = media
|
||||
|
||||
return getTitle(show)
|
||||
@@ -1,10 +1,10 @@
|
||||
from CodernityDB.tree_index import TreeBasedIndex
|
||||
from couchpotato.core.helpers.database import TreeBasedIndex
|
||||
|
||||
|
||||
class NotificationIndex(TreeBasedIndex):
|
||||
_version = 1
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
|
||||
custom_header = """from couchpotato.core.helpers.database import TreeBasedIndex
|
||||
import time"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -22,7 +22,7 @@ import time"""
|
||||
class NotificationUnreadIndex(TreeBasedIndex):
|
||||
_version = 1
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
|
||||
custom_header = """from couchpotato.core.helpers.database import TreeBasedIndex
|
||||
import time"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -3,11 +3,11 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from CodernityDB.database import RecordDeleted
|
||||
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView, addNonBlockApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.database import RecordDeleted
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
|
||||
68
couchpotato/core/notifications/notifymywp.py
Normal file
68
couchpotato/core/notifications/notifymywp.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from pynmwp import PyNMWP
|
||||
import six
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'NotifyMyWP'
|
||||
|
||||
|
||||
class NotifyMyWP(Notification):
|
||||
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
keys = splitString(self.conf('api_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)
|
||||
|
||||
for key in keys:
|
||||
if not response[key]['Code'] == six.u('200'):
|
||||
log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message']))
|
||||
return False
|
||||
|
||||
return response
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'notifymywp',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'notifymywp',
|
||||
'label': 'Windows Phone',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'description': 'Multiple keys seperated by a comma. Maximum of 5.'
|
||||
},
|
||||
{
|
||||
'name': 'dev_key',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'default': 0,
|
||||
'type': 'dropdown',
|
||||
'values': [('Very Low', -2), ('Moderate', -1), ('Normal', 0), ('High', 1), ('Emergency', 2)],
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import timedelta, datetime
|
||||
from urlparse import urlparse
|
||||
from six.moves import urllib
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
@@ -106,7 +106,7 @@ class PlexServer(object):
|
||||
def createHost(self, host, port = None):
|
||||
|
||||
h = cleanHost(host)
|
||||
p = urlparse(h)
|
||||
p = urllib.urlparse(h)
|
||||
h = h.rstrip('/')
|
||||
|
||||
if port and not p.port:
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
from .main import Twitter
|
||||
from six import PY3
|
||||
|
||||
try:
|
||||
from .main import Twitter
|
||||
|
||||
def autoload():
|
||||
return Twitter()
|
||||
def autoload():
|
||||
return Twitter()
|
||||
except:
|
||||
if PY3:
|
||||
from couchpotato.core.helpers.py3 import NotSupported
|
||||
raise NotSupported
|
||||
else:
|
||||
raise
|
||||
|
||||
config = [{
|
||||
'name': 'twitter',
|
||||
|
||||
@@ -3,12 +3,20 @@ import traceback
|
||||
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import xmpp
|
||||
from six import PY3
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Xmpp'
|
||||
try:
|
||||
import xmpp
|
||||
autoload = 'Xmpp'
|
||||
except:
|
||||
if PY3:
|
||||
from couchpotato.core.helpers.py3 import NotSupported
|
||||
raise NotSupported
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class Xmpp(Notification):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import threading
|
||||
from urllib import quote
|
||||
from urlparse import urlparse
|
||||
from six.moves import urllib
|
||||
import glob
|
||||
import inspect
|
||||
import os.path
|
||||
@@ -183,20 +182,20 @@ class Plugin(object):
|
||||
|
||||
# http request
|
||||
def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True, stream = False):
|
||||
url = quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
|
||||
url = urllib.parse.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
|
||||
|
||||
if not headers: headers = {}
|
||||
if not data: data = {}
|
||||
|
||||
# Fill in some headers
|
||||
parsed_url = urlparse(url)
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
|
||||
|
||||
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
||||
headers['Host'] = headers.get('Host', None)
|
||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
headers['Connection'] = headers.get('Connection', 'close')
|
||||
headers['Connection'] = headers.get('Connection', 'keep-alive')
|
||||
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
|
||||
|
||||
r = Env.get('http_opener')
|
||||
@@ -232,7 +231,11 @@ class Plugin(object):
|
||||
|
||||
status_code = response.status_code
|
||||
if response.status_code == requests.codes.ok:
|
||||
data = response if stream else response.content
|
||||
if stream:
|
||||
data = response
|
||||
else:
|
||||
data = response.content
|
||||
data = data.decode(response.encoding)
|
||||
else:
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -279,7 +282,7 @@ class Plugin(object):
|
||||
wait = (last_use - now) + self.http_time_between_calls
|
||||
|
||||
if wait > 0:
|
||||
log.debug('Waiting for %s, %d seconds', (self.getName(), max(1, wait)))
|
||||
log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
|
||||
time.sleep(min(wait, 30))
|
||||
|
||||
def beforeCall(self, handler):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from CodernityDB.tree_index import TreeBasedIndex
|
||||
from couchpotato.core.helpers.database import TreeBasedIndex
|
||||
|
||||
|
||||
class CategoryIndex(TreeBasedIndex):
|
||||
|
||||
5
couchpotato/core/plugins/dashboard.py
Executable file → Normal file
5
couchpotato/core/plugins/dashboard.py
Executable file → Normal file
@@ -1,13 +1,14 @@
|
||||
import random as rndm
|
||||
import time
|
||||
from CodernityDB.database import RecordDeleted
|
||||
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.database import RecordDeleted
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -41,7 +42,7 @@ class Dashboard(Plugin):
|
||||
# Add limit
|
||||
limit = 12
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, six.string_types) else limit_offset
|
||||
limit = tryInt(splt[0])
|
||||
|
||||
# Get all active medias
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -103,9 +103,8 @@ class Logging(Plugin):
|
||||
if not os.path.isfile(path):
|
||||
break
|
||||
|
||||
f = open(path, 'r')
|
||||
log_content = toUnicode(f.read())
|
||||
raw_lines = self.toList(log_content)
|
||||
f = codecs.open(path, 'r', 'utf-8')
|
||||
raw_lines = self.toList(f.read())
|
||||
raw_lines.reverse()
|
||||
|
||||
brk = False
|
||||
@@ -131,7 +130,7 @@ class Logging(Plugin):
|
||||
|
||||
def toList(self, log_content = ''):
|
||||
|
||||
logs_raw = toUnicode(log_content).split('[0m\n')
|
||||
logs_raw = log_content.split('[0m\n')
|
||||
|
||||
logs = []
|
||||
for log_line in logs_raw:
|
||||
|
||||
@@ -123,7 +123,7 @@ class Manage(Plugin):
|
||||
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, check_file_date = False, on_found = onFound, single = True)
|
||||
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():
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from CodernityDB.tree_index import TreeBasedIndex
|
||||
from couchpotato.core.helpers.database import TreeBasedIndex
|
||||
|
||||
|
||||
class ProfileIndex(TreeBasedIndex):
|
||||
|
||||
@@ -86,7 +86,6 @@ class ProfilePlugin(Plugin):
|
||||
'label': toUnicode(kwargs.get('label')),
|
||||
'order': tryInt(kwargs.get('order', 999)),
|
||||
'core': kwargs.get('core', False),
|
||||
'minimum_score': tryInt(kwargs.get('minimum_score', 1)),
|
||||
'qualities': [],
|
||||
'wait_for': [],
|
||||
'stop_after': [],
|
||||
@@ -218,7 +217,6 @@ class ProfilePlugin(Plugin):
|
||||
'label': toUnicode(profile.get('label')),
|
||||
'order': order,
|
||||
'qualities': profile.get('qualities'),
|
||||
'minimum_score': 1,
|
||||
'finish': [],
|
||||
'wait_for': [],
|
||||
'stop_after': [],
|
||||
|
||||
@@ -51,11 +51,6 @@
|
||||
margin: 0 5px !important;
|
||||
}
|
||||
|
||||
.profile .wait_for .minimum_score_input {
|
||||
width: 40px !important;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.profile .types {
|
||||
padding: 0;
|
||||
margin: 0 20px 0 -4px;
|
||||
|
||||
@@ -53,21 +53,12 @@ var Profile = new Class({
|
||||
}),
|
||||
new Element('span', {'text':'day(s) for a better quality '}),
|
||||
new Element('span.advanced', {'text':'and keep searching'}),
|
||||
|
||||
// "After a checked quality is found and downloaded, continue searching for even better quality releases for the entered number of days."
|
||||
new Element('input.inlay.xsmall.stop_after_input.advanced', {
|
||||
'type':'text',
|
||||
'value': data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0
|
||||
}),
|
||||
new Element('span.advanced', {'text':'day(s) for a better (checked) quality.'}),
|
||||
|
||||
// Minimum score of
|
||||
new Element('span.advanced', {'html':'<br/>Releases need a minimum score of'}),
|
||||
new Element('input.advanced.inlay.xsmall.minimum_score_input', {
|
||||
'size': 4,
|
||||
'type':'text',
|
||||
'value': data.minimum_score || 1
|
||||
})
|
||||
new Element('span.advanced', {'text':'day(s) for a better (checked) quality.'})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -135,7 +126,6 @@ var Profile = new Class({
|
||||
'label' : self.el.getElement('.quality_label input').get('value'),
|
||||
'wait_for' : self.el.getElement('.wait_for_input').get('value'),
|
||||
'stop_after' : self.el.getElement('.stop_after_input').get('value'),
|
||||
'minimum_score' : self.el.getElement('.minimum_score_input').get('value'),
|
||||
'types': []
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from hashlib import md5
|
||||
|
||||
from CodernityDB.hash_index import HashIndex
|
||||
from couchpotato.core.helpers.database import HashIndex
|
||||
|
||||
|
||||
class QualityIndex(HashIndex):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user