Merge branch 'refs/heads/develop' into desktop
This commit is contained in:
@@ -110,6 +110,7 @@ class ApiHandler(RequestHandler):
|
|||||||
|
|
||||||
if jsonp_callback:
|
if jsonp_callback:
|
||||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||||
|
self.set_header("Content-Type", "text/javascript")
|
||||||
elif isinstance(result, tuple) and result[0] == 'redirect':
|
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||||
self.redirect(result[1])
|
self.redirect(result[1])
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ class SourceUpdater(BaseUpdater):
|
|||||||
|
|
||||||
def replaceWith(self, path):
|
def replaceWith(self, path):
|
||||||
app_dir = ss(Env.get('app_dir'))
|
app_dir = ss(Env.get('app_dir'))
|
||||||
|
data_dir = ss(Env.get('data_dir'))
|
||||||
|
|
||||||
# Get list of files we want to overwrite
|
# Get list of files we want to overwrite
|
||||||
self.deletePyc()
|
self.deletePyc()
|
||||||
@@ -329,12 +330,15 @@ class SourceUpdater(BaseUpdater):
|
|||||||
log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
|
log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if Env.get('app_dir') not in Env.get('data_dir'):
|
for still_exists in existing_files:
|
||||||
for still_exists in existing_files:
|
|
||||||
try:
|
if data_dir in still_exists:
|
||||||
os.remove(still_exists)
|
continue
|
||||||
except:
|
|
||||||
log.error('Failed removing non-used file: %s', traceback.format_exc())
|
try:
|
||||||
|
os.remove(still_exists)
|
||||||
|
except:
|
||||||
|
log.error('Failed removing non-used file: %s', traceback.format_exc())
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
config = {
|
config = [{
|
||||||
'name': 'download_providers',
|
'name': 'download_providers',
|
||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
@@ -10,4 +10,4 @@ config = {
|
|||||||
'options': [],
|
'options': [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}]
|
||||||
|
|||||||
@@ -66,36 +66,36 @@ class Downloader(Provider):
|
|||||||
def getAllDownloadStatus(self):
|
def getAllDownloadStatus(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
def _removeFailed(self, item):
|
def _removeFailed(self, release_download):
|
||||||
if self.isDisabled(manual = True, data = {}):
|
if self.isDisabled(manual = True, data = {}):
|
||||||
return
|
return
|
||||||
|
|
||||||
if item and item.get('downloader') == self.getName():
|
if release_download and release_download.get('downloader') == self.getName():
|
||||||
if self.conf('delete_failed'):
|
if self.conf('delete_failed'):
|
||||||
return self.removeFailed(item)
|
return self.removeFailed(release_download)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
return
|
return
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, release_download):
|
||||||
return
|
return
|
||||||
|
|
||||||
def _processComplete(self, item):
|
def _processComplete(self, release_download):
|
||||||
if self.isDisabled(manual = True, data = {}):
|
if self.isDisabled(manual = True, data = {}):
|
||||||
return
|
return
|
||||||
|
|
||||||
if item and item.get('downloader') == self.getName():
|
if release_download and release_download.get('downloader') == self.getName():
|
||||||
if self.conf('remove_complete', default = False):
|
if self.conf('remove_complete', default = False):
|
||||||
return self.processComplete(item = item, delete_files = self.conf('delete_files', default = False))
|
return self.processComplete(release_download = release_download, delete_files = self.conf('delete_files', default = False))
|
||||||
|
|
||||||
return False
|
return False
|
||||||
return
|
return
|
||||||
|
|
||||||
def processComplete(self, item, delete_files):
|
def processComplete(self, release_download, delete_files):
|
||||||
return
|
return
|
||||||
|
|
||||||
def isCorrectProtocol(self, item_protocol):
|
def isCorrectProtocol(self, protocol):
|
||||||
is_correct = item_protocol in self.protocol
|
is_correct = protocol in self.protocol
|
||||||
|
|
||||||
if not is_correct:
|
if not is_correct:
|
||||||
log.debug("Downloader doesn't support this protocol")
|
log.debug("Downloader doesn't support this protocol")
|
||||||
@@ -151,20 +151,20 @@ class Downloader(Provider):
|
|||||||
(d_manual and manual or d_manual is False) and \
|
(d_manual and manual or d_manual is False) and \
|
||||||
(not data or self.isCorrectProtocol(data.get('protocol')))
|
(not data or self.isCorrectProtocol(data.get('protocol')))
|
||||||
|
|
||||||
def _pause(self, item, pause = True):
|
def _pause(self, release_download, pause = True):
|
||||||
if self.isDisabled(manual = True, data = {}):
|
if self.isDisabled(manual = True, data = {}):
|
||||||
return
|
return
|
||||||
|
|
||||||
if item and item.get('downloader') == self.getName():
|
if release_download and release_download.get('downloader') == self.getName():
|
||||||
self.pause(item, pause)
|
self.pause(release_download, pause)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def pause(self, item, pause):
|
def pause(self, release_download, pause):
|
||||||
return
|
return
|
||||||
|
|
||||||
class StatusList(list):
|
class ReleaseDownloadList(list):
|
||||||
|
|
||||||
provider = None
|
provider = None
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class StatusList(list):
|
|||||||
self.provider = provider
|
self.provider = provider
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
super(StatusList, self).__init__()
|
super(ReleaseDownloadList, self).__init__()
|
||||||
|
|
||||||
def extend(self, results):
|
def extend(self, results):
|
||||||
for r in results:
|
for r in results:
|
||||||
@@ -181,7 +181,7 @@ class StatusList(list):
|
|||||||
|
|
||||||
def append(self, result):
|
def append(self, result):
|
||||||
new_result = self.fillResult(result)
|
new_result = self.fillResult(result)
|
||||||
super(StatusList, self).append(new_result)
|
super(ReleaseDownloadList, self).append(new_result)
|
||||||
|
|
||||||
def fillResult(self, result):
|
def fillResult(self, result):
|
||||||
|
|
||||||
@@ -190,6 +190,7 @@ class StatusList(list):
|
|||||||
'status': 'busy',
|
'status': 'busy',
|
||||||
'downloader': self.provider.getName(),
|
'downloader': self.provider.getName(),
|
||||||
'folder': '',
|
'folder': '',
|
||||||
|
'files': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeDicts(defaults, result)
|
return mergeDicts(defaults, result)
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ config = [{
|
|||||||
'type': 'dropdown',
|
'type': 'dropdown',
|
||||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'create_subdir',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Create a sub directory when saving the .nzb (or .torrent).',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'manual',
|
'name': 'manual',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
|
|||||||
@@ -33,17 +33,27 @@ class Blackhole(Downloader):
|
|||||||
log.error('No nzb/torrent available: %s', data.get('url'))
|
log.error('No nzb/torrent available: %s', data.get('url'))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
|
file_name = self.createFileName(data, filedata, movie)
|
||||||
|
full_path = os.path.join(directory, file_name)
|
||||||
|
|
||||||
|
if self.conf('create_subdir'):
|
||||||
|
try:
|
||||||
|
new_path = os.path.splitext(full_path)[0]
|
||||||
|
if not os.path.exists(new_path):
|
||||||
|
os.makedirs(new_path)
|
||||||
|
full_path = os.path.join(new_path, file_name)
|
||||||
|
except:
|
||||||
|
log.error('Couldnt create sub dir, reverting to old one: %s', full_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not os.path.isfile(fullPath):
|
if not os.path.isfile(full_path):
|
||||||
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
|
log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
|
||||||
with open(fullPath, 'wb') as f:
|
with open(full_path, 'wb') as f:
|
||||||
f.write(filedata)
|
f.write(filedata)
|
||||||
os.chmod(fullPath, Env.getPermission('file'))
|
os.chmod(full_path, Env.getPermission('file'))
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
log.info('File %s already exists.', fullPath)
|
log.info('File %s already exists.', full_path)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode, b16encode, b32decode
|
||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from bencode import bencode as benc, bdecode
|
||||||
from couchpotato.core.helpers.encoding import isInt, ss
|
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||||
|
from couchpotato.core.helpers.encoding import isInt, sp
|
||||||
from couchpotato.core.helpers.variable import tryFloat
|
from couchpotato.core.helpers.variable import tryFloat
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from hashlib import sha1
|
||||||
from synchronousdeluge import DelugeClient
|
from synchronousdeluge import DelugeClient
|
||||||
import os.path
|
import os.path
|
||||||
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -72,7 +74,7 @@ class Deluge(Downloader):
|
|||||||
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
||||||
else:
|
else:
|
||||||
filename = self.createFileName(data, filedata, movie)
|
filename = self.createFileName(data, filedata, movie)
|
||||||
remote_torrent = self.drpc.add_torrent_file(filename, b64encode(filedata), options)
|
remote_torrent = self.drpc.add_torrent_file(filename, filedata, options)
|
||||||
|
|
||||||
if not remote_torrent:
|
if not remote_torrent:
|
||||||
log.error('Failed sending torrent to Deluge')
|
log.error('Failed sending torrent to Deluge')
|
||||||
@@ -85,14 +87,10 @@ class Deluge(Downloader):
|
|||||||
|
|
||||||
log.debug('Checking Deluge download status.')
|
log.debug('Checking Deluge download status.')
|
||||||
|
|
||||||
if not os.path.isdir(Env.setting('from', 'renamer')):
|
|
||||||
log.error('Renamer "from" folder doesn\'t to exist.')
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
statuses = StatusList(self)
|
release_downloads = ReleaseDownloadList(self)
|
||||||
|
|
||||||
queue = self.drpc.get_alltorrents()
|
queue = self.drpc.get_alltorrents()
|
||||||
|
|
||||||
@@ -101,50 +99,55 @@ class Deluge(Downloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
for torrent_id in queue:
|
for torrent_id in queue:
|
||||||
item = queue[torrent_id]
|
torrent = queue[torrent_id]
|
||||||
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (item['name'], item['hash'], item['save_path'], item['move_completed_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], item['stop_ratio'], item['is_seed'], item['is_finished'], item['paused']))
|
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
|
||||||
|
|
||||||
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||||
#status = 'failed'
|
#status = 'failed'
|
||||||
status = 'busy'
|
status = 'busy'
|
||||||
if item['is_seed'] and tryFloat(item['ratio']) < tryFloat(item['stop_ratio']):
|
if torrent['is_seed'] and tryFloat(torrent['ratio']) < tryFloat(torrent['stop_ratio']):
|
||||||
# We have item['seeding_time'] to work out what the seeding time is, but we do not
|
# We have torrent['seeding_time'] to work out what the seeding time is, but we do not
|
||||||
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
||||||
# when the torrent is added. So Deluge will only look at the ratio.
|
# when the torrent is added. So Deluge will only look at the ratio.
|
||||||
# See above comment in download().
|
# See above comment in download().
|
||||||
status = 'seeding'
|
status = 'seeding'
|
||||||
elif item['is_seed'] and item['is_finished'] and item['paused'] and item['state'] == 'Paused':
|
elif torrent['is_seed'] and torrent['is_finished'] and torrent['paused'] and torrent['state'] == 'Paused':
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
|
|
||||||
download_dir = item['save_path']
|
download_dir = sp(torrent['save_path'])
|
||||||
if item['move_on_completed']:
|
if torrent['move_on_completed']:
|
||||||
download_dir = item['move_completed_path']
|
download_dir = torrent['move_completed_path']
|
||||||
|
|
||||||
statuses.append({
|
torrent_files = []
|
||||||
'id': item['hash'],
|
for file_item in torrent['files']:
|
||||||
'name': item['name'],
|
torrent_files.append(sp(os.path.join(download_dir, file_item['path'])))
|
||||||
|
|
||||||
|
release_downloads.append({
|
||||||
|
'id': torrent['hash'],
|
||||||
|
'name': torrent['name'],
|
||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['state'],
|
'original_status': torrent['state'],
|
||||||
'seed_ratio': item['ratio'],
|
'seed_ratio': torrent['ratio'],
|
||||||
'timeleft': str(timedelta(seconds = item['eta'])),
|
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||||
'folder': ss(os.path.join(download_dir, item['name'])),
|
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])),
|
||||||
|
'files': '|'.join(torrent_files),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return release_downloads
|
||||||
|
|
||||||
def pause(self, item, pause = True):
|
def pause(self, release_download, pause = True):
|
||||||
if pause:
|
if pause:
|
||||||
return self.drpc.pause_torrent([item['id']])
|
return self.drpc.pause_torrent([release_download['id']])
|
||||||
else:
|
else:
|
||||||
return self.drpc.resume_torrent([item['id']])
|
return self.drpc.resume_torrent([release_download['id']])
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, release_download):
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||||
return self.drpc.remove_torrent(item['id'], True)
|
return self.drpc.remove_torrent(release_download['id'], True)
|
||||||
|
|
||||||
def processComplete(self, item, delete_files = False):
|
def processComplete(self, release_download, delete_files = False):
|
||||||
log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files)
|
return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files)
|
||||||
|
|
||||||
class DelugeRPC(object):
|
class DelugeRPC(object):
|
||||||
|
|
||||||
@@ -171,7 +174,10 @@ class DelugeRPC(object):
|
|||||||
try:
|
try:
|
||||||
self.connect()
|
self.connect()
|
||||||
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
|
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
|
||||||
if options['label']:
|
if not torrent_id:
|
||||||
|
torrent_id = self._check_torrent(True, torrent)
|
||||||
|
|
||||||
|
if torrent_id and options['label']:
|
||||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
||||||
@@ -185,8 +191,11 @@ class DelugeRPC(object):
|
|||||||
torrent_id = False
|
torrent_id = False
|
||||||
try:
|
try:
|
||||||
self.connect()
|
self.connect()
|
||||||
torrent_id = self.client.core.add_torrent_file(filename, torrent, options).get()
|
torrent_id = self.client.core.add_torrent_file(filename, b64encode(torrent), options).get()
|
||||||
if options['label']:
|
if not torrent_id:
|
||||||
|
torrent_id = self._check_torrent(False, torrent)
|
||||||
|
|
||||||
|
if torrent_id and options['label']:
|
||||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
||||||
@@ -242,3 +251,22 @@ class DelugeRPC(object):
|
|||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
self.client.disconnect()
|
self.client.disconnect()
|
||||||
|
|
||||||
|
def _check_torrent(self, magnet, torrent):
|
||||||
|
# Torrent not added, check if it already existed.
|
||||||
|
if magnet:
|
||||||
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', torrent)[0]
|
||||||
|
else:
|
||||||
|
info = bdecode(torrent)["info"]
|
||||||
|
torrent_hash = sha1(benc(info)).hexdigest()
|
||||||
|
|
||||||
|
# Convert base 32 to hex
|
||||||
|
if len(torrent_hash) == 32:
|
||||||
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||||
|
|
||||||
|
torrent_hash = torrent_hash.lower()
|
||||||
|
torrent_check = self.client.core.get_torrent_status(torrent_hash, {}).get()
|
||||||
|
if torrent_check['hash']:
|
||||||
|
return torrent_hash
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ config = [{
|
|||||||
'name': 'nzbget',
|
'name': 'nzbget',
|
||||||
'label': 'NZBGet',
|
'label': 'NZBGet',
|
||||||
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
|
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
|
||||||
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss, sp
|
||||||
from couchpotato.core.helpers.variable import tryInt, md5
|
from couchpotato.core.helpers.variable import tryInt, md5
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -99,60 +99,60 @@ class NZBGet(Downloader):
|
|||||||
log.error('Failed getting data: %s', traceback.format_exc(1))
|
log.error('Failed getting data: %s', traceback.format_exc(1))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
statuses = StatusList(self)
|
release_downloads = ReleaseDownloadList(self)
|
||||||
|
|
||||||
for item in groups:
|
for nzb in groups:
|
||||||
log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
|
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
|
||||||
try:
|
try:
|
||||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||||
except:
|
except:
|
||||||
nzb_id = item['NZBID']
|
nzb_id = nzb['NZBID']
|
||||||
|
|
||||||
|
|
||||||
timeleft = -1
|
timeleft = -1
|
||||||
try:
|
try:
|
||||||
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||||
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
statuses.append({
|
release_downloads.append({
|
||||||
'id': nzb_id,
|
'id': nzb_id,
|
||||||
'name': item['NZBFilename'],
|
'name': nzb['NZBFilename'],
|
||||||
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
|
'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED',
|
||||||
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
|
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
|
||||||
'timeleft': timeleft,
|
'timeleft': timeleft,
|
||||||
})
|
})
|
||||||
|
|
||||||
for item in queue: # 'Parameters' is not passed in rpc.postqueue
|
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||||
log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
|
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
||||||
statuses.append({
|
release_downloads.append({
|
||||||
'id': item['NZBID'],
|
'id': nzb['NZBID'],
|
||||||
'name': item['NZBFilename'],
|
'name': nzb['NZBFilename'],
|
||||||
'original_status': item['Stage'],
|
'original_status': nzb['Stage'],
|
||||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||||
})
|
})
|
||||||
|
|
||||||
for item in history:
|
for nzb in history:
|
||||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
|
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||||
try:
|
try:
|
||||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||||
except:
|
except:
|
||||||
nzb_id = item['NZBID']
|
nzb_id = nzb['NZBID']
|
||||||
statuses.append({
|
release_downloads.append({
|
||||||
'id': nzb_id,
|
'id': nzb_id,
|
||||||
'name': item['NZBFilename'],
|
'name': nzb['NZBFilename'],
|
||||||
'status': 'completed' if item['ParStatus'] in ['SUCCESS','NONE'] and item['ScriptStatus'] in ['SUCCESS','NONE'] else 'failed',
|
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
||||||
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
||||||
'timeleft': str(timedelta(seconds = 0)),
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
'folder': ss(item['DestDir'])
|
'folder': sp(nzb['DestDir'])
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return release_downloads
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, release_download):
|
||||||
|
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||||
|
|
||||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||||
|
|
||||||
@@ -179,9 +179,9 @@ class NZBGet(Downloader):
|
|||||||
|
|
||||||
for hist in history:
|
for hist in history:
|
||||||
for param in hist['Parameters']:
|
for param in hist['Parameters']:
|
||||||
if param['Name'] == 'couchpotato' and param['Value'] == item['id']:
|
if param['Name'] == 'couchpotato' and param['Value'] == release_download['id']:
|
||||||
nzb_id = hist['ID']
|
nzb_id = hist['ID']
|
||||||
path = hist['DestDir']
|
path = hist['DestDir']
|
||||||
|
|
||||||
if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
|
if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
|
||||||
shutil.rmtree(path, True)
|
shutil.rmtree(path, True)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
from couchpotato.core.helpers.encoding import tryUrlencode, sp
|
||||||
from couchpotato.core.helpers.variable import cleanHost
|
from couchpotato.core.helpers.variable import cleanHost
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from urllib2 import URLError
|
from urllib2 import URLError
|
||||||
@@ -30,10 +30,10 @@ class NZBVortex(Downloader):
|
|||||||
# Send the nzb
|
# Send the nzb
|
||||||
try:
|
try:
|
||||||
nzb_filename = self.createFileName(data, filedata, movie)
|
nzb_filename = self.createFileName(data, filedata, movie)
|
||||||
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
|
self.call('nzb/add', params = {'file': (nzb_filename, filedata)}, multipart = True)
|
||||||
|
|
||||||
raw_statuses = self.call('nzb')
|
raw_statuses = self.call('nzb')
|
||||||
nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['name'] == nzb_filename][0]
|
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if nzb['name'] == nzb_filename][0]
|
||||||
return self.downloadReturnId(nzb_id)
|
return self.downloadReturnId(nzb_id)
|
||||||
except:
|
except:
|
||||||
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
|
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
|
||||||
@@ -43,33 +43,33 @@ class NZBVortex(Downloader):
|
|||||||
|
|
||||||
raw_statuses = self.call('nzb')
|
raw_statuses = self.call('nzb')
|
||||||
|
|
||||||
statuses = StatusList(self)
|
release_downloads = ReleaseDownloadList(self)
|
||||||
for item in raw_statuses.get('nzbs', []):
|
for nzb in raw_statuses.get('nzbs', []):
|
||||||
|
|
||||||
# Check status
|
# Check status
|
||||||
status = 'busy'
|
status = 'busy'
|
||||||
if item['state'] == 20:
|
if nzb['state'] == 20:
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
elif item['state'] in [21, 22, 24]:
|
elif nzb['state'] in [21, 22, 24]:
|
||||||
status = 'failed'
|
status = 'failed'
|
||||||
|
|
||||||
statuses.append({
|
release_downloads.append({
|
||||||
'id': item['id'],
|
'id': nzb['id'],
|
||||||
'name': item['uiTitle'],
|
'name': nzb['uiTitle'],
|
||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['state'],
|
'original_status': nzb['state'],
|
||||||
'timeleft':-1,
|
'timeleft':-1,
|
||||||
'folder': ss(item['destinationPath']),
|
'folder': sp(nzb['destinationPath']),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return release_downloads
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, release_download):
|
||||||
|
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.call('nzb/%s/cancel' % item['id'])
|
self.call('nzb/%s/cancel' % release_download['id'])
|
||||||
except:
|
except:
|
||||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'url',
|
'name': 'url',
|
||||||
'default': 'http://localhost:80/RPC2',
|
'default': 'http://localhost:80/RPC2',
|
||||||
|
'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
|
||||||
|
'or <strong>http://localhost:80/RPC2</strong>'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'username',
|
'name': 'username',
|
||||||
@@ -35,6 +37,11 @@ config = [{
|
|||||||
'name': 'label',
|
'name': 'label',
|
||||||
'description': 'Label to apply on added torrents.',
|
'description': 'Label to apply on added torrents.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'directory',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'remove_complete',
|
'name': 'remove_complete',
|
||||||
'label': 'Remove torrent',
|
'label': 'Remove torrent',
|
||||||
@@ -51,6 +58,14 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Also remove the leftover files.',
|
'description': 'Also remove the leftover files.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'append_label',
|
||||||
|
'label': 'Append Label',
|
||||||
|
'default': False,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Append label to download location. Requires you to set the download location above.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'paused',
|
'name': 'paused',
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
from base64 import b16encode, b32decode
|
from base64 import b16encode, b32decode
|
||||||
from bencode import bencode, bdecode
|
from bencode import bencode, bdecode
|
||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import sp
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from rtorrent import RTorrent
|
from rtorrent import RTorrent
|
||||||
from rtorrent.err import MethodError
|
from rtorrent.err import MethodError
|
||||||
import shutil
|
import os
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class rTorrent(Downloader):
|
|||||||
group.set_command()
|
group.set_command()
|
||||||
group.disable()
|
group.disable()
|
||||||
except MethodError, err:
|
except MethodError, err:
|
||||||
log.error('Unable to set group options: %s', err.message)
|
log.error('Unable to set group options: %s', err.msg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -91,6 +91,7 @@ class rTorrent(Downloader):
|
|||||||
if self.conf('label'):
|
if self.conf('label'):
|
||||||
torrent_params['label'] = self.conf('label')
|
torrent_params['label'] = self.conf('label')
|
||||||
|
|
||||||
|
|
||||||
if not filedata and data.get('protocol') == 'torrent':
|
if not filedata and data.get('protocol') == 'torrent':
|
||||||
log.error('Failed sending torrent, no data')
|
log.error('Failed sending torrent, no data')
|
||||||
return False
|
return False
|
||||||
@@ -116,10 +117,19 @@ class rTorrent(Downloader):
|
|||||||
# Send torrent to rTorrent
|
# Send torrent to rTorrent
|
||||||
torrent = self.rt.load_torrent(filedata)
|
torrent = self.rt.load_torrent(filedata)
|
||||||
|
|
||||||
|
if not torrent:
|
||||||
|
log.error('Unable to find the torrent, did it fail to load?')
|
||||||
|
return False
|
||||||
|
|
||||||
# Set label
|
# Set label
|
||||||
if self.conf('label'):
|
if self.conf('label'):
|
||||||
torrent.set_custom(1, self.conf('label'))
|
torrent.set_custom(1, self.conf('label'))
|
||||||
|
|
||||||
|
if self.conf('directory') and self.conf('append_label'):
|
||||||
|
torrent.set_directory(os.path.join(self.conf('directory'), self.conf('label')))
|
||||||
|
elif self.conf('directory'):
|
||||||
|
torrent.set_directory(self.conf('directory'))
|
||||||
|
|
||||||
# Set Ratio Group
|
# Set Ratio Group
|
||||||
torrent.set_visible(group_name)
|
torrent.set_visible(group_name)
|
||||||
|
|
||||||
@@ -141,37 +151,42 @@ class rTorrent(Downloader):
|
|||||||
try:
|
try:
|
||||||
torrents = self.rt.get_torrents()
|
torrents = self.rt.get_torrents()
|
||||||
|
|
||||||
statuses = StatusList(self)
|
release_downloads = ReleaseDownloadList(self)
|
||||||
|
|
||||||
|
for torrent in torrents:
|
||||||
|
torrent_files = []
|
||||||
|
for file_item in torrent.get_files():
|
||||||
|
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
|
||||||
|
|
||||||
for item in torrents:
|
|
||||||
status = 'busy'
|
status = 'busy'
|
||||||
if item.complete:
|
if torrent.complete:
|
||||||
if item.active:
|
if torrent.active:
|
||||||
status = 'seeding'
|
status = 'seeding'
|
||||||
else:
|
else:
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
|
|
||||||
statuses.append({
|
release_downloads.append({
|
||||||
'id': item.info_hash,
|
'id': torrent.info_hash,
|
||||||
'name': item.name,
|
'name': torrent.name,
|
||||||
'status': status,
|
'status': status,
|
||||||
'seed_ratio': item.ratio,
|
'seed_ratio': torrent.ratio,
|
||||||
'original_status': item.state,
|
'original_status': torrent.state,
|
||||||
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) if item.down_rate > 0 else -1,
|
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
||||||
'folder': ss(item.directory)
|
'folder': sp(torrent.directory),
|
||||||
|
'files': '|'.join(torrent_files)
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return release_downloads
|
||||||
|
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to get status from rTorrent: %s', err)
|
log.error('Failed to get status from rTorrent: %s', err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def pause(self, download_info, pause = True):
|
def pause(self, release_download, pause = True):
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
torrent = self.rt.find_torrent(download_info['id'])
|
torrent = self.rt.find_torrent(release_download['id'])
|
||||||
if torrent is None:
|
if torrent is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -179,23 +194,34 @@ class rTorrent(Downloader):
|
|||||||
return torrent.pause()
|
return torrent.pause()
|
||||||
return torrent.resume()
|
return torrent.resume()
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, release_download):
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||||
return self.processComplete(item, delete_files = True)
|
return self.processComplete(release_download, delete_files = True)
|
||||||
|
|
||||||
def processComplete(self, item, delete_files):
|
def processComplete(self, release_download, delete_files):
|
||||||
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
||||||
(item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
|
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
torrent = self.rt.find_torrent(item['id'])
|
torrent = self.rt.find_torrent(release_download['id'])
|
||||||
|
|
||||||
if torrent is None:
|
if torrent is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if delete_files:
|
||||||
|
for file_item in torrent.get_files(): # will only delete files, not dir/sub-dir
|
||||||
|
os.unlink(os.path.join(torrent.directory, file_item.path))
|
||||||
|
|
||||||
|
if torrent.is_multi_file() and torrent.directory.endswith(torrent.name):
|
||||||
|
# Remove empty directories bottom up
|
||||||
|
try:
|
||||||
|
for path, _, _ in os.walk(torrent.directory, topdown = False):
|
||||||
|
os.rmdir(path)
|
||||||
|
except OSError:
|
||||||
|
log.info('Directory "%s" contains extra files, unable to remove', torrent.directory)
|
||||||
|
|
||||||
torrent.erase() # just removes the torrent, doesn't delete data
|
torrent.erase() # just removes the torrent, doesn't delete data
|
||||||
|
|
||||||
if delete_files:
|
|
||||||
shutil.rmtree(item['folder'], True)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp
|
||||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib2 import URLError
|
from urllib2 import URLError
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -86,47 +87,58 @@ class Sabnzbd(Downloader):
|
|||||||
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
statuses = StatusList(self)
|
release_downloads = ReleaseDownloadList(self)
|
||||||
|
|
||||||
# Get busy releases
|
# Get busy releases
|
||||||
for item in queue.get('slots', []):
|
for nzb in queue.get('slots', []):
|
||||||
statuses.append({
|
status = 'busy'
|
||||||
'id': item['nzo_id'],
|
if 'ENCRYPTED / ' in nzb['filename']:
|
||||||
'name': item['filename'],
|
status = 'failed'
|
||||||
'original_status': item['status'],
|
|
||||||
'timeleft': item['timeleft'] if not queue['paused'] else -1,
|
release_downloads.append({
|
||||||
|
'id': nzb['nzo_id'],
|
||||||
|
'name': nzb['filename'],
|
||||||
|
'status': status,
|
||||||
|
'original_status': nzb['status'],
|
||||||
|
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Get old releases
|
# Get old releases
|
||||||
for item in history.get('slots', []):
|
for nzb in history.get('slots', []):
|
||||||
|
|
||||||
status = 'busy'
|
status = 'busy'
|
||||||
if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
|
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
|
||||||
status = 'failed'
|
status = 'failed'
|
||||||
elif item['status'] == 'Completed':
|
elif nzb['status'] == 'Completed':
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
|
|
||||||
statuses.append({
|
release_downloads.append({
|
||||||
'id': item['nzo_id'],
|
'id': nzb['nzo_id'],
|
||||||
'name': item['name'],
|
'name': nzb['name'],
|
||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['status'],
|
'original_status': nzb['status'],
|
||||||
'timeleft': str(timedelta(seconds = 0)),
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
'folder': ss(item['storage']),
|
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return release_downloads
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, release_download):
|
||||||
|
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self.call({
|
||||||
|
'mode': 'queue',
|
||||||
|
'name': 'delete',
|
||||||
|
'del_files': '1',
|
||||||
|
'value': release_download['id']
|
||||||
|
}, use_json = False)
|
||||||
self.call({
|
self.call({
|
||||||
'mode': 'history',
|
'mode': 'history',
|
||||||
'name': 'delete',
|
'name': 'delete',
|
||||||
'del_files': '1',
|
'del_files': '1',
|
||||||
'value': item['id']
|
'value': release_download['id']
|
||||||
}, use_json = False)
|
}, use_json = False)
|
||||||
except:
|
except:
|
||||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||||
@@ -134,15 +146,15 @@ class Sabnzbd(Downloader):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def processComplete(self, item, delete_files = False):
|
def processComplete(self, release_download, delete_files = False):
|
||||||
log.debug('Requesting SabNZBd to remove the NZB %s.', item['name'])
|
log.debug('Requesting SabNZBd to remove the NZB %s.', release_download['name'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.call({
|
self.call({
|
||||||
'mode': 'history',
|
'mode': 'history',
|
||||||
'name': 'delete',
|
'name': 'delete',
|
||||||
'del_files': '0',
|
'del_files': '0',
|
||||||
'value': item['id']
|
'value': release_download['id']
|
||||||
}, use_json = False)
|
}, use_json = False)
|
||||||
except:
|
except:
|
||||||
log.error('Failed removing: %s', traceback.format_exc(0))
|
log.error('Failed removing: %s', traceback.format_exc(0))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import isInt
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -34,12 +35,12 @@ class Synology(Downloader):
|
|||||||
elif data['protocol'] in ['nzb', 'torrent']:
|
elif data['protocol'] in ['nzb', 'torrent']:
|
||||||
log.info('Adding %s' % data['protocol'])
|
log.info('Adding %s' % data['protocol'])
|
||||||
if not filedata:
|
if not filedata:
|
||||||
log.error('No %s data found' % data['protocol'])
|
log.error('No %s data found', data['protocol'])
|
||||||
else:
|
else:
|
||||||
filename = data['name'] + '.' + data['protocol']
|
filename = data['name'] + '.' + data['protocol']
|
||||||
response = srpc.create_task(filename = filename, filedata = filedata)
|
response = srpc.create_task(filename = filename, filedata = filedata)
|
||||||
except Exception, err:
|
except:
|
||||||
log.error('Exception while adding torrent: %s', err)
|
log.error('Exception while adding torrent: %s', traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||||
from couchpotato.core.helpers.encoding import isInt, ss
|
from couchpotato.core.helpers.encoding import isInt, sp
|
||||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
@@ -89,10 +88,10 @@ class Transmission(Downloader):
|
|||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
statuses = StatusList(self)
|
release_downloads = ReleaseDownloadList(self)
|
||||||
|
|
||||||
return_params = {
|
return_params = {
|
||||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit']
|
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files']
|
||||||
}
|
}
|
||||||
|
|
||||||
queue = self.trpc.get_alltorrents(return_params)
|
queue = self.trpc.get_alltorrents(return_params)
|
||||||
@@ -100,47 +99,48 @@ class Transmission(Downloader):
|
|||||||
log.debug('Nothing in queue or error')
|
log.debug('Nothing in queue or error')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for item in queue['torrents']:
|
for torrent in queue['torrents']:
|
||||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
||||||
(item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], item['isFinished']))
|
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
|
||||||
|
|
||||||
if not os.path.isdir(Env.setting('from', 'renamer')):
|
torrent_files = []
|
||||||
log.error('Renamer "from" folder doesn\'t to exist.')
|
for file_item in torrent['files']:
|
||||||
return
|
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
|
||||||
|
|
||||||
status = 'busy'
|
status = 'busy'
|
||||||
if item['isStalled'] and self.conf('stalled_as_failed'):
|
if torrent.get('isStalled') and self.conf('stalled_as_failed'):
|
||||||
status = 'failed'
|
status = 'failed'
|
||||||
elif item['status'] == 0 and item['percentDone'] == 1:
|
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
elif item['status'] in [5, 6]:
|
elif torrent['status'] in [5, 6]:
|
||||||
status = 'seeding'
|
status = 'seeding'
|
||||||
|
|
||||||
statuses.append({
|
release_downloads.append({
|
||||||
'id': item['hashString'],
|
'id': torrent['hashString'],
|
||||||
'name': item['name'],
|
'name': torrent['name'],
|
||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['status'],
|
'original_status': torrent['status'],
|
||||||
'seed_ratio': item['uploadRatio'],
|
'seed_ratio': torrent['uploadRatio'],
|
||||||
'timeleft': str(timedelta(seconds = item['eta'])),
|
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||||
'folder': ss(os.path.join(item['downloadDir'], item['name'])),
|
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
|
||||||
|
'files': '|'.join(torrent_files)
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return release_downloads
|
||||||
|
|
||||||
def pause(self, item, pause = True):
|
def pause(self, release_download, pause = True):
|
||||||
if pause:
|
if pause:
|
||||||
return self.trpc.stop_torrent(item['id'])
|
return self.trpc.stop_torrent(release_download['id'])
|
||||||
else:
|
else:
|
||||||
return self.trpc.start_torrent(item['id'])
|
return self.trpc.start_torrent(release_download['id'])
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, release_download):
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||||
return self.trpc.remove_torrent(item['hashString'], True)
|
return self.trpc.remove_torrent(release_download['id'], True)
|
||||||
|
|
||||||
def processComplete(self, item, delete_files = False):
|
def processComplete(self, release_download, delete_files = False):
|
||||||
log.debug('Requesting Transmission to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
log.debug('Requesting Transmission to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
return self.trpc.remove_torrent(item['hashString'], delete_files)
|
return self.trpc.remove_torrent(release_download['id'], delete_files)
|
||||||
|
|
||||||
class TransmissionRPC(object):
|
class TransmissionRPC(object):
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from base64 import b16encode, b32decode
|
from base64 import b16encode, b32decode
|
||||||
from bencode import bencode as benc, bdecode
|
from bencode import bencode as benc, bdecode
|
||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||||
from couchpotato.core.helpers.encoding import isInt, ss
|
from couchpotato.core.helpers.encoding import isInt, ss, sp
|
||||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -102,39 +102,6 @@ class uTorrent(Downloader):
|
|||||||
if self.conf('paused', default = 0):
|
if self.conf('paused', default = 0):
|
||||||
self.utorrent_api.pause_torrent(torrent_hash)
|
self.utorrent_api.pause_torrent(torrent_hash)
|
||||||
|
|
||||||
count = 0
|
|
||||||
while True:
|
|
||||||
|
|
||||||
count += 1
|
|
||||||
# Check if torrent is saved in subfolder of torrent name
|
|
||||||
data = self.utorrent_api.get_files(torrent_hash)
|
|
||||||
|
|
||||||
torrent_files = json.loads(data)
|
|
||||||
if torrent_files.get('error'):
|
|
||||||
log.error('Error getting data from uTorrent: %s', torrent_files.get('error'))
|
|
||||||
return False
|
|
||||||
|
|
||||||
if (torrent_files.get('files') and len(torrent_files['files'][1]) > 0) or count > 60:
|
|
||||||
break
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Torrent has only one file, so uTorrent wont create a folder for it
|
|
||||||
if len(torrent_files['files'][1]) == 1:
|
|
||||||
# Remove torrent and try again
|
|
||||||
self.utorrent_api.remove_torrent(torrent_hash, remove_data = True)
|
|
||||||
|
|
||||||
# Send request to uTorrent
|
|
||||||
if data.get('protocol') == 'torrent_magnet':
|
|
||||||
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'), add_folder = True)
|
|
||||||
else:
|
|
||||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata, add_folder = True)
|
|
||||||
|
|
||||||
# Change settings of added torrent
|
|
||||||
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
|
||||||
if self.conf('paused', default = 0):
|
|
||||||
self.utorrent_api.pause_torrent(torrent_hash)
|
|
||||||
|
|
||||||
return self.downloadReturnId(torrent_hash)
|
return self.downloadReturnId(torrent_hash)
|
||||||
|
|
||||||
def getAllDownloadStatus(self):
|
def getAllDownloadStatus(self):
|
||||||
@@ -144,7 +111,7 @@ class uTorrent(Downloader):
|
|||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
statuses = StatusList(self)
|
release_downloads = ReleaseDownloadList(self)
|
||||||
|
|
||||||
data = self.utorrent_api.get_status()
|
data = self.utorrent_api.get_status()
|
||||||
if not data:
|
if not data:
|
||||||
@@ -161,52 +128,74 @@ class uTorrent(Downloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get torrents
|
# Get torrents
|
||||||
for item in queue['torrents']:
|
for torrent in queue['torrents']:
|
||||||
|
|
||||||
|
#Get files of the torrent
|
||||||
|
torrent_files = []
|
||||||
|
try:
|
||||||
|
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
|
||||||
|
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
|
||||||
|
except:
|
||||||
|
log.debug('Failed getting files from torrent: %s', torrent[2])
|
||||||
|
|
||||||
|
status_flags = {
|
||||||
|
"STARTED" : 1,
|
||||||
|
"CHECKING" : 2,
|
||||||
|
"CHECK-START" : 4,
|
||||||
|
"CHECKED" : 8,
|
||||||
|
"ERROR" : 16,
|
||||||
|
"PAUSED" : 32,
|
||||||
|
"QUEUED" : 64,
|
||||||
|
"LOADED" : 128
|
||||||
|
}
|
||||||
|
|
||||||
# item[21] = Paused | Downloading | Seeding | Finished
|
|
||||||
status = 'busy'
|
status = 'busy'
|
||||||
if 'Finished' in item[21]:
|
if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000:
|
||||||
status = 'completed'
|
|
||||||
self.removeReadOnly(item[26])
|
|
||||||
elif 'Seeding' in item[21]:
|
|
||||||
status = 'seeding'
|
status = 'seeding'
|
||||||
self.removeReadOnly(item[26])
|
elif (torrent[1] & status_flags["ERROR"]):
|
||||||
|
status = 'failed'
|
||||||
|
elif torrent[4] == 1000:
|
||||||
|
status = 'completed'
|
||||||
|
|
||||||
statuses.append({
|
if not status == 'busy':
|
||||||
'id': item[0],
|
self.removeReadOnly(torrent_files)
|
||||||
'name': item[2],
|
|
||||||
'status': status,
|
release_downloads.append({
|
||||||
'seed_ratio': float(item[7]) / 1000,
|
'id': torrent[0],
|
||||||
'original_status': item[1],
|
'name': torrent[2],
|
||||||
'timeleft': str(timedelta(seconds = item[10])),
|
'status': status,
|
||||||
'folder': ss(item[26]),
|
'seed_ratio': float(torrent[7]) / 1000,
|
||||||
|
'original_status': torrent[1],
|
||||||
|
'timeleft': str(timedelta(seconds = torrent[10])),
|
||||||
|
'folder': sp(torrent[26]),
|
||||||
|
'files': '|'.join(torrent_files)
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return release_downloads
|
||||||
|
|
||||||
def pause(self, item, pause = True):
|
def pause(self, release_download, pause = True):
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
return self.utorrent_api.pause_torrent(item['id'], pause)
|
return self.utorrent_api.pause_torrent(release_download['id'], pause)
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, release_download):
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
return self.utorrent_api.remove_torrent(item['id'], remove_data = True)
|
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = True)
|
||||||
|
|
||||||
def processComplete(self, item, delete_files = False):
|
def processComplete(self, release_download, delete_files = False):
|
||||||
log.debug('Requesting uTorrent to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
log.debug('Requesting uTorrent to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files)
|
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = delete_files)
|
||||||
|
|
||||||
def removeReadOnly(self, folder):
|
def removeReadOnly(self, files):
|
||||||
#Removes all read-only flags in a folder
|
#Removes all read-on ly flags in a for all files
|
||||||
if folder and os.path.isdir(folder):
|
for filepath in files:
|
||||||
for root, folders, filenames in os.walk(folder):
|
if os.path.isfile(filepath):
|
||||||
for filename in filenames:
|
#Windows only needs S_IWRITE, but we bitwise-or with current perms to preserve other permission bits on Linux
|
||||||
os.chmod(os.path.join(root, filename), stat.S_IWRITE)
|
os.chmod(filepath, stat.S_IWRITE | os.stat(filepath).st_mode)
|
||||||
|
|
||||||
class uTorrentAPI(object):
|
class uTorrentAPI(object):
|
||||||
|
|
||||||
@@ -304,13 +293,13 @@ class uTorrentAPI(object):
|
|||||||
utorrent_settings = json.loads(self._request(action))
|
utorrent_settings = json.loads(self._request(action))
|
||||||
|
|
||||||
# Create settings dict
|
# Create settings dict
|
||||||
for item in utorrent_settings['settings']:
|
for setting in utorrent_settings['settings']:
|
||||||
if item[1] == 0: # int
|
if setting[1] == 0: # int
|
||||||
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
|
settings_dict[setting[0]] = int(setting[2] if not setting[2].strip() == '' else '0')
|
||||||
elif item[1] == 1: # bool
|
elif setting[1] == 1: # bool
|
||||||
settings_dict[item[0]] = True if item[2] == 'true' else False
|
settings_dict[setting[0]] = True if setting[2] == 'true' else False
|
||||||
elif item[1] == 2: # string
|
elif setting[1] == 2: # string
|
||||||
settings_dict[item[0]] = item[2]
|
settings_dict[setting[0]] = setting[2]
|
||||||
|
|
||||||
#log.debug('uTorrent settings: %s', settings_dict)
|
#log.debug('uTorrent settings: %s', settings_dict)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@@ -47,6 +48,10 @@ def ss(original, *args):
|
|||||||
log.debug('Failed ss encoding char, force UTF8: %s', e)
|
log.debug('Failed ss encoding char, force UTF8: %s', e)
|
||||||
return u_original.encode('UTF-8')
|
return u_original.encode('UTF-8')
|
||||||
|
|
||||||
|
def sp(path, *args):
|
||||||
|
# Standardise encoding, normalise case, path and strip trailing '/' or '\'
|
||||||
|
return os.path.normcase(os.path.normpath(ss(path, *args))).rstrip(os.path.sep)
|
||||||
|
|
||||||
def ek(original, *args):
|
def ek(original, *args):
|
||||||
if isinstance(original, (str, unicode)):
|
if isinstance(original, (str, unicode)):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
|
import collections
|
||||||
import hashlib
|
import hashlib
|
||||||
import os.path
|
import os.path
|
||||||
import platform
|
import platform
|
||||||
@@ -136,18 +137,20 @@ def getImdb(txt, check_inside = False, multiple = False):
|
|||||||
output.close()
|
output.close()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ids = re.findall('(tt\d{7})', txt)
|
ids = re.findall('(tt\d{4,7})', txt)
|
||||||
|
|
||||||
if multiple:
|
if multiple:
|
||||||
return list(set(ids)) if len(ids) > 0 else []
|
return list(set(['tt%07d' % tryInt(x[2:]) for x in ids])) if len(ids) > 0 else []
|
||||||
return ids[0]
|
|
||||||
|
return 'tt%07d' % tryInt(ids[0][2:])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def tryInt(s):
|
def tryInt(s, default = 0):
|
||||||
try: return int(s)
|
try: return int(s)
|
||||||
except: return 0
|
except: return default
|
||||||
|
|
||||||
def tryFloat(s):
|
def tryFloat(s):
|
||||||
try:
|
try:
|
||||||
@@ -163,6 +166,11 @@ def natsortKey(s):
|
|||||||
def natcmp(a, b):
|
def natcmp(a, b):
|
||||||
return cmp(natsortKey(a), natsortKey(b))
|
return cmp(natsortKey(a), natsortKey(b))
|
||||||
|
|
||||||
|
def toIterable(value):
|
||||||
|
if isinstance(value, collections.Iterable):
|
||||||
|
return value
|
||||||
|
return [value]
|
||||||
|
|
||||||
def getTitle(library_dict):
|
def getTitle(library_dict):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
@@ -205,3 +213,6 @@ def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
|||||||
def splitString(str, split_on = ',', clean = True):
|
def splitString(str, split_on = ',', clean = True):
|
||||||
list = [x.strip() for x in str.split(split_on)] if str else []
|
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||||
return filter(None, list) if clean else list
|
return filter(None, list) if clean else list
|
||||||
|
|
||||||
|
def dictIsSubset(a, b):
|
||||||
|
return all([k in b and b[k] == v for k, v in a.items()])
|
||||||
|
|||||||
+30
-29
@@ -1,7 +1,8 @@
|
|||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import glob
|
from importlib import import_module
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -12,17 +13,6 @@ class Loader(object):
|
|||||||
providers = {}
|
providers = {}
|
||||||
modules = {}
|
modules = {}
|
||||||
|
|
||||||
def addPath(self, root, base_path, priority, recursive = False):
|
|
||||||
for filename in os.listdir(os.path.join(root, *base_path)):
|
|
||||||
path = os.path.join(os.path.join(root, *base_path), filename)
|
|
||||||
if os.path.isdir(path) and filename[:2] != '__':
|
|
||||||
if u'__init__.py' in os.listdir(path):
|
|
||||||
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
|
||||||
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
|
||||||
|
|
||||||
if recursive:
|
|
||||||
self.addPath(root, base_path + [filename], priority, recursive = True)
|
|
||||||
|
|
||||||
def preload(self, root = ''):
|
def preload(self, root = ''):
|
||||||
core = os.path.join(root, 'couchpotato', 'core')
|
core = os.path.join(root, 'couchpotato', 'core')
|
||||||
|
|
||||||
@@ -39,6 +29,14 @@ class Loader(object):
|
|||||||
# Add media to loader
|
# Add media to loader
|
||||||
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
||||||
|
|
||||||
|
# Add custom plugin folder
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
custom_plugin_dir = os.path.join(Env.get('data_dir'), 'custom_plugins')
|
||||||
|
if os.path.isdir(custom_plugin_dir):
|
||||||
|
sys.path.insert(0, custom_plugin_dir)
|
||||||
|
self.paths['custom_plugins'] = (30, '', custom_plugin_dir)
|
||||||
|
|
||||||
|
# Loop over all paths and add to module list
|
||||||
for plugin_type, plugin_tuple in self.paths.iteritems():
|
for plugin_type, plugin_tuple in self.paths.iteritems():
|
||||||
priority, module, dir_name = plugin_tuple
|
priority, module, dir_name = plugin_tuple
|
||||||
self.addFromDir(plugin_type, priority, module, dir_name)
|
self.addFromDir(plugin_type, priority, module, dir_name)
|
||||||
@@ -46,8 +44,9 @@ class Loader(object):
|
|||||||
def run(self):
|
def run(self):
|
||||||
did_save = 0
|
did_save = 0
|
||||||
|
|
||||||
for priority in self.modules:
|
for priority in sorted(self.modules):
|
||||||
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
||||||
|
|
||||||
# Load module
|
# Load module
|
||||||
try:
|
try:
|
||||||
if plugin.get('name')[:2] == '__':
|
if plugin.get('name')[:2] == '__':
|
||||||
@@ -56,7 +55,6 @@ class Loader(object):
|
|||||||
m = self.loadModule(module_name)
|
m = self.loadModule(module_name)
|
||||||
if m is None:
|
if m is None:
|
||||||
continue
|
continue
|
||||||
m = getattr(m, plugin.get('name'))
|
|
||||||
|
|
||||||
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
||||||
|
|
||||||
@@ -78,20 +76,26 @@ class Loader(object):
|
|||||||
if did_save:
|
if did_save:
|
||||||
fireEvent('settings.save')
|
fireEvent('settings.save')
|
||||||
|
|
||||||
|
def addPath(self, root, base_path, priority, recursive = False):
|
||||||
|
root_path = os.path.join(root, *base_path)
|
||||||
|
for filename in os.listdir(root_path):
|
||||||
|
path = os.path.join(root_path, filename)
|
||||||
|
if os.path.isdir(path) and filename[:2] != '__':
|
||||||
|
if u'__init__.py' in os.listdir(path):
|
||||||
|
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
||||||
|
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
||||||
|
|
||||||
|
if recursive:
|
||||||
|
self.addPath(root, base_path + [filename], priority, recursive = True)
|
||||||
|
|
||||||
def addFromDir(self, plugin_type, priority, module, dir_name):
|
def addFromDir(self, plugin_type, priority, module, dir_name):
|
||||||
|
|
||||||
# Load dir module
|
# Load dir module
|
||||||
try:
|
if module and len(module) > 0:
|
||||||
m = __import__(module)
|
self.addModule(priority, plugin_type, module, os.path.basename(dir_name))
|
||||||
splitted = module.split('.')
|
|
||||||
for sub in splitted[1:]:
|
|
||||||
m = getattr(m, sub)
|
|
||||||
except:
|
|
||||||
raise
|
|
||||||
|
|
||||||
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
for name in os.listdir(dir_name):
|
||||||
name = os.path.basename(cur_file)
|
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(dir_name, name, '__init__.py')):
|
||||||
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(cur_file, '__init__.py')):
|
|
||||||
module_name = '%s.%s' % (module, name)
|
module_name = '%s.%s' % (module, name)
|
||||||
self.addModule(priority, plugin_type, module_name, name)
|
self.addModule(priority, plugin_type, module_name, name)
|
||||||
|
|
||||||
@@ -131,6 +135,7 @@ class Loader(object):
|
|||||||
if not self.modules.get(priority):
|
if not self.modules.get(priority):
|
||||||
self.modules[priority] = {}
|
self.modules[priority] = {}
|
||||||
|
|
||||||
|
module = module.lstrip('.')
|
||||||
self.modules[priority][module] = {
|
self.modules[priority][module] = {
|
||||||
'priority': priority,
|
'priority': priority,
|
||||||
'module': module,
|
'module': module,
|
||||||
@@ -140,11 +145,7 @@ class Loader(object):
|
|||||||
|
|
||||||
def loadModule(self, name):
|
def loadModule(self, name):
|
||||||
try:
|
try:
|
||||||
m = __import__(name)
|
return import_module(name)
|
||||||
splitted = name.split('.')
|
|
||||||
for sub in splitted[1:-1]:
|
|
||||||
m = getattr(m, sub)
|
|
||||||
return m
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,13 +1,44 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato import get_session
|
||||||
|
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.core.settings.model import Media
|
||||||
|
|
||||||
|
|
||||||
class MediaBase(Plugin):
|
class MediaBase(Plugin):
|
||||||
|
|
||||||
_type = None
|
_type = None
|
||||||
|
|
||||||
|
default_dict = {
|
||||||
|
'profile': {'types': {'quality': {}}},
|
||||||
|
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||||
|
'library': {'titles': {}, 'files':{}},
|
||||||
|
'files': {},
|
||||||
|
'status': {},
|
||||||
|
'category': {},
|
||||||
|
}
|
||||||
|
|
||||||
def initType(self):
|
def initType(self):
|
||||||
addEvent('media.types', self.getType)
|
addEvent('media.types', self.getType)
|
||||||
|
|
||||||
def getType(self):
|
def getType(self):
|
||||||
return self._type
|
return self._type
|
||||||
|
|
||||||
|
def createOnComplete(self, id):
|
||||||
|
|
||||||
|
def onComplete():
|
||||||
|
db = get_session()
|
||||||
|
media = db.query(Media).filter_by(id = id).first()
|
||||||
|
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.default_dict), on_complete = self.createNotifyFront(id))
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
|
return onComplete
|
||||||
|
|
||||||
|
def createNotifyFront(self, media_id):
|
||||||
|
|
||||||
|
def notifyFront():
|
||||||
|
db = get_session()
|
||||||
|
media = db.query(Media).filter_by(id = media_id).first()
|
||||||
|
fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict))
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
|
return notifyFront
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import MediaPlugin
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return MediaPlugin()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from couchpotato import get_session
|
||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||||
|
from couchpotato.core.helpers.variable import splitString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media import MediaBase
|
||||||
|
from couchpotato.core.settings.model import Media
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MediaPlugin(MediaBase):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
addApiView('media.refresh', self.refresh, docs = {
|
||||||
|
'desc': 'Refresh a any media type by ID',
|
||||||
|
'params': {
|
||||||
|
'id': {'desc': 'Movie, Show, Season or Episode ID(s) you want to refresh.', 'type': 'int (comma separated)'},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addEvent('app.load', self.addSingleRefresh)
|
||||||
|
|
||||||
|
def refresh(self, id = '', **kwargs):
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
for x in splitString(id):
|
||||||
|
media = db.query(Media).filter_by(id = x).first()
|
||||||
|
|
||||||
|
if media:
|
||||||
|
# Get current selected title
|
||||||
|
default_title = ''
|
||||||
|
for title in media.library.titles:
|
||||||
|
if title.default: default_title = title.title
|
||||||
|
|
||||||
|
fireEvent('notify.frontend', type = '%s.busy.%s' % (media.type, x), data = True)
|
||||||
|
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def addSingleRefresh(self):
|
||||||
|
|
||||||
|
for media_type in fireEvent('media.types', merge = True):
|
||||||
|
addApiView('%s.refresh' % media_type, self.refresh)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import Search
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Search()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
|
from couchpotato.core.helpers.variable import mergeDicts
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Search(Plugin):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
addApiView('search', self.search, docs = {
|
||||||
|
'desc': 'Search the info in providers for a movie',
|
||||||
|
'params': {
|
||||||
|
'q': {'desc': 'The (partial) movie name you want to search for'},
|
||||||
|
'type': {'desc': 'Search for a specific media type. Leave empty to search all.'},
|
||||||
|
},
|
||||||
|
'return': {'type': 'object', 'example': """{
|
||||||
|
'success': True,
|
||||||
|
'movies': array,
|
||||||
|
'show': array,
|
||||||
|
etc
|
||||||
|
}"""}
|
||||||
|
})
|
||||||
|
|
||||||
|
addEvent('app.load', self.addSingleSearches)
|
||||||
|
|
||||||
|
def search(self, q = '', types = None, **kwargs):
|
||||||
|
|
||||||
|
# Make sure types is the correct instance
|
||||||
|
if isinstance(types, (str, unicode)):
|
||||||
|
types = [types]
|
||||||
|
elif isinstance(types, (list, tuple, set)):
|
||||||
|
types = list(types)
|
||||||
|
|
||||||
|
if not types:
|
||||||
|
result = fireEvent('info.search', q = q, merge = True)
|
||||||
|
else:
|
||||||
|
result = {}
|
||||||
|
for media_type in types:
|
||||||
|
result[media_type] = fireEvent('%s.search' % media_type)
|
||||||
|
|
||||||
|
return mergeDicts({
|
||||||
|
'success': True,
|
||||||
|
}, result)
|
||||||
|
|
||||||
|
def createSingleSearch(self, media_type):
|
||||||
|
|
||||||
|
def singleSearch(q, **kwargs):
|
||||||
|
return self.search(q, type = media_type, **kwargs)
|
||||||
|
|
||||||
|
return singleSearch
|
||||||
|
|
||||||
|
def addSingleSearches(self):
|
||||||
|
|
||||||
|
for media_type in fireEvent('media.types', merge = True):
|
||||||
|
addApiView('%s.search' % media_type, self.createSingleSearch(media_type))
|
||||||
+24
-24
@@ -129,13 +129,13 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result {
|
.media_result {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options {
|
.media_result .options {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -147,48 +147,48 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
||||||
}
|
}
|
||||||
.movie_result .options > .in_library_wanted {
|
.media_result .options > .in_library_wanted {
|
||||||
margin-top: -7px;
|
margin-top: -7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options > div {
|
.media_result .options > div {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options .thumbnail {
|
.media_result .options .thumbnail {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options select {
|
.media_result .options select {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.movie_result .options select[name=title] { width: 170px; }
|
.media_result .options select[name=title] { width: 170px; }
|
||||||
.movie_result .options select[name=profile] { width: 90px; }
|
.media_result .options select[name=profile] { width: 90px; }
|
||||||
.movie_result .options select[name=category] { width: 80px; }
|
.media_result .options select[name=category] { width: 80px; }
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
@media all and (max-width: 480px) {
|
||||||
|
|
||||||
.movie_result .options select[name=title] { width: 90px; }
|
.media_result .options select[name=title] { width: 90px; }
|
||||||
.movie_result .options select[name=profile] { width: 50px; }
|
.media_result .options select[name=profile] { width: 50px; }
|
||||||
.movie_result .options select[name=category] { width: 50px; }
|
.media_result .options select[name=category] { width: 50px; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options .button {
|
.media_result .options .button {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options .message {
|
.media_result .options .message {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .data {
|
.media_result .data {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -199,20 +199,20 @@
|
|||||||
border-top: 1px solid rgba(255,255,255, 0.08);
|
border-top: 1px solid rgba(255,255,255, 0.08);
|
||||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||||
}
|
}
|
||||||
.movie_result .data.open {
|
.media_result .data.open {
|
||||||
left: 100% !important;
|
left: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result:last-child .data { border-bottom: 0; }
|
.media_result:last-child .data { border-bottom: 0; }
|
||||||
|
|
||||||
.movie_result .in_wanted, .movie_result .in_library {
|
.media_result .in_wanted, .media_result .in_library {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
left: 14px;
|
left: 14px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .thumbnail {
|
.media_result .thumbnail {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .info {
|
.media_result .info {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20%;
|
top: 20%;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .info h2 {
|
.media_result .info h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .info h2 .title {
|
.media_result .info h2 .title {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
width: 88%;
|
width: 88%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .info h2 .year {
|
.media_result .info h2 .year {
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search_form .mask,
|
.search_form .mask,
|
||||||
.movie_result .mask {
|
.media_result .mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
Block.Search = new Class({
|
||||||
|
|
||||||
|
Extends: BlockBase,
|
||||||
|
|
||||||
|
cache: {},
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var focus_timer = 0;
|
||||||
|
self.el = new Element('div.search_form').adopt(
|
||||||
|
new Element('div.input').adopt(
|
||||||
|
self.input = new Element('input', {
|
||||||
|
'placeholder': 'Search & add a new media',
|
||||||
|
'events': {
|
||||||
|
'keyup': self.keyup.bind(self),
|
||||||
|
'focus': function(){
|
||||||
|
if(focus_timer) clearTimeout(focus_timer);
|
||||||
|
self.el.addClass('focused')
|
||||||
|
if(this.get('value'))
|
||||||
|
self.hideResults(false)
|
||||||
|
},
|
||||||
|
'blur': function(){
|
||||||
|
focus_timer = (function(){
|
||||||
|
self.el.removeClass('focused')
|
||||||
|
}).delay(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('a.icon2', {
|
||||||
|
'events': {
|
||||||
|
'click': self.clear.bind(self),
|
||||||
|
'touchend': self.clear.bind(self)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
self.result_container = new Element('div.results_container', {
|
||||||
|
'tween': {
|
||||||
|
'duration': 200
|
||||||
|
},
|
||||||
|
'events': {
|
||||||
|
'mousewheel': function(e){
|
||||||
|
(e).stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).adopt(
|
||||||
|
self.results = new Element('div.results')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function(e){
|
||||||
|
var self = this;
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
if(self.last_q === ''){
|
||||||
|
self.input.blur()
|
||||||
|
self.last_q = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
self.last_q = '';
|
||||||
|
self.input.set('value', '');
|
||||||
|
self.input.focus()
|
||||||
|
|
||||||
|
self.media = {}
|
||||||
|
self.results.empty()
|
||||||
|
self.el.removeClass('filled')
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hideResults: function(bool){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(self.hidden == bool) return;
|
||||||
|
|
||||||
|
self.el[bool ? 'removeClass' : 'addClass']('shown');
|
||||||
|
|
||||||
|
if(bool){
|
||||||
|
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
||||||
|
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
History.addEvent('change', self.hideResults.bind(self, !bool));
|
||||||
|
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hidden = bool;
|
||||||
|
},
|
||||||
|
|
||||||
|
keyup: function(e){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||||
|
|
||||||
|
if(self.q() != self.last_q){
|
||||||
|
if(self.api_request && self.api_request.isRunning())
|
||||||
|
self.api_request.cancel();
|
||||||
|
|
||||||
|
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||||
|
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(!self.q()){
|
||||||
|
self.hideResults(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.list()
|
||||||
|
},
|
||||||
|
|
||||||
|
list: function(){
|
||||||
|
var self = this,
|
||||||
|
q = self.q(),
|
||||||
|
cache = self.cache[q];
|
||||||
|
|
||||||
|
self.hideResults(false);
|
||||||
|
|
||||||
|
if(!cache){
|
||||||
|
self.mask.fade('in');
|
||||||
|
|
||||||
|
if(!self.spinner)
|
||||||
|
self.spinner = createSpinner(self.mask);
|
||||||
|
|
||||||
|
self.api_request = Api.request('search', {
|
||||||
|
'data': {
|
||||||
|
'q': q
|
||||||
|
},
|
||||||
|
'onComplete': self.fill.bind(self, q)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else
|
||||||
|
self.fill(q, cache)
|
||||||
|
|
||||||
|
self.last_q = q;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
fill: function(q, json){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.cache[q] = json
|
||||||
|
|
||||||
|
self.media = {}
|
||||||
|
self.results.empty()
|
||||||
|
|
||||||
|
Object.each(json, function(media, type){
|
||||||
|
if(typeOf(media) == 'array'){
|
||||||
|
Object.each(media, function(m){
|
||||||
|
|
||||||
|
var m = new Block.Search[m.type.capitalize() + 'Item'](m);
|
||||||
|
$(m).inject(self.results)
|
||||||
|
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
||||||
|
|
||||||
|
if(q == m.imdb)
|
||||||
|
m.showOptions()
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate result heights
|
||||||
|
var w = window.getSize(),
|
||||||
|
rc = self.result_container.getCoordinates();
|
||||||
|
|
||||||
|
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
||||||
|
self.mask.fade('out')
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
loading: function(bool){
|
||||||
|
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
||||||
|
},
|
||||||
|
|
||||||
|
q: function(){
|
||||||
|
return this.input.get('value').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -47,7 +47,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'ignored_words',
|
'name': 'ignored_words',
|
||||||
'label': 'Ignored',
|
'label': 'Ignored',
|
||||||
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
|
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs, vain',
|
||||||
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
|
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
from couchpotato import get_session
|
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
from couchpotato.core.helpers.encoding import simplifyString
|
||||||
from couchpotato.core.helpers.variable import md5, getTitle
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from inspect import ismethod, isfunction
|
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -23,7 +17,8 @@ class Searcher(SearcherBase):
|
|||||||
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
|
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
|
||||||
addEvent('searcher.correct_year', self.correctYear)
|
addEvent('searcher.correct_year', self.correctYear)
|
||||||
addEvent('searcher.correct_name', self.correctName)
|
addEvent('searcher.correct_name', self.correctName)
|
||||||
addEvent('searcher.download', self.download)
|
addEvent('searcher.correct_words', self.correctWords)
|
||||||
|
addEvent('searcher.search', self.search)
|
||||||
|
|
||||||
addApiView('searcher.full_search', self.searchAllView, docs = {
|
addApiView('searcher.full_search', self.searchAllView, docs = {
|
||||||
'desc': 'Starts a full search for all media',
|
'desc': 'Starts a full search for all media',
|
||||||
@@ -49,86 +44,21 @@ class Searcher(SearcherBase):
|
|||||||
progress = fireEvent('searcher.progress', merge = True)
|
progress = fireEvent('searcher.progress', merge = True)
|
||||||
return progress
|
return progress
|
||||||
|
|
||||||
def download(self, data, movie, manual = False):
|
def search(self, protocols, media, quality):
|
||||||
|
results = []
|
||||||
|
|
||||||
if not data.get('protocol'):
|
for search_protocol in protocols:
|
||||||
data['protocol'] = data['type']
|
protocol_results = fireEvent('provider.search.%s.%s' % (search_protocol, media['type']), media, quality, merge = True)
|
||||||
data['type'] = 'movie'
|
if protocol_results:
|
||||||
|
results += protocol_results
|
||||||
|
|
||||||
# Test to see if any downloaders are enabled for this type
|
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
||||||
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
|
|
||||||
|
|
||||||
if downloader_enabled:
|
download_preference = self.conf('preferred_method', section = 'searcher')
|
||||||
|
if download_preference != 'both':
|
||||||
|
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
||||||
|
|
||||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
return sorted_results
|
||||||
|
|
||||||
# Download movie to temp
|
|
||||||
filedata = None
|
|
||||||
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
|
||||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
|
||||||
if filedata == 'try_next':
|
|
||||||
return filedata
|
|
||||||
|
|
||||||
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
|
||||||
log.debug('Downloader result: %s', download_result)
|
|
||||||
|
|
||||||
if download_result:
|
|
||||||
try:
|
|
||||||
# Mark release as snatched
|
|
||||||
db = get_session()
|
|
||||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
|
||||||
if rls:
|
|
||||||
renamer_enabled = Env.setting('enabled', 'renamer')
|
|
||||||
|
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
|
||||||
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
|
|
||||||
|
|
||||||
# Save download-id info if returned
|
|
||||||
if isinstance(download_result, dict):
|
|
||||||
for key in download_result:
|
|
||||||
rls_info = ReleaseInfo(
|
|
||||||
identifier = 'download_%s' % key,
|
|
||||||
value = toUnicode(download_result.get(key))
|
|
||||||
)
|
|
||||||
rls.info.append(rls_info)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
|
||||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
|
||||||
log.info(snatch_message)
|
|
||||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
|
||||||
|
|
||||||
# If renamer isn't used, mark movie done
|
|
||||||
if not renamer_enabled:
|
|
||||||
active_status = fireEvent('status.get', 'active', single = True)
|
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
|
||||||
try:
|
|
||||||
if movie['status_id'] == active_status.get('id'):
|
|
||||||
for profile_type in movie['profile']['types']:
|
|
||||||
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
|
||||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
|
||||||
|
|
||||||
# Mark release done
|
|
||||||
rls.status_id = done_status.get('id')
|
|
||||||
rls.last_edit = int(time.time())
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Mark movie done
|
|
||||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
|
||||||
mvie.status_id = done_status.get('id')
|
|
||||||
mvie.last_edit = int(time.time())
|
|
||||||
db.commit()
|
|
||||||
except:
|
|
||||||
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('protocol')))
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def getSearchProtocols(self):
|
def getSearchProtocols(self):
|
||||||
|
|
||||||
@@ -217,7 +147,7 @@ class Searcher(SearcherBase):
|
|||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
# Match longest name between []
|
# Match longest name between []
|
||||||
try: check_names.append(max(check_name.split('['), key = len))
|
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip())
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
for check_name in list(set(check_names)):
|
for check_name in list(set(check_names)):
|
||||||
@@ -234,5 +164,49 @@ class Searcher(SearcherBase):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def correctWords(self, rel_name, media):
|
||||||
|
media_title = fireEvent('searcher.get_search_title', media, single = True)
|
||||||
|
media_words = re.split('\W+', simplifyString(media_title))
|
||||||
|
|
||||||
|
rel_name = simplifyString(rel_name)
|
||||||
|
rel_words = re.split('\W+', rel_name)
|
||||||
|
|
||||||
|
# Make sure it has required words
|
||||||
|
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
||||||
|
try: required_words = list(set(required_words + splitString(media['category']['required'].lower())))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
req_match = 0
|
||||||
|
for req_set in required_words:
|
||||||
|
req = splitString(req_set, '&')
|
||||||
|
req_match += len(list(set(rel_words) & set(req))) == len(req)
|
||||||
|
|
||||||
|
if len(required_words) > 0 and req_match == 0:
|
||||||
|
log.info2('Wrong: Required word missing: %s', rel_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ignore releases
|
||||||
|
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
||||||
|
try: ignored_words = list(set(ignored_words + splitString(media['category']['ignored'].lower())))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
ignored_match = 0
|
||||||
|
for ignored_set in ignored_words:
|
||||||
|
ignored = splitString(ignored_set, '&')
|
||||||
|
ignored_match += len(list(set(rel_words) & set(ignored))) == len(ignored)
|
||||||
|
|
||||||
|
if len(ignored_words) > 0 and ignored_match:
|
||||||
|
log.info2("Wrong: '%s' contains 'ignored words'", rel_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ignore porn stuff
|
||||||
|
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
|
||||||
|
pron_words = list(set(rel_words) & set(pron_tags) - set(media_words))
|
||||||
|
if pron_words:
|
||||||
|
log.info('Wrong: %s, probably pr0n', rel_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
class SearchSetupError(Exception):
|
class SearchSetupError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
||||||
mergeDicts
|
mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.media.movie import MovieTypeBase
|
from couchpotato.core.media.movie import MovieTypeBase
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
|
from couchpotato.core.settings.model import Library, LibraryTitle, Media, \
|
||||||
Release
|
Release
|
||||||
from couchpotato.environment import Env
|
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||||
from string import ascii_lowercase
|
from string import ascii_lowercase
|
||||||
@@ -19,14 +18,7 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class MovieBase(MovieTypeBase):
|
class MovieBase(MovieTypeBase):
|
||||||
|
|
||||||
default_dict = {
|
_type = 'movie'
|
||||||
'profile': {'types': {'quality': {}}},
|
|
||||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
|
||||||
'library': {'titles': {}, 'files':{}},
|
|
||||||
'files': {},
|
|
||||||
'status': {},
|
|
||||||
'category': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -34,17 +26,6 @@ class MovieBase(MovieTypeBase):
|
|||||||
super(MovieBase, self).__init__()
|
super(MovieBase, self).__init__()
|
||||||
self.initType()
|
self.initType()
|
||||||
|
|
||||||
addApiView('movie.search', self.search, docs = {
|
|
||||||
'desc': 'Search the movie providers for a movie',
|
|
||||||
'params': {
|
|
||||||
'q': {'desc': 'The (partial) movie name you want to search for'},
|
|
||||||
},
|
|
||||||
'return': {'type': 'object', 'example': """{
|
|
||||||
'success': True,
|
|
||||||
'empty': bool, any movies returned or not,
|
|
||||||
'movies': array, movies found,
|
|
||||||
}"""}
|
|
||||||
})
|
|
||||||
addApiView('movie.list', self.listView, docs = {
|
addApiView('movie.list', self.listView, docs = {
|
||||||
'desc': 'List movies in wanted list',
|
'desc': 'List movies in wanted list',
|
||||||
'params': {
|
'params': {
|
||||||
@@ -66,12 +47,6 @@ class MovieBase(MovieTypeBase):
|
|||||||
'id': {'desc': 'The id of the movie'},
|
'id': {'desc': 'The id of the movie'},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
addApiView('movie.refresh', self.refresh, docs = {
|
|
||||||
'desc': 'Refresh a movie by id',
|
|
||||||
'params': {
|
|
||||||
'id': {'desc': 'Movie ID(s) you want to refresh.', 'type': 'int (comma separated)'},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
addApiView('movie.available_chars', self.charView)
|
addApiView('movie.available_chars', self.charView)
|
||||||
addApiView('movie.add', self.addView, docs = {
|
addApiView('movie.add', self.addView, docs = {
|
||||||
'desc': 'Add new movie to the wanted list',
|
'desc': 'Add new movie to the wanted list',
|
||||||
@@ -103,34 +78,6 @@ class MovieBase(MovieTypeBase):
|
|||||||
addEvent('movie.list', self.list)
|
addEvent('movie.list', self.list)
|
||||||
addEvent('movie.restatus', self.restatus)
|
addEvent('movie.restatus', self.restatus)
|
||||||
|
|
||||||
# Clean releases that didn't have activity in the last week
|
|
||||||
addEvent('app.load', self.cleanReleases)
|
|
||||||
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
|
|
||||||
|
|
||||||
def cleanReleases(self):
|
|
||||||
|
|
||||||
log.debug('Removing releases from dashboard')
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
week = 262080
|
|
||||||
|
|
||||||
done_status, available_status, snatched_status = \
|
|
||||||
fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
|
|
||||||
|
|
||||||
db = get_session()
|
|
||||||
|
|
||||||
# get movies last_edit more than a week ago
|
|
||||||
movies = db.query(Movie) \
|
|
||||||
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
|
|
||||||
.all()
|
|
||||||
|
|
||||||
for movie in movies:
|
|
||||||
for rel in movie.releases:
|
|
||||||
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
|
|
||||||
fireEvent('release.delete', id = rel.id, single = True)
|
|
||||||
|
|
||||||
db.expire_all()
|
|
||||||
|
|
||||||
def getView(self, id = None, **kwargs):
|
def getView(self, id = None, **kwargs):
|
||||||
|
|
||||||
movie = self.get(id) if id else None
|
movie = self.get(id) if id else None
|
||||||
@@ -147,9 +94,9 @@ class MovieBase(MovieTypeBase):
|
|||||||
imdb_id = getImdb(str(movie_id))
|
imdb_id = getImdb(str(movie_id))
|
||||||
|
|
||||||
if imdb_id:
|
if imdb_id:
|
||||||
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
|
||||||
else:
|
else:
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Media).filter_by(id = movie_id).first()
|
||||||
|
|
||||||
results = None
|
results = None
|
||||||
if m:
|
if m:
|
||||||
@@ -169,20 +116,20 @@ class MovieBase(MovieTypeBase):
|
|||||||
release_status = [release_status]
|
release_status = [release_status]
|
||||||
|
|
||||||
# query movie ids
|
# query movie ids
|
||||||
q = db.query(Movie) \
|
q = db.query(Media) \
|
||||||
.with_entities(Movie.id) \
|
.with_entities(Media.id) \
|
||||||
.group_by(Movie.id)
|
.group_by(Media.id)
|
||||||
|
|
||||||
# Filter on movie status
|
# Filter on movie status
|
||||||
if status and len(status) > 0:
|
if status and len(status) > 0:
|
||||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||||
statuses = [s.get('id') for s in statuses]
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
q = q.filter(Movie.status_id.in_(statuses))
|
q = q.filter(Media.status_id.in_(statuses))
|
||||||
|
|
||||||
# Filter on release status
|
# Filter on release status
|
||||||
if release_status and len(release_status) > 0:
|
if release_status and len(release_status) > 0:
|
||||||
q = q.join(Movie.releases)
|
q = q.join(Media.releases)
|
||||||
|
|
||||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||||
statuses = [s.get('id') for s in statuses]
|
statuses = [s.get('id') for s in statuses]
|
||||||
@@ -191,7 +138,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
|
|
||||||
# Only join when searching / ordering
|
# Only join when searching / ordering
|
||||||
if starts_with or search or order != 'release_order':
|
if starts_with or search or order != 'release_order':
|
||||||
q = q.join(Movie.library, Library.titles) \
|
q = q.join(Media.library, Library.titles) \
|
||||||
.filter(LibraryTitle.default == True)
|
.filter(LibraryTitle.default == True)
|
||||||
|
|
||||||
# Add search filters
|
# Add search filters
|
||||||
@@ -242,13 +189,13 @@ class MovieBase(MovieTypeBase):
|
|||||||
releases_count[release.movie_id] += 1
|
releases_count[release.movie_id] += 1
|
||||||
|
|
||||||
# Get main movie data
|
# Get main movie data
|
||||||
q2 = db.query(Movie) \
|
q2 = db.query(Media) \
|
||||||
.options(joinedload_all('library.titles')) \
|
.options(joinedload_all('library.titles')) \
|
||||||
.options(joinedload_all('library.files')) \
|
.options(joinedload_all('library.files')) \
|
||||||
.options(joinedload_all('status')) \
|
.options(joinedload_all('status')) \
|
||||||
.options(joinedload_all('files'))
|
.options(joinedload_all('files'))
|
||||||
|
|
||||||
q2 = q2.filter(Movie.id.in_(movie_ids))
|
q2 = q2.filter(Media.id.in_(movie_ids))
|
||||||
|
|
||||||
results = q2.all()
|
results = q2.all()
|
||||||
|
|
||||||
@@ -291,14 +238,14 @@ class MovieBase(MovieTypeBase):
|
|||||||
if release_status and not isinstance(release_status, (list, tuple)):
|
if release_status and not isinstance(release_status, (list, tuple)):
|
||||||
release_status = [release_status]
|
release_status = [release_status]
|
||||||
|
|
||||||
q = db.query(Movie)
|
q = db.query(Media)
|
||||||
|
|
||||||
# Filter on movie status
|
# Filter on movie status
|
||||||
if status and len(status) > 0:
|
if status and len(status) > 0:
|
||||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||||
statuses = [s.get('id') for s in statuses]
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
q = q.filter(Movie.status_id.in_(statuses))
|
q = q.filter(Media.status_id.in_(statuses))
|
||||||
|
|
||||||
# Filter on release status
|
# Filter on release status
|
||||||
if release_status and len(release_status) > 0:
|
if release_status and len(release_status) > 0:
|
||||||
@@ -306,7 +253,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||||
statuses = [s.get('id') for s in statuses]
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
q = q.join(Movie.releases) \
|
q = q.join(Media.releases) \
|
||||||
.filter(Release.status_id.in_(statuses))
|
.filter(Release.status_id.in_(statuses))
|
||||||
|
|
||||||
q = q.join(Library, LibraryTitle) \
|
q = q.join(Library, LibraryTitle) \
|
||||||
@@ -367,47 +314,6 @@ class MovieBase(MovieTypeBase):
|
|||||||
'chars': chars,
|
'chars': chars,
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh(self, id = '', **kwargs):
|
|
||||||
|
|
||||||
db = get_session()
|
|
||||||
|
|
||||||
for x in splitString(id):
|
|
||||||
movie = db.query(Movie).filter_by(id = x).first()
|
|
||||||
|
|
||||||
if movie:
|
|
||||||
|
|
||||||
# Get current selected title
|
|
||||||
default_title = ''
|
|
||||||
for title in movie.library.titles:
|
|
||||||
if title.default: default_title = title.title
|
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
|
||||||
fireEventAsync('library.update.movie', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
|
||||||
|
|
||||||
db.expire_all()
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def search(self, q = '', **kwargs):
|
|
||||||
|
|
||||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
|
||||||
movies = Env.get('cache').get(cache_key)
|
|
||||||
|
|
||||||
if not movies:
|
|
||||||
|
|
||||||
if getImdb(q):
|
|
||||||
movies = [fireEvent('movie.info', identifier = q, merge = True)]
|
|
||||||
else:
|
|
||||||
movies = fireEvent('movie.search', q = q, merge = True)
|
|
||||||
Env.get('cache').set(cache_key, movies)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'empty': len(movies) == 0 if movies else 0,
|
|
||||||
'movies': movies,
|
|
||||||
}
|
|
||||||
|
|
||||||
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
|
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||||
if not params: params = {}
|
if not params: params = {}
|
||||||
|
|
||||||
@@ -438,12 +344,12 @@ class MovieBase(MovieTypeBase):
|
|||||||
cat_id = params.get('category_id')
|
cat_id = params.get('category_id')
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
m = db.query(Media).filter_by(library_id = library.get('id')).first()
|
||||||
added = True
|
added = True
|
||||||
do_search = False
|
do_search = False
|
||||||
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
||||||
if not m:
|
if not m:
|
||||||
m = Movie(
|
m = Media(
|
||||||
library_id = library.get('id'),
|
library_id = library.get('id'),
|
||||||
profile_id = params.get('profile_id', default_profile.get('id')),
|
profile_id = params.get('profile_id', default_profile.get('id')),
|
||||||
status_id = status_id if status_id else status_active.get('id'),
|
status_id = status_id if status_id else status_active.get('id'),
|
||||||
@@ -500,15 +406,12 @@ class MovieBase(MovieTypeBase):
|
|||||||
db.expire_all()
|
db.expire_all()
|
||||||
return movie_dict
|
return movie_dict
|
||||||
|
|
||||||
|
|
||||||
def addView(self, **kwargs):
|
def addView(self, **kwargs):
|
||||||
|
add_dict = self.add(params = kwargs)
|
||||||
movie_dict = self.add(params = kwargs)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True if add_dict else False,
|
||||||
'added': True if movie_dict else False,
|
'movie': add_dict,
|
||||||
'movie': movie_dict,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def edit(self, id = '', **kwargs):
|
def edit(self, id = '', **kwargs):
|
||||||
@@ -520,7 +423,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
ids = splitString(id)
|
ids = splitString(id)
|
||||||
for movie_id in ids:
|
for movie_id in ids:
|
||||||
|
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Media).filter_by(id = movie_id).first()
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -567,7 +470,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
movie = db.query(Media).filter_by(id = movie_id).first()
|
||||||
if movie:
|
if movie:
|
||||||
deleted = False
|
deleted = False
|
||||||
if delete_from == 'all':
|
if delete_from == 'all':
|
||||||
@@ -617,7 +520,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Media).filter_by(id = movie_id).first()
|
||||||
if not m or len(m.library.titles) == 0:
|
if not m or len(m.library.titles) == 0:
|
||||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||||
return False
|
return False
|
||||||
@@ -638,24 +541,3 @@ class MovieBase(MovieTypeBase):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def createOnComplete(self, movie_id):
|
|
||||||
|
|
||||||
def onComplete():
|
|
||||||
db = get_session()
|
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
|
||||||
fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
|
||||||
db.expire_all()
|
|
||||||
|
|
||||||
return onComplete
|
|
||||||
|
|
||||||
|
|
||||||
def createNotifyFront(self, movie_id):
|
|
||||||
|
|
||||||
def notifyFront():
|
|
||||||
db = get_session()
|
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
|
||||||
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
|
||||||
db.expire_all()
|
|
||||||
|
|
||||||
return notifyFront
|
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ var MovieList = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
var ids = self.getSelectedMovies()
|
var ids = self.getSelectedMovies()
|
||||||
|
|
||||||
Api.request('movie.refresh', {
|
Api.request('media.refresh', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': ids.join(','),
|
'id': ids.join(','),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,7 +241,6 @@ MA.Release = new Class({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
).inject(self.release_container);
|
).inject(self.release_container);
|
||||||
|
|
||||||
release['el'] = item;
|
release['el'] = item;
|
||||||
|
|
||||||
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||||
@@ -251,6 +250,30 @@ MA.Release = new Class({
|
|||||||
else if(!self.next_release && status.identifier == 'available'){
|
else if(!self.next_release && status.identifier == 'available'){
|
||||||
self.next_release = release;
|
self.next_release = release;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var update_handle = function(notification) {
|
||||||
|
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
|
||||||
|
status = Status.get(release.status_id),
|
||||||
|
new_status = Status.get(notification.data);
|
||||||
|
|
||||||
|
release.status_id = new_status.id
|
||||||
|
release.el.set('class', 'item ' + new_status.identifier);
|
||||||
|
|
||||||
|
var status_el = release.el.getElement('.release_status');
|
||||||
|
status_el.set('class', 'release_status ' + new_status.identifier);
|
||||||
|
status_el.set('text', new_status.identifier);
|
||||||
|
|
||||||
|
if(!q && (new_status.identifier == 'snatched' || new_status.identifier == 'seeding' || new_status.identifier == 'done'))
|
||||||
|
var q = self.addQuality(release.quality_id);
|
||||||
|
|
||||||
|
if(new_status && q && !q.hasClass(new_status.identifier)) {
|
||||||
|
q.removeClass(status.identifier).addClass(new_status.identifier);
|
||||||
|
q.set('title', q.get('title').replace(status.label, new_status.label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
App.addEvent('release.update_status.' + release.id, update_handle);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if(self.last_release)
|
if(self.last_release)
|
||||||
@@ -358,7 +381,7 @@ MA.Release = new Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
get: function(release, type){
|
get: function(release, type){
|
||||||
return release.info[type] || 'n/a'
|
return release.info[type] !== undefined ? release.info[type] : 'n/a'
|
||||||
},
|
},
|
||||||
|
|
||||||
download: function(release){
|
download: function(release){
|
||||||
@@ -370,7 +393,7 @@ MA.Release = new Class({
|
|||||||
if(icon)
|
if(icon)
|
||||||
icon.addClass('icon spinner').removeClass('download');
|
icon.addClass('icon spinner').removeClass('download');
|
||||||
|
|
||||||
Api.request('release.download', {
|
Api.request('release.manual_download', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': release.id
|
'id': release.id
|
||||||
},
|
},
|
||||||
@@ -397,17 +420,6 @@ MA.Release = new Class({
|
|||||||
'data': {
|
'data': {
|
||||||
'id': release.id
|
'id': release.id
|
||||||
},
|
},
|
||||||
'onComplete': function(){
|
|
||||||
var el = release.el;
|
|
||||||
if(el && (el.hasClass('failed') || el.hasClass('ignored'))){
|
|
||||||
el.removeClass('failed').removeClass('ignored');
|
|
||||||
el.getElement('.release_status').set('text', 'available');
|
|
||||||
}
|
|
||||||
else if(el) {
|
|
||||||
el.addClass('ignored');
|
|
||||||
el.getElement('.release_status').set('text', 'ignored');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -694,7 +706,7 @@ MA.Refresh = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
(e).preventDefault();
|
(e).preventDefault();
|
||||||
|
|
||||||
Api.request('movie.refresh', {
|
Api.request('media.refresh', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': self.movie.get('id')
|
'id': self.movie.get('id')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,22 +419,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.movies .data .quality .available,
|
.movies .data .quality .available,
|
||||||
.movies .data .quality .snatched {
|
.movies .data .quality .snatched,
|
||||||
|
.movies .data .quality .seeding {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .data .quality .available { background-color: #578bc3; }
|
.movies .data .quality .available { background-color: #578bc3; }
|
||||||
.movies .data .quality .failed { background-color: #a43d34; }
|
.movies .data .quality .failed,
|
||||||
|
.movies .data .quality .missing,
|
||||||
|
.movies .data .quality .ignored { background-color: #a43d34; }
|
||||||
.movies .data .quality .snatched { background-color: #a2a232; }
|
.movies .data .quality .snatched { background-color: #a2a232; }
|
||||||
.movies .data .quality .seeding { background-color: #0a6819; }
|
|
||||||
.movies .data .quality .done {
|
.movies .data .quality .done {
|
||||||
background-color: #369545;
|
background-color: #369545;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
.movies .data .quality .seeding { background-color: #0a6819; }
|
||||||
.movies .data .quality .finish {
|
.movies .data .quality .finish {
|
||||||
background-image: url('../images/sprite.png');
|
background-image: url('../../images/sprite.png');
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 0 2px;
|
background-position: 0 2px;
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
background-size: 14px
|
background-size: 14px
|
||||||
@@ -646,7 +649,7 @@
|
|||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.trailer_container.hide {
|
.trailer_container.hide {
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -989,7 +992,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.movies .empty_wanted {
|
.movies .empty_wanted {
|
||||||
background-image: url('../images/emptylist.png');
|
background-image: url('../../images/emptylist.png');
|
||||||
background-position: 80% 0;
|
background-position: 80% 0;
|
||||||
height: 750px;
|
height: 750px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1029,7 +1032,7 @@
|
|||||||
.movies .progress > div .folder {
|
.movies .progress > div .folder {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 5px 20px 5px 0;
|
padding: 5px 20px 5px 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 85%;
|
width: 85%;
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ var Movie = new Class({
|
|||||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||||
status = Status.get(release.status_id);
|
status = Status.get(release.status_id);
|
||||||
|
|
||||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
|
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
|
||||||
var q = self.addQuality(release.quality_id)
|
var q = self.addQuality(release.quality_id)
|
||||||
|
|
||||||
if (status && q && !q.hasClass(status.identifier)){
|
if (status && q && !q.hasClass(status.identifier)){
|
||||||
|
|||||||
@@ -1,189 +1,4 @@
|
|||||||
Block.Search = new Class({
|
Block.Search.MovieItem = new Class({
|
||||||
|
|
||||||
Extends: BlockBase,
|
|
||||||
|
|
||||||
cache: {},
|
|
||||||
|
|
||||||
create: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var focus_timer = 0;
|
|
||||||
self.el = new Element('div.search_form').adopt(
|
|
||||||
new Element('div.input').adopt(
|
|
||||||
self.input = new Element('input', {
|
|
||||||
'placeholder': 'Search & add a new movie',
|
|
||||||
'events': {
|
|
||||||
'keyup': self.keyup.bind(self),
|
|
||||||
'focus': function(){
|
|
||||||
if(focus_timer) clearTimeout(focus_timer);
|
|
||||||
self.el.addClass('focused')
|
|
||||||
if(this.get('value'))
|
|
||||||
self.hideResults(false)
|
|
||||||
},
|
|
||||||
'blur': function(){
|
|
||||||
focus_timer = (function(){
|
|
||||||
self.el.removeClass('focused')
|
|
||||||
}).delay(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new Element('a.icon2', {
|
|
||||||
'events': {
|
|
||||||
'click': self.clear.bind(self),
|
|
||||||
'touchend': self.clear.bind(self)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
),
|
|
||||||
self.result_container = new Element('div.results_container', {
|
|
||||||
'tween': {
|
|
||||||
'duration': 200
|
|
||||||
},
|
|
||||||
'events': {
|
|
||||||
'mousewheel': function(e){
|
|
||||||
(e).stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).adopt(
|
|
||||||
self.results = new Element('div.results')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
clear: function(e){
|
|
||||||
var self = this;
|
|
||||||
(e).preventDefault();
|
|
||||||
|
|
||||||
if(self.last_q === ''){
|
|
||||||
self.input.blur()
|
|
||||||
self.last_q = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
self.last_q = '';
|
|
||||||
self.input.set('value', '');
|
|
||||||
self.input.focus()
|
|
||||||
|
|
||||||
self.movies = []
|
|
||||||
self.results.empty()
|
|
||||||
self.el.removeClass('filled')
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hideResults: function(bool){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(self.hidden == bool) return;
|
|
||||||
|
|
||||||
self.el[bool ? 'removeClass' : 'addClass']('shown');
|
|
||||||
|
|
||||||
if(bool){
|
|
||||||
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
|
||||||
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
History.addEvent('change', self.hideResults.bind(self, !bool));
|
|
||||||
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.hidden = bool;
|
|
||||||
},
|
|
||||||
|
|
||||||
keyup: function(e){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
|
||||||
|
|
||||||
if(self.q() != self.last_q){
|
|
||||||
if(self.api_request && self.api_request.isRunning())
|
|
||||||
self.api_request.cancel();
|
|
||||||
|
|
||||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
|
||||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(!self.q()){
|
|
||||||
self.hideResults(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.list()
|
|
||||||
},
|
|
||||||
|
|
||||||
list: function(){
|
|
||||||
var self = this,
|
|
||||||
q = self.q(),
|
|
||||||
cache = self.cache[q];
|
|
||||||
|
|
||||||
self.hideResults(false);
|
|
||||||
|
|
||||||
if(!cache){
|
|
||||||
self.mask.fade('in');
|
|
||||||
|
|
||||||
if(!self.spinner)
|
|
||||||
self.spinner = createSpinner(self.mask);
|
|
||||||
|
|
||||||
self.api_request = Api.request('movie.search', {
|
|
||||||
'data': {
|
|
||||||
'q': q
|
|
||||||
},
|
|
||||||
'onComplete': self.fill.bind(self, q)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else
|
|
||||||
self.fill(q, cache)
|
|
||||||
|
|
||||||
self.last_q = q;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
fill: function(q, json){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
self.cache[q] = json
|
|
||||||
|
|
||||||
self.movies = {}
|
|
||||||
self.results.empty()
|
|
||||||
|
|
||||||
Object.each(json.movies, function(movie){
|
|
||||||
|
|
||||||
var m = new Block.Search.Item(movie);
|
|
||||||
$(m).inject(self.results)
|
|
||||||
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
|
||||||
|
|
||||||
if(q == movie.imdb)
|
|
||||||
m.showOptions()
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate result heights
|
|
||||||
var w = window.getSize(),
|
|
||||||
rc = self.result_container.getCoordinates();
|
|
||||||
|
|
||||||
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
|
||||||
self.mask.fade('out')
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
loading: function(bool){
|
|
||||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
|
||||||
},
|
|
||||||
|
|
||||||
q: function(){
|
|
||||||
return this.input.get('value').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Block.Search.Item = new Class({
|
|
||||||
|
|
||||||
Implements: [Options, Events],
|
Implements: [Options, Events],
|
||||||
|
|
||||||
@@ -201,7 +16,7 @@ Block.Search.Item = new Class({
|
|||||||
var self = this,
|
var self = this,
|
||||||
info = self.info;
|
info = self.info;
|
||||||
|
|
||||||
self.el = new Element('div.movie_result', {
|
self.el = new Element('div.media_result', {
|
||||||
'id': info.imdb
|
'id': info.imdb
|
||||||
}).adopt(
|
}).adopt(
|
||||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||||
@@ -292,7 +107,7 @@ Block.Search.Item = new Class({
|
|||||||
self.options_el.empty();
|
self.options_el.empty();
|
||||||
self.options_el.adopt(
|
self.options_el.adopt(
|
||||||
new Element('div.message', {
|
new Element('div.message', {
|
||||||
'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
|
'text': json.success ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
self.mask.fade('out');
|
self.mask.fade('out');
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class MovieLibraryPlugin(LibraryBase):
|
|||||||
else:
|
else:
|
||||||
dates = library.info.get('release_date')
|
dates = library.info.get('release_date')
|
||||||
|
|
||||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates:
|
||||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||||
library.info.update({'release_date': dates })
|
library.info.update({'release_date': dates })
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
|
from couchpotato.core.helpers.encoding import simplifyString
|
||||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb
|
||||||
possibleTitles, getImdb
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||||
from couchpotato.core.media.movie import MovieTypeBase
|
from couchpotato.core.media.movie import MovieTypeBase
|
||||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
from couchpotato.core.settings.model import Media, Release
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from sqlalchemy.exc import InterfaceError
|
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -29,9 +27,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
addEvent('movie.searcher.all', self.searchAll)
|
addEvent('movie.searcher.all', self.searchAll)
|
||||||
addEvent('movie.searcher.all_view', self.searchAllView)
|
addEvent('movie.searcher.all_view', self.searchAllView)
|
||||||
addEvent('movie.searcher.single', self.single)
|
addEvent('movie.searcher.single', self.single)
|
||||||
addEvent('movie.searcher.correct_movie', self.correctMovie)
|
|
||||||
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
|
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
|
||||||
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
|
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
|
||||||
|
addEvent('searcher.correct_release', self.correctRelease)
|
||||||
|
addEvent('searcher.get_search_title', self.getSearchTitle)
|
||||||
|
|
||||||
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
|
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
|
||||||
'desc': 'Marks the snatched results as ignored and try the next best release',
|
'desc': 'Marks the snatched results as ignored and try the next best release',
|
||||||
@@ -74,8 +73,8 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
movies = db.query(Movie).filter(
|
movies = db.query(Media).filter(
|
||||||
Movie.status.has(identifier = 'active')
|
Media.status.has(identifier = 'active')
|
||||||
).all()
|
).all()
|
||||||
random.shuffle(movies)
|
random.shuffle(movies)
|
||||||
|
|
||||||
@@ -117,6 +116,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
|
|
||||||
def single(self, movie, search_protocols = None, manual = False):
|
def single(self, movie, search_protocols = None, manual = False):
|
||||||
|
|
||||||
|
# movies don't contain 'type' yet, so just set to default here
|
||||||
|
if not movie.has_key('type'):
|
||||||
|
movie['type'] = 'movie'
|
||||||
|
|
||||||
# Find out search type
|
# Find out search type
|
||||||
try:
|
try:
|
||||||
if not search_protocols:
|
if not search_protocols:
|
||||||
@@ -167,82 +170,20 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
||||||
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
||||||
|
|
||||||
results = []
|
results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
|
||||||
for search_protocol in search_protocols:
|
if len(results) == 0:
|
||||||
protocol_results = fireEvent('provider.search.%s.movie' % search_protocol, movie, quality, merge = True)
|
|
||||||
if protocol_results:
|
|
||||||
results += protocol_results
|
|
||||||
|
|
||||||
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
|
||||||
if len(sorted_results) == 0:
|
|
||||||
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
|
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
|
||||||
|
|
||||||
download_preference = self.conf('preferred_method', section = 'searcher')
|
|
||||||
if download_preference != 'both':
|
|
||||||
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
|
||||||
|
|
||||||
# Check if movie isn't deleted while searching
|
# Check if movie isn't deleted while searching
|
||||||
if not db.query(Movie).filter_by(id = movie.get('id')).first():
|
if not db.query(Media).filter_by(id = movie.get('id')).first():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Add them to this movie releases list
|
# Add them to this movie releases list
|
||||||
for nzb in sorted_results:
|
found_releases += fireEvent('release.create_from_search', results, movie, quality_type, single = True)
|
||||||
|
|
||||||
nzb_identifier = md5(nzb['url'])
|
# Try find a valid result and download it
|
||||||
found_releases.append(nzb_identifier)
|
if fireEvent('release.try_download_result', results, movie, quality_type, manual, single = True):
|
||||||
|
ret = True
|
||||||
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
|
|
||||||
if not rls:
|
|
||||||
rls = Release(
|
|
||||||
identifier = nzb_identifier,
|
|
||||||
movie_id = movie.get('id'),
|
|
||||||
quality_id = quality_type.get('quality_id'),
|
|
||||||
status_id = available_status.get('id')
|
|
||||||
)
|
|
||||||
db.add(rls)
|
|
||||||
else:
|
|
||||||
[db.delete(old_info) for old_info in rls.info]
|
|
||||||
rls.last_edit = int(time.time())
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
for info in nzb:
|
|
||||||
try:
|
|
||||||
if not isinstance(nzb[info], (str, unicode, int, long, float)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
rls_info = ReleaseInfo(
|
|
||||||
identifier = info,
|
|
||||||
value = toUnicode(nzb[info])
|
|
||||||
)
|
|
||||||
rls.info.append(rls_info)
|
|
||||||
except InterfaceError:
|
|
||||||
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
nzb['status_id'] = rls.status_id
|
|
||||||
|
|
||||||
|
|
||||||
for nzb in sorted_results:
|
|
||||||
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
|
||||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
|
||||||
log.info('Ignored: %s', nzb['name'])
|
|
||||||
continue
|
|
||||||
|
|
||||||
if nzb['score'] <= 0:
|
|
||||||
log.info('Ignored, score to low: %s', nzb['name'])
|
|
||||||
continue
|
|
||||||
|
|
||||||
downloaded = fireEvent('searcher.download', data = nzb, movie = movie, manual = manual, single = True)
|
|
||||||
if downloaded is True:
|
|
||||||
ret = True
|
|
||||||
break
|
|
||||||
elif downloaded != 'try_next':
|
|
||||||
break
|
|
||||||
|
|
||||||
# Remove releases that aren't found anymore
|
# Remove releases that aren't found anymore
|
||||||
for release in movie.get('releases', []):
|
for release in movie.get('releases', []):
|
||||||
@@ -265,7 +206,11 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
|
def correctRelease(self, nzb = None, media = None, quality = None, **kwargs):
|
||||||
|
|
||||||
|
if media.get('type') != 'movie': return
|
||||||
|
|
||||||
|
media_title = fireEvent('searcher.get_search_title', media, single = True)
|
||||||
|
|
||||||
imdb_results = kwargs.get('imdb_results', False)
|
imdb_results = kwargs.get('imdb_results', False)
|
||||||
retention = Env.setting('retention', section = 'nzb')
|
retention = Env.setting('retention', section = 'nzb')
|
||||||
@@ -274,50 +219,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
movie_name = getTitle(movie['library'])
|
# Check for required and ignored words
|
||||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
if not fireEvent('searcher.correct_words', nzb['name'], media, single = True):
|
||||||
nzb_name = simplifyString(nzb['name'])
|
|
||||||
nzb_words = re.split('\W+', nzb_name)
|
|
||||||
|
|
||||||
# Make sure it has required words
|
|
||||||
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
|
||||||
try: required_words = list(set(required_words + splitString(movie['category']['required'].lower())))
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
req_match = 0
|
|
||||||
for req_set in required_words:
|
|
||||||
req = splitString(req_set, '&')
|
|
||||||
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
|
||||||
|
|
||||||
if len(required_words) > 0 and req_match == 0:
|
|
||||||
log.info2('Wrong: Required word missing: %s', nzb['name'])
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ignore releases
|
|
||||||
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
|
||||||
try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower())))
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
ignored_match = 0
|
|
||||||
for ignored_set in ignored_words:
|
|
||||||
ignored = splitString(ignored_set, '&')
|
|
||||||
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
|
|
||||||
|
|
||||||
if len(ignored_words) > 0 and ignored_match:
|
|
||||||
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ignore porn stuff
|
|
||||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
|
|
||||||
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
|
||||||
if pron_words:
|
|
||||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
||||||
|
|
||||||
# Contains lower quality string
|
# Contains lower quality string
|
||||||
if fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True):
|
if fireEvent('searcher.contains_other_quality', nzb, movie_year = media['library']['year'], preferred_quality = preferred_quality, single = True):
|
||||||
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -347,23 +256,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Check if nzb contains imdb link
|
# Check if nzb contains imdb link
|
||||||
if getImdb(nzb.get('description', '')) == movie['library']['identifier']:
|
if getImdb(nzb.get('description', '')) == media['library']['identifier']:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for raw_title in movie['library']['titles']:
|
for raw_title in media['library']['titles']:
|
||||||
for movie_title in possibleTitles(raw_title['title']):
|
for movie_title in possibleTitles(raw_title['title']):
|
||||||
movie_words = re.split('\W+', simplifyString(movie_title))
|
movie_words = re.split('\W+', simplifyString(movie_title))
|
||||||
|
|
||||||
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
|
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
|
||||||
# if no IMDB link, at least check year range 1
|
# if no IMDB link, at least check year range 1
|
||||||
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True):
|
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 1, single = True):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# if no IMDB link, at least check year
|
# if no IMDB link, at least check year
|
||||||
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True):
|
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 0, single = True):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
|
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['library']['year']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def couldBeReleased(self, is_pre_release, dates, year = None):
|
def couldBeReleased(self, is_pre_release, dates, year = None):
|
||||||
@@ -434,5 +343,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
log.error('Failed searching for next release: %s', traceback.format_exc())
|
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getSearchTitle(self, media):
|
||||||
|
if media['type'] == 'movie':
|
||||||
|
return getTitle(media['library'])
|
||||||
|
|
||||||
class SearchSetupError(Exception):
|
class SearchSetupError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|||||||
+5
-5
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
|
|||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.helpers.variable import splitString
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie, Library
|
from couchpotato.core.settings.model import Media, Library
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
from sqlalchemy.sql.expression import or_
|
from sqlalchemy.sql.expression import or_
|
||||||
@@ -29,9 +29,9 @@ class Suggestion(Plugin):
|
|||||||
|
|
||||||
if not movies or len(movies) == 0:
|
if not movies or len(movies) == 0:
|
||||||
db = get_session()
|
db = get_session()
|
||||||
active_movies = db.query(Movie) \
|
active_movies = db.query(Media) \
|
||||||
.options(joinedload_all('library')) \
|
.options(joinedload_all('library')) \
|
||||||
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all()
|
.filter(or_(*[Media.status.has(identifier = s) for s in ['active', 'done']])).all()
|
||||||
movies = [x.library.identifier for x in active_movies]
|
movies = [x.library.identifier for x in active_movies]
|
||||||
|
|
||||||
if not ignored or len(ignored) == 0:
|
if not ignored or len(ignored) == 0:
|
||||||
@@ -89,10 +89,10 @@ class Suggestion(Plugin):
|
|||||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
active_movies = db.query(Movie) \
|
active_movies = db.query(Media) \
|
||||||
.join(Library) \
|
.join(Library) \
|
||||||
.with_entities(Library.identifier) \
|
.with_entities(Library.identifier) \
|
||||||
.filter(Movie.status_id.in_([active_status.get('id'), done_status.get('id')])).all()
|
.filter(Media.status_id.in_([active_status.get('id'), done_status.get('id')])).all()
|
||||||
movies = [x[0] for x in active_movies]
|
movies = [x[0] for x in active_movies]
|
||||||
movies.extend(seen)
|
movies.extend(seen)
|
||||||
|
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
.suggestions {
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions > h2 {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result {
|
||||||
|
display: inline-block;
|
||||||
|
width: 33.333%;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 960px) {
|
||||||
|
.suggestions .media_result {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 600px) {
|
||||||
|
.suggestions .media_result {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data {
|
||||||
|
left: 100px;
|
||||||
|
background: #4e5969;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data .info {
|
||||||
|
top: 10px;
|
||||||
|
left: 15px;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data .info h2 {
|
||||||
|
white-space: normal;
|
||||||
|
max-height: 120px;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data .info .rating,
|
||||||
|
.suggestions .media_result .data .info .genres,
|
||||||
|
.suggestions .media_result .data .info .year {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data .info .year {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data .info .rating {
|
||||||
|
font-size: 20px;
|
||||||
|
float: right;
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
.suggestions .media_result .data .info .rating:before {
|
||||||
|
content: "\e031";
|
||||||
|
font-family: 'Elusive-Icons';
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data .info .genres {
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data .info .plot {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: justify;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
top: 64px;
|
||||||
|
position: absolute;
|
||||||
|
background: #4e5969;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .4s ease-in-out;
|
||||||
|
padding: 0 3px 10px 0;
|
||||||
|
}
|
||||||
|
.suggestions .media_result .data:before {
|
||||||
|
bottom: 0;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 10px;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 10px;
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(78, 89, 105, 1) 0%,
|
||||||
|
rgba(78, 89, 105, 0) 100%
|
||||||
|
);
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data .info .plot.full {
|
||||||
|
top: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .data {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .options {
|
||||||
|
left: 100px;
|
||||||
|
}
|
||||||
|
.suggestions .media_result .options select[name=title] { width: 100%; }
|
||||||
|
.suggestions .media_result .options select[name=profile] { width: 100%; }
|
||||||
|
.suggestions .media_result .options select[name=category] { width: 100%; }
|
||||||
|
|
||||||
|
.suggestions .media_result .button {
|
||||||
|
position: absolute;
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.suggestions .media_result .thumbnail {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: none;
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
.suggestions .media_result:hover .actions {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.suggestions .media_result:hover h2 .title {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.suggestions .media_result .data.open .actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .media_result .actions a {
|
||||||
|
margin-left: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
+15
-3
@@ -17,7 +17,7 @@ var SuggestList = new Class({
|
|||||||
'click:relay(a.delete)': function(e, el){
|
'click:relay(a.delete)': function(e, el){
|
||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
$(el).getParent('.movie_result').destroy();
|
$(el).getParent('.media_result').destroy();
|
||||||
|
|
||||||
Api.request('suggestion.ignore', {
|
Api.request('suggestion.ignore', {
|
||||||
'data': {
|
'data': {
|
||||||
@@ -30,7 +30,7 @@ var SuggestList = new Class({
|
|||||||
'click:relay(a.eye-open)': function(e, el){
|
'click:relay(a.eye-open)': function(e, el){
|
||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
$(el).getParent('.movie_result').destroy();
|
$(el).getParent('.media_result').destroy();
|
||||||
|
|
||||||
Api.request('suggestion.ignore', {
|
Api.request('suggestion.ignore', {
|
||||||
'data': {
|
'data': {
|
||||||
@@ -65,7 +65,7 @@ var SuggestList = new Class({
|
|||||||
|
|
||||||
Object.each(json.suggestions, function(movie){
|
Object.each(json.suggestions, function(movie){
|
||||||
|
|
||||||
var m = new Block.Search.Item(movie, {
|
var m = new Block.Search.MovieItem(movie, {
|
||||||
'onAdded': function(){
|
'onAdded': function(){
|
||||||
self.afterAdded(m, movie)
|
self.afterAdded(m, movie)
|
||||||
}
|
}
|
||||||
@@ -95,6 +95,10 @@ var SuggestList = new Class({
|
|||||||
);
|
);
|
||||||
m.data_container.removeEvents('click');
|
m.data_container.removeEvents('click');
|
||||||
|
|
||||||
|
var plot = false;
|
||||||
|
if(m.info.plot && m.info.plot.length > 0)
|
||||||
|
plot = m.info.plot;
|
||||||
|
|
||||||
// Add rating
|
// Add rating
|
||||||
m.info_container.adopt(
|
m.info_container.adopt(
|
||||||
m.rating = m.info.rating && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
|
m.rating = m.info.rating && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
|
||||||
@@ -103,6 +107,14 @@ var SuggestList = new Class({
|
|||||||
}) : null,
|
}) : null,
|
||||||
m.genre = m.info.genres && m.info.genres.length > 0 ? new Element('span.genres', {
|
m.genre = m.info.genres && m.info.genres.length > 0 ? new Element('span.genres', {
|
||||||
'text': m.info.genres.slice(0, 3).join(', ')
|
'text': m.info.genres.slice(0, 3).join(', ')
|
||||||
|
}) : null,
|
||||||
|
m.plot = plot ? new Element('span.plot', {
|
||||||
|
'text': plot,
|
||||||
|
'events': {
|
||||||
|
'click': function(){
|
||||||
|
this.toggleClass('full')
|
||||||
|
}
|
||||||
|
}
|
||||||
}) : null
|
}) : null
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
config = {
|
config = [{
|
||||||
'name': 'notification_providers',
|
'name': 'notification_providers',
|
||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
@@ -10,4 +10,4 @@ config = {
|
|||||||
'options': [],
|
'options': [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}]
|
||||||
|
|||||||
@@ -28,12 +28,23 @@ config = [{
|
|||||||
'name': 'smtp_server',
|
'name': 'smtp_server',
|
||||||
'label': 'SMTP server',
|
'label': 'SMTP server',
|
||||||
},
|
},
|
||||||
|
{ 'name': 'smtp_port',
|
||||||
|
'label': 'SMTP server port',
|
||||||
|
'default': '25',
|
||||||
|
'type': 'int',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'ssl',
|
'name': 'ssl',
|
||||||
'label': 'Enable SSL',
|
'label': 'Enable SSL',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'starttls',
|
||||||
|
'label': 'Enable StartTLS',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'smtp_user',
|
'name': 'smtp_user',
|
||||||
'label': 'SMTP user',
|
'label': 'SMTP user',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from couchpotato.core.helpers.encoding import toUnicode
|
|||||||
from couchpotato.core.helpers.variable import splitString
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
from couchpotato.environment import Env
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
import smtplib
|
import smtplib
|
||||||
import traceback
|
import traceback
|
||||||
@@ -21,18 +22,28 @@ class Email(Notification):
|
|||||||
smtp_server = self.conf('smtp_server')
|
smtp_server = self.conf('smtp_server')
|
||||||
smtp_user = self.conf('smtp_user')
|
smtp_user = self.conf('smtp_user')
|
||||||
smtp_pass = self.conf('smtp_pass')
|
smtp_pass = self.conf('smtp_pass')
|
||||||
|
smtp_port = self.conf('smtp_port')
|
||||||
|
starttls = self.conf('starttls')
|
||||||
|
|
||||||
# Make the basic message
|
# Make the basic message
|
||||||
message = MIMEText(toUnicode(message))
|
message = MIMEText(toUnicode(message), _charset = Env.get('encoding'))
|
||||||
message['Subject'] = self.default_title
|
message['Subject'] = self.default_title
|
||||||
message['From'] = from_address
|
message['From'] = from_address
|
||||||
message['To'] = to_address
|
message['To'] = to_address
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Open the SMTP connection, via SSL if requested
|
# Open the SMTP connection, via SSL if requested
|
||||||
|
log.debug("Connecting to host %s on port %s" % (smtp_server, smtp_port))
|
||||||
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
|
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
|
||||||
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
|
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
|
||||||
|
|
||||||
|
if (starttls):
|
||||||
|
log.debug("Using StartTLS to initiate the connection with the SMTP server")
|
||||||
|
mailserver.starttls()
|
||||||
|
|
||||||
|
# Say hello to the server
|
||||||
|
mailserver.ehlo()
|
||||||
|
|
||||||
# Check too see if an login attempt should be attempted
|
# Check too see if an login attempt should be attempted
|
||||||
if len(smtp_user) > 0:
|
if len(smtp_user) > 0:
|
||||||
log.debug("Logging on to SMTP server using username \'%s\'%s", (smtp_user, " and a password" if len(smtp_pass) > 0 else ""))
|
log.debug("Logging on to SMTP server using username \'%s\'%s", (smtp_user, " and a password" if len(smtp_pass) > 0 else ""))
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
from .main import Notifo
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Notifo()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'notifo',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'notifications',
|
|
||||||
'list': 'notification_providers',
|
|
||||||
'name': 'notifo',
|
|
||||||
'description': 'Keep in mind that Notifo service will end soon.',
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'api_key',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'on_snatch',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Also send message when movie is snatched.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.notifications.base import Notification
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Notifo(Notification):
|
|
||||||
|
|
||||||
url = 'https://api.notifo.com/v1/send_notification'
|
|
||||||
|
|
||||||
def notify(self, message = '', data = None, listener = None):
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
params = {
|
|
||||||
'label': self.default_title,
|
|
||||||
'msg': toUnicode(message),
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('api_key')))[:-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
handle = self.urlopen(self.url, params = params, headers = headers)
|
|
||||||
result = json.loads(handle)
|
|
||||||
|
|
||||||
if result['status'] != 'success' or result['response_message'] != 'OK':
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.error('Notification failed: %s', traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.info('Notifo notification successful.')
|
|
||||||
return True
|
|
||||||
Regular → Executable
+8
-3
@@ -17,10 +17,15 @@ config = [{
|
|||||||
'type': 'enabler',
|
'type': 'enabler',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'host',
|
'name': 'media_server',
|
||||||
|
'label': 'Media Server',
|
||||||
'default': 'localhost',
|
'default': 'localhost',
|
||||||
'description': 'Default should be on localhost',
|
'description': 'Hostname/IP, default localhost'
|
||||||
'advanced': True,
|
},
|
||||||
|
{
|
||||||
|
'name': 'clients',
|
||||||
|
'default': '',
|
||||||
|
'description': 'Comma separated list of client names\'s (computer names). Top right when you start Plex'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'on_snatch',
|
'name': 'on_snatch',
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import json
|
||||||
|
from couchpotato import CPLog
|
||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
|
import requests
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PlexClientProtocol(object):
|
||||||
|
def __init__(self, plex):
|
||||||
|
self.plex = plex
|
||||||
|
|
||||||
|
addEvent('notify.plex.notifyClient', self.notify)
|
||||||
|
|
||||||
|
def notify(self, client, message):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class PlexClientHTTP(PlexClientProtocol):
|
||||||
|
def request(self, command, client):
|
||||||
|
url = 'http://%s:%s/xbmcCmds/xbmcHttp/?%s' % (
|
||||||
|
client['address'],
|
||||||
|
client['port'],
|
||||||
|
tryUrlencode(command)
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False)
|
||||||
|
except Exception, err:
|
||||||
|
log.error("Couldn't sent command to Plex: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def notify(self, client, message):
|
||||||
|
if client.get('protocol') != 'xbmchttp':
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'command': 'ExecBuiltIn',
|
||||||
|
'parameter': 'Notification(CouchPotato, %s)' % message
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.request(data, client)
|
||||||
|
|
||||||
|
|
||||||
|
class PlexClientJSON(PlexClientProtocol):
|
||||||
|
def request(self, method, params, client):
|
||||||
|
log.debug('sendJSON("%s", %s, %s)', (method, params, client))
|
||||||
|
url = 'http://%s:%s/jsonrpc' % (
|
||||||
|
client['address'],
|
||||||
|
client['port']
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
request = {
|
||||||
|
'id': 1,
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': method,
|
||||||
|
'params': params
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
requests.post(url, headers = headers, timeout = 3, data = json.dumps(request))
|
||||||
|
except Exception, err:
|
||||||
|
log.error("Couldn't sent command to Plex: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def notify(self, client, message):
|
||||||
|
if client.get('protocol') not in ['xbmcjson', 'plex']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'title': 'CouchPotato',
|
||||||
|
'message': message
|
||||||
|
}
|
||||||
|
return self.request('GUI.ShowNotification', params, client)
|
||||||
Regular → Executable
+42
-72
@@ -1,78 +1,64 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from urllib2 import URLError
|
from .client import PlexClientHTTP, PlexClientJSON
|
||||||
from urlparse import urlparse
|
from .server import PlexServer
|
||||||
from xml.dom import minidom
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Plex(Notification):
|
class Plex(Notification):
|
||||||
|
|
||||||
|
http_time_between_calls = 0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(Plex, self).__init__()
|
super(Plex, self).__init__()
|
||||||
|
|
||||||
|
self.server = PlexServer(self)
|
||||||
|
|
||||||
|
self.client_protocols = {
|
||||||
|
'http': PlexClientHTTP(self),
|
||||||
|
'json': PlexClientJSON(self)
|
||||||
|
}
|
||||||
|
|
||||||
addEvent('renamer.after', self.addToLibrary)
|
addEvent('renamer.after', self.addToLibrary)
|
||||||
|
|
||||||
def addToLibrary(self, message = None, group = None):
|
|
||||||
|
def addToLibrary(self, message = None, group = {}):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
if not group: group = {}
|
|
||||||
|
|
||||||
log.info('Sending notification to Plex')
|
return self.server.refresh()
|
||||||
hosts = self.getHosts(port = 32400)
|
|
||||||
|
|
||||||
for host in hosts:
|
def getClientNames(self):
|
||||||
|
return [
|
||||||
|
x.strip().lower()
|
||||||
|
for x in self.conf('clients').split(',')
|
||||||
|
]
|
||||||
|
|
||||||
source_type = ['movie']
|
def notifyClients(self, message, client_names):
|
||||||
base_url = '%s/library/sections' % host
|
success = True
|
||||||
refresh_url = '%s/%%s/refresh' % base_url
|
|
||||||
|
|
||||||
try:
|
for client_name in client_names:
|
||||||
sections_xml = self.urlopen(base_url)
|
|
||||||
xml_sections = minidom.parseString(sections_xml)
|
|
||||||
sections = xml_sections.getElementsByTagName('Directory')
|
|
||||||
|
|
||||||
for s in sections:
|
client_success = False
|
||||||
if s.getAttribute('type') in source_type:
|
client = self.server.clients.get(client_name)
|
||||||
url = refresh_url % s.getAttribute('key')
|
|
||||||
self.urlopen(url)
|
|
||||||
|
|
||||||
except:
|
if client and client['found']:
|
||||||
log.error('Plex library update failed for %s, Media Server not running: %s', (host, traceback.format_exc(1)))
|
client_success = fireEvent('notify.plex.notifyClient', client, message, single = True)
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
if not client_success:
|
||||||
|
if self.server.staleClients() or not client:
|
||||||
|
log.info('Failed to send notification to client "%s". '
|
||||||
|
'Client list is stale, updating the client list and retrying.', client_name)
|
||||||
|
self.server.updateClients(self.getClientNames())
|
||||||
|
else:
|
||||||
|
log.warning('Failed to send notification to client %s, skipping this time', client_name)
|
||||||
|
success = False
|
||||||
|
|
||||||
def notify(self, message = '', data = None, listener = None):
|
return success
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
hosts = self.getHosts(port = 3000)
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
successful = 0
|
return self.notifyClients(message, self.getClientNames())
|
||||||
for host in hosts:
|
|
||||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
|
|
||||||
successful += 1
|
|
||||||
|
|
||||||
return successful == len(hosts)
|
|
||||||
|
|
||||||
def send(self, command, host):
|
|
||||||
|
|
||||||
url = '%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
|
||||||
headers = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.urlopen(url, headers = headers, show_error = False)
|
|
||||||
except URLError:
|
|
||||||
log.error("Couldn't sent command to Plex, probably just running Media Server")
|
|
||||||
return False
|
|
||||||
except:
|
|
||||||
log.error("Couldn't sent command to Plex: %s", traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.info('Plex notification to %s successful.', host)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def test(self, **kwargs):
|
def test(self, **kwargs):
|
||||||
|
|
||||||
@@ -80,28 +66,12 @@ class Plex(Notification):
|
|||||||
|
|
||||||
log.info('Sending test to %s', test_type)
|
log.info('Sending test to %s', test_type)
|
||||||
|
|
||||||
success = self.notify(
|
notify_success = self.notify(
|
||||||
message = self.test_message,
|
message = self.test_message,
|
||||||
data = {},
|
data = {},
|
||||||
listener = 'test'
|
listener = 'test'
|
||||||
)
|
)
|
||||||
success2 = self.addToLibrary()
|
|
||||||
|
|
||||||
return {
|
refresh_success = self.addToLibrary()
|
||||||
'success': success or success2
|
|
||||||
}
|
|
||||||
|
|
||||||
def getHosts(self, port = None):
|
return {'success': notify_success or refresh_success}
|
||||||
|
|
||||||
raw_hosts = splitString(self.conf('host'))
|
|
||||||
hosts = []
|
|
||||||
|
|
||||||
for h in raw_hosts:
|
|
||||||
h = cleanHost(h)
|
|
||||||
p = urlparse(h)
|
|
||||||
h = h.rstrip('/')
|
|
||||||
if port and not p.port:
|
|
||||||
h += ':%s' % port
|
|
||||||
hosts.append(h)
|
|
||||||
|
|
||||||
return hosts
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
from datetime import timedelta, datetime
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost
|
||||||
|
from couchpotato import CPLog
|
||||||
|
from urlparse import urlparse
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import xml.etree.cElementTree as etree
|
||||||
|
except ImportError:
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PlexServer(object):
|
||||||
|
def __init__(self, plex):
|
||||||
|
self.plex = plex
|
||||||
|
|
||||||
|
self.clients = {}
|
||||||
|
self.last_clients_update = None
|
||||||
|
|
||||||
|
def staleClients(self):
|
||||||
|
if not self.last_clients_update:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return self.last_clients_update + timedelta(minutes=15) < datetime.now()
|
||||||
|
|
||||||
|
def request(self, path, data_type='xml'):
|
||||||
|
if not self.plex.conf('media_server'):
|
||||||
|
log.warning("Plex media server hostname is required")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if path.startswith('/'):
|
||||||
|
path = path[1:]
|
||||||
|
|
||||||
|
data = self.plex.urlopen('%s/%s' % (
|
||||||
|
self.createHost(self.plex.conf('media_server'), port = 32400),
|
||||||
|
path
|
||||||
|
))
|
||||||
|
|
||||||
|
if data_type == 'xml':
|
||||||
|
return etree.fromstring(data)
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
|
def updateClients(self, client_names):
|
||||||
|
log.info('Searching for clients on Plex Media Server')
|
||||||
|
|
||||||
|
self.clients = {}
|
||||||
|
|
||||||
|
result = self.request('clients')
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
found_clients = [
|
||||||
|
c for c in result.findall('Server')
|
||||||
|
if c.get('name') and c.get('name').lower() in client_names
|
||||||
|
]
|
||||||
|
|
||||||
|
# Store client details in cache
|
||||||
|
for client in found_clients:
|
||||||
|
name = client.get('name').lower()
|
||||||
|
|
||||||
|
self.clients[name] = {
|
||||||
|
'name': client.get('name'),
|
||||||
|
'found': True,
|
||||||
|
'address': client.get('address'),
|
||||||
|
'port': client.get('port'),
|
||||||
|
'protocol': client.get('protocol', 'xbmchttp')
|
||||||
|
}
|
||||||
|
|
||||||
|
client_names.remove(name)
|
||||||
|
|
||||||
|
# Store dummy info for missing clients
|
||||||
|
for client_name in client_names:
|
||||||
|
self.clients[client_name] = {
|
||||||
|
'found': False
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(client_names) > 0:
|
||||||
|
log.debug('Unable to find clients: %s', ', '.join(client_names))
|
||||||
|
|
||||||
|
self.last_clients_update = datetime.now()
|
||||||
|
|
||||||
|
def refresh(self, section_types=None):
|
||||||
|
if not section_types:
|
||||||
|
section_types = ['movie']
|
||||||
|
|
||||||
|
sections = self.request('library/sections')
|
||||||
|
|
||||||
|
try:
|
||||||
|
for section in sections.findall('Directory'):
|
||||||
|
if section.get('type') not in section_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.request('library/sections/%s/refresh' % section.get('key'), 'text')
|
||||||
|
except:
|
||||||
|
log.error('Plex library update failed for %s, Media Server not running: %s',
|
||||||
|
(self.plex.conf('media_server'), traceback.format_exc(1)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def createHost(self, host, port = None):
|
||||||
|
|
||||||
|
h = cleanHost(host)
|
||||||
|
p = urlparse(h)
|
||||||
|
h = h.rstrip('/')
|
||||||
|
|
||||||
|
if port and not p.port:
|
||||||
|
h += ':%s' % port
|
||||||
|
|
||||||
|
return h
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from .main import Xmpp
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Xmpp()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'xmpp',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
|
'name': 'xmpp',
|
||||||
|
'label': 'XMPP',
|
||||||
|
'description`': 'for Jabber, Hangouts (Google Talk), AIM...',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'description': 'User sending the message. For Hangouts, e-mail of a single-step authentication Google account.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'Password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'hostname',
|
||||||
|
'default': 'talk.google.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'to',
|
||||||
|
'description': 'Username (or e-mail for Hangouts) of the person to send the messages to.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'port',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 5222,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'on_snatch',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also send message when movie is snatched.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
from time import sleep
|
||||||
|
import traceback
|
||||||
|
import xmpp
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Xmpp(Notification):
|
||||||
|
|
||||||
|
def notify(self, message = '', data = None, listener = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
jid = xmpp.protocol.JID(self.conf('username'))
|
||||||
|
client = xmpp.Client(jid.getDomain(), debug = [])
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
if not client.connect(server = (self.conf('hostname'), self.conf('port'))):
|
||||||
|
log.error('XMPP failed: Connection to server failed.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
if not client.auth(jid.getNode(), self.conf('password'), resource = jid.getResource()):
|
||||||
|
log.error('XMPP failed: Failed to authenticate.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Send message
|
||||||
|
client.send(xmpp.protocol.Message(to = self.conf('to'), body = message, typ = 'chat'))
|
||||||
|
|
||||||
|
# Disconnect
|
||||||
|
# some older servers will not send the message if you disconnect immediately after sending
|
||||||
|
sleep(1)
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
log.info('XMPP notifications sent.')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('XMPP failed: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
|
||||||
toUnicode
|
toUnicode, sp
|
||||||
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP
|
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
@@ -121,7 +121,7 @@ class Plugin(object):
|
|||||||
|
|
||||||
# http request
|
# http request
|
||||||
def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
|
def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
|
||||||
url = ss(url)
|
url = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
|
||||||
|
|
||||||
if not headers: headers = {}
|
if not headers: headers = {}
|
||||||
if not params: params = {}
|
if not params: params = {}
|
||||||
@@ -291,10 +291,10 @@ class Plugin(object):
|
|||||||
|
|
||||||
def createNzbName(self, data, movie):
|
def createNzbName(self, data, movie):
|
||||||
tag = self.cpTag(movie)
|
tag = self.cpTag(movie)
|
||||||
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
|
return '%s%s' % (toSafeString(toUnicode(data.get('name'))[:127 - len(tag)]), tag)
|
||||||
|
|
||||||
def createFileName(self, data, filedata, movie):
|
def createFileName(self, data, filedata, movie):
|
||||||
name = os.path.join(self.createNzbName(data, movie))
|
name = sp(os.path.join(self.createNzbName(data, movie)))
|
||||||
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||||
return '%s.%s' % (name, 'rar')
|
return '%s.%s' % (name, 'rar')
|
||||||
return '%s.%s' % (name, data.get('protocol'))
|
return '%s.%s' % (name, data.get('protocol'))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie, Category
|
from couchpotato.core.settings.model import Media, Category
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ class CategoryPlugin(Plugin):
|
|||||||
def removeFromMovie(self, category_id):
|
def removeFromMovie(self, category_id):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
movies = db.query(Movie).filter(Movie.category_id == category_id).all()
|
movies = db.query(Media).filter(Media.category_id == category_id).all()
|
||||||
|
|
||||||
if len(movies) > 0:
|
if len(movies) > 0:
|
||||||
for movie in movies:
|
for movie in movies:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import Custom
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Custom()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
import os
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Custom(Plugin):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
addEvent('app.load', self.createStructure)
|
||||||
|
|
||||||
|
def createStructure(self):
|
||||||
|
|
||||||
|
custom_dir = os.path.join(Env.get('data_dir'), 'custom_plugins')
|
||||||
|
|
||||||
|
if not os.path.isdir(custom_dir):
|
||||||
|
self.makeDir(custom_dir)
|
||||||
|
self.createFile(os.path.join(custom_dir, '__init__.py'), '# Don\'t remove this file')
|
||||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import fireEvent
|
|||||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie, Library, LibraryTitle, \
|
from couchpotato.core.settings.model import Media, Library, LibraryTitle, \
|
||||||
Release
|
Release
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
from sqlalchemy.sql.expression import asc, or_
|
from sqlalchemy.sql.expression import asc, or_
|
||||||
@@ -50,12 +50,12 @@ class Dashboard(Plugin):
|
|||||||
|
|
||||||
# Get all active movies
|
# Get all active movies
|
||||||
active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True)
|
active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True)
|
||||||
q = db.query(Movie) \
|
q = db.query(Media) \
|
||||||
.join(Library) \
|
.join(Library) \
|
||||||
.outerjoin(Movie.releases) \
|
.outerjoin(Media.releases) \
|
||||||
.filter(Movie.status_id == active_status.get('id')) \
|
.filter(Media.status_id == active_status.get('id')) \
|
||||||
.with_entities(Movie.id, Movie.profile_id, Library.info, Library.year) \
|
.with_entities(Media.id, Media.profile_id, Library.info, Library.year) \
|
||||||
.group_by(Movie.id) \
|
.group_by(Media.id) \
|
||||||
.filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
|
.filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
|
||||||
|
|
||||||
if not random:
|
if not random:
|
||||||
@@ -101,11 +101,11 @@ class Dashboard(Plugin):
|
|||||||
if len(movie_ids) > 0:
|
if len(movie_ids) > 0:
|
||||||
|
|
||||||
# Get all movie information
|
# Get all movie information
|
||||||
movies_raw = db.query(Movie) \
|
movies_raw = db.query(Media) \
|
||||||
.options(joinedload_all('library.titles')) \
|
.options(joinedload_all('library.titles')) \
|
||||||
.options(joinedload_all('library.files')) \
|
.options(joinedload_all('library.files')) \
|
||||||
.options(joinedload_all('files')) \
|
.options(joinedload_all('files')) \
|
||||||
.filter(Movie.id.in_(movie_ids)) \
|
.filter(Media.id.in_(movie_ids)) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
# Create dict by movie id
|
# Create dict by movie id
|
||||||
|
|||||||
@@ -222,9 +222,10 @@ class Manage(Plugin):
|
|||||||
|
|
||||||
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
|
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
|
||||||
|
|
||||||
for group in groups.itervalues():
|
if groups:
|
||||||
if group['library'] and group['library'].get('identifier'):
|
for group in groups.itervalues():
|
||||||
fireEvent('release.add', group = group)
|
if group['library'] and group['library'].get('identifier'):
|
||||||
|
fireEvent('release.add', group = group)
|
||||||
|
|
||||||
def getDiskSpace(self):
|
def getDiskSpace(self):
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent, fireEvent
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Profile, ProfileType, Movie
|
from couchpotato.core.settings.model import Profile, ProfileType, Media
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -38,7 +38,7 @@ class ProfilePlugin(Plugin):
|
|||||||
active_status = fireEvent('status.get', 'active', single = True)
|
active_status = fireEvent('status.get', 'active', single = True)
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all()
|
movies = db.query(Media).filter(Media.status_id == active_status.get('id'), Media.profile == None).all()
|
||||||
|
|
||||||
if len(movies) > 0:
|
if len(movies) > 0:
|
||||||
default_profile = self.default()
|
default_profile = self.default()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||||
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
@@ -17,12 +17,12 @@ class QualityPlugin(Plugin):
|
|||||||
|
|
||||||
qualities = [
|
qualities = [
|
||||||
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
||||||
{'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
{'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']},
|
||||||
{'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
{'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']},
|
||||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
|
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
|
||||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]},
|
||||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
|
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
|
||||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
@@ -30,6 +30,9 @@ class QualityPlugin(Plugin):
|
|||||||
]
|
]
|
||||||
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
||||||
|
|
||||||
|
cached_qualities = None
|
||||||
|
cached_order = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('quality.all', self.all)
|
addEvent('quality.all', self.all)
|
||||||
addEvent('quality.single', self.single)
|
addEvent('quality.single', self.single)
|
||||||
@@ -59,6 +62,9 @@ class QualityPlugin(Plugin):
|
|||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
|
|
||||||
|
if self.cached_qualities:
|
||||||
|
return self.cached_qualities
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
qualities = db.query(Quality).all()
|
qualities = db.query(Quality).all()
|
||||||
@@ -68,6 +74,7 @@ class QualityPlugin(Plugin):
|
|||||||
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
|
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
|
||||||
temp.append(q)
|
temp.append(q)
|
||||||
|
|
||||||
|
self.cached_qualities = temp
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
def single(self, identifier = ''):
|
def single(self, identifier = ''):
|
||||||
@@ -96,6 +103,8 @@ class QualityPlugin(Plugin):
|
|||||||
setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
|
setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
self.cached_qualities = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
}
|
}
|
||||||
@@ -161,68 +170,118 @@ class QualityPlugin(Plugin):
|
|||||||
if cached and len(extra) == 0: return cached
|
if cached and len(extra) == 0: return cached
|
||||||
|
|
||||||
qualities = self.all()
|
qualities = self.all()
|
||||||
|
|
||||||
|
# Start with 0
|
||||||
|
score = {}
|
||||||
|
for quality in qualities:
|
||||||
|
score[quality.get('identifier')] = 0
|
||||||
|
|
||||||
for cur_file in files:
|
for cur_file in files:
|
||||||
words = re.split('\W+', cur_file.lower())
|
words = re.split('\W+', cur_file.lower())
|
||||||
|
|
||||||
found = {}
|
|
||||||
for quality in qualities:
|
for quality in qualities:
|
||||||
contains = self.containsTag(quality, words, cur_file)
|
contains_score = self.containsTagScore(quality, words, cur_file)
|
||||||
if contains:
|
self.calcScore(score, quality, contains_score)
|
||||||
found[quality['identifier']] = True
|
|
||||||
|
|
||||||
for quality in qualities:
|
|
||||||
|
|
||||||
# Check identifier
|
|
||||||
if quality['identifier'] in words:
|
|
||||||
if len(found) == 0 or len(found) == 1 and found.get(quality['identifier']):
|
|
||||||
log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
|
|
||||||
return self.setCache(cache_key, quality)
|
|
||||||
|
|
||||||
# Check alt and tags
|
|
||||||
contains = self.containsTag(quality, words, cur_file)
|
|
||||||
if contains:
|
|
||||||
return self.setCache(cache_key, quality)
|
|
||||||
|
|
||||||
# Try again with loose testing
|
# Try again with loose testing
|
||||||
quality = self.guessLoose(cache_key, files = files, extra = extra)
|
for quality in qualities:
|
||||||
if quality:
|
loose_score = self.guessLooseScore(quality, files = files, extra = extra)
|
||||||
return self.setCache(cache_key, quality)
|
self.calcScore(score, quality, loose_score)
|
||||||
|
|
||||||
|
|
||||||
|
# Return nothing if all scores are 0
|
||||||
|
has_non_zero = 0
|
||||||
|
for s in score:
|
||||||
|
if score[s] > 0:
|
||||||
|
has_non_zero += 1
|
||||||
|
|
||||||
|
if not has_non_zero:
|
||||||
|
return None
|
||||||
|
|
||||||
|
heighest_quality = max(score, key = score.get)
|
||||||
|
if heighest_quality:
|
||||||
|
for quality in qualities:
|
||||||
|
if quality.get('identifier') == heighest_quality:
|
||||||
|
return self.setCache(cache_key, quality)
|
||||||
|
|
||||||
log.debug('Could not identify quality for: %s', files)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def containsTag(self, quality, words, cur_file = ''):
|
def containsTagScore(self, quality, words, cur_file = ''):
|
||||||
|
cur_file = ss(cur_file)
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
points = {
|
||||||
|
'identifier': 10,
|
||||||
|
'label': 10,
|
||||||
|
'alternative': 9,
|
||||||
|
'tags': 9,
|
||||||
|
'ext': 3,
|
||||||
|
}
|
||||||
|
|
||||||
# Check alt and tags
|
# Check alt and tags
|
||||||
for tag_type in ['alternative', 'tags']:
|
for tag_type in ['identifier', 'alternative', 'tags', 'label']:
|
||||||
for alt in quality.get(tag_type, []):
|
qualities = quality.get(tag_type, [])
|
||||||
if isinstance(alt, tuple) and '.'.join(alt) in '.'.join(words):
|
qualities = [qualities] if isinstance(qualities, (str, unicode)) else qualities
|
||||||
|
|
||||||
|
for alt in qualities:
|
||||||
|
if (isinstance(alt, tuple)):
|
||||||
|
if len(set(words) & set(alt)) == len(alt):
|
||||||
|
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||||
|
score += points.get(tag_type)
|
||||||
|
elif len(set(words) & set(alt)) > 0:
|
||||||
|
partial = list(set(words) & set(alt))[0]
|
||||||
|
if len(partial) > 2:
|
||||||
|
log.debug('Found %s via partial %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||||
|
score += points.get(tag_type) / 3
|
||||||
|
|
||||||
|
if (isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower()):
|
||||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||||
return True
|
score += points.get(tag_type) / 2
|
||||||
|
|
||||||
if list(set(quality.get(tag_type, [])) & set(words)):
|
if list(set(qualities) & set(words)):
|
||||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||||
return True
|
score += points.get(tag_type)
|
||||||
|
|
||||||
return
|
# Check extention
|
||||||
|
for ext in quality.get('ext', []):
|
||||||
|
if ext == words[-1]:
|
||||||
|
log.debug('Found %s extension in %s', (ext, cur_file))
|
||||||
|
score += points['ext']
|
||||||
|
|
||||||
def guessLoose(self, cache_key, files = None, extra = None):
|
return score
|
||||||
|
|
||||||
|
def guessLooseScore(self, quality, files = None, extra = None):
|
||||||
|
|
||||||
|
score = 0
|
||||||
|
|
||||||
if extra:
|
if extra:
|
||||||
for quality in self.all():
|
|
||||||
|
|
||||||
# Check width resolution, range 20
|
# Check width resolution, range 20
|
||||||
if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
|
if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
|
||||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
|
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
|
||||||
return self.setCache(cache_key, quality)
|
score += 5
|
||||||
|
|
||||||
# Check height resolution, range 20
|
# Check height resolution, range 20
|
||||||
if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
|
if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
|
||||||
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
|
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
|
||||||
return self.setCache(cache_key, quality)
|
score += 5
|
||||||
|
|
||||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
if quality.get('identifier') == 'dvdrip' and 480 <= extra.get('resolution_width', 0) <= 720:
|
||||||
log.debug('Found as dvdrip')
|
log.debug('Add point for correct dvdrip resolutions')
|
||||||
return self.setCache(cache_key, self.single('dvdrip'))
|
score += 1
|
||||||
|
|
||||||
return None
|
return score
|
||||||
|
|
||||||
|
def calcScore(self, score, quality, add_score):
|
||||||
|
|
||||||
|
score[quality['identifier']] += add_score
|
||||||
|
|
||||||
|
# Set order for allow calculation (and cache)
|
||||||
|
if not self.cached_order:
|
||||||
|
self.cached_order = {}
|
||||||
|
for q in self.qualities:
|
||||||
|
self.cached_order[q.get('identifier')] = self.qualities.index(q)
|
||||||
|
|
||||||
|
if add_score != 0:
|
||||||
|
for allow in quality.get('allow', []):
|
||||||
|
score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session, md5
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss, toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import getTitle
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.plugins.scanner.main import Scanner
|
from couchpotato.core.plugins.scanner.main import Scanner
|
||||||
from couchpotato.core.settings.model import File, Release as Relea, Movie
|
from couchpotato.core.settings.model import File, Release as Relea, Media, \
|
||||||
|
ReleaseInfo
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from inspect import ismethod, isfunction
|
||||||
|
from sqlalchemy.exc import InterfaceError
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -19,7 +25,7 @@ class Release(Plugin):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('release.add', self.add)
|
addEvent('release.add', self.add)
|
||||||
|
|
||||||
addApiView('release.download', self.download, docs = {
|
addApiView('release.manual_download', self.manualDownload, docs = {
|
||||||
'desc': 'Send a release manually to the downloaders',
|
'desc': 'Send a release manually to the downloaders',
|
||||||
'params': {
|
'params': {
|
||||||
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
|
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
|
||||||
@@ -44,9 +50,45 @@ class Release(Plugin):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addEvent('release.download', self.download)
|
||||||
|
addEvent('release.try_download_result', self.tryDownloadResult)
|
||||||
|
addEvent('release.create_from_search', self.createFromSearch)
|
||||||
addEvent('release.for_movie', self.forMovie)
|
addEvent('release.for_movie', self.forMovie)
|
||||||
addEvent('release.delete', self.delete)
|
addEvent('release.delete', self.delete)
|
||||||
addEvent('release.clean', self.clean)
|
addEvent('release.clean', self.clean)
|
||||||
|
addEvent('release.update_status', self.updateStatus)
|
||||||
|
|
||||||
|
# Clean releases that didn't have activity in the last week
|
||||||
|
addEvent('app.load', self.cleanDone)
|
||||||
|
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 4)
|
||||||
|
|
||||||
|
def cleanDone(self):
|
||||||
|
|
||||||
|
log.debug('Removing releases from dashboard')
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
week = 262080
|
||||||
|
|
||||||
|
done_status, available_status, snatched_status, downloaded_status, ignored_status = \
|
||||||
|
fireEvent('status.get', ['done', 'available', 'snatched', 'downloaded', 'ignored'], single = True)
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
# get movies last_edit more than a week ago
|
||||||
|
media = db.query(Media) \
|
||||||
|
.filter(Media.status_id == done_status.get('id'), Media.last_edit < (now - week)) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
for item in media:
|
||||||
|
for rel in item.releases:
|
||||||
|
# Remove all available releases
|
||||||
|
if rel.status_id in [available_status.get('id')]:
|
||||||
|
fireEvent('release.delete', id = rel.id, single = True)
|
||||||
|
# Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move
|
||||||
|
elif rel.status_id in [snatched_status.get('id'), downloaded_status.get('id')]:
|
||||||
|
self.updateStatus(id = rel.id, status = ignored_status)
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
def add(self, group):
|
def add(self, group):
|
||||||
|
|
||||||
@@ -58,9 +100,9 @@ class Release(Plugin):
|
|||||||
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
|
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
|
||||||
|
|
||||||
# Add movie
|
# Add movie
|
||||||
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
|
movie = db.query(Media).filter_by(library_id = group['library'].get('id')).first()
|
||||||
if not movie:
|
if not movie:
|
||||||
movie = Movie(
|
movie = Media(
|
||||||
library_id = group['library'].get('id'),
|
library_id = group['library'].get('id'),
|
||||||
profile_id = 0,
|
profile_id = 0,
|
||||||
status_id = done_status.get('id')
|
status_id = done_status.get('id')
|
||||||
@@ -104,7 +146,6 @@ class Release(Plugin):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def saveFile(self, filepath, type = 'unknown', include_media_info = False):
|
def saveFile(self, filepath, type = 'unknown', include_media_info = False):
|
||||||
|
|
||||||
properties = {}
|
properties = {}
|
||||||
@@ -159,26 +200,23 @@ class Release(Plugin):
|
|||||||
rel = db.query(Relea).filter_by(id = id).first()
|
rel = db.query(Relea).filter_by(id = id).first()
|
||||||
if rel:
|
if rel:
|
||||||
ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
|
ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
|
||||||
rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
|
self.updateStatus(id, available_status if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status)
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
}
|
}
|
||||||
|
|
||||||
def download(self, id = None, **kwargs):
|
def manualDownload(self, id = None, **kwargs):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
|
|
||||||
|
|
||||||
rel = db.query(Relea).filter_by(id = id).first()
|
rel = db.query(Relea).filter_by(id = id).first()
|
||||||
if rel:
|
if rel:
|
||||||
item = {}
|
item = {}
|
||||||
for info in rel.info:
|
for info in rel.info:
|
||||||
item[info.identifier] = info.value
|
item[info.identifier] = info.value
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name'])
|
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Snatching "%s"' % item['name'])
|
||||||
|
|
||||||
# Get matching provider
|
# Get matching provider
|
||||||
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
||||||
@@ -190,23 +228,18 @@ class Release(Plugin):
|
|||||||
if item.get('protocol') != 'torrent_magnet':
|
if item.get('protocol') != 'torrent_magnet':
|
||||||
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
||||||
|
|
||||||
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
|
success = self.download(data = item, media = rel.movie.to_dict({
|
||||||
'profile': {'types': {'quality': {}}},
|
'profile': {'types': {'quality': {}}},
|
||||||
'releases': {'status': {}, 'quality': {}},
|
'releases': {'status': {}, 'quality': {}},
|
||||||
'library': {'titles': {}, 'files':{}},
|
'library': {'titles': {}, 'files':{}},
|
||||||
'files': {}
|
'files': {}
|
||||||
}), manual = True, single = True)
|
}), manual = True)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
db.expunge_all()
|
db.expunge_all()
|
||||||
rel = db.query(Relea).filter_by(id = id).first() # Get release again
|
rel = db.query(Relea).filter_by(id = id).first() # Get release again @RuudBurger why do we need to get it again??
|
||||||
|
|
||||||
if rel.status_id != done_status.get('id'):
|
|
||||||
rel.status_id = snatched_status.get('id')
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
|
||||||
|
|
||||||
|
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
||||||
return {
|
return {
|
||||||
'success': success
|
'success': success
|
||||||
}
|
}
|
||||||
@@ -217,6 +250,152 @@ class Release(Plugin):
|
|||||||
'success': False
|
'success': False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def download(self, data, media, manual = False):
|
||||||
|
|
||||||
|
if not data.get('protocol'):
|
||||||
|
data['protocol'] = data['type']
|
||||||
|
data['type'] = 'movie'
|
||||||
|
|
||||||
|
# Test to see if any downloaders are enabled for this type
|
||||||
|
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
|
||||||
|
|
||||||
|
if downloader_enabled:
|
||||||
|
snatched_status, done_status, active_status = fireEvent('status.get', ['snatched', 'done', 'active'], single = True)
|
||||||
|
|
||||||
|
# Download release to temp
|
||||||
|
filedata = None
|
||||||
|
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||||
|
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||||
|
if filedata == 'try_next':
|
||||||
|
return filedata
|
||||||
|
|
||||||
|
download_result = fireEvent('download', data = data, movie = media, manual = manual, filedata = filedata, single = True)
|
||||||
|
log.debug('Downloader result: %s', download_result)
|
||||||
|
|
||||||
|
if download_result:
|
||||||
|
try:
|
||||||
|
# Mark release as snatched
|
||||||
|
db = get_session()
|
||||||
|
rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first()
|
||||||
|
if rls:
|
||||||
|
renamer_enabled = Env.setting('enabled', 'renamer')
|
||||||
|
|
||||||
|
# Save download-id info if returned
|
||||||
|
if isinstance(download_result, dict):
|
||||||
|
for key in download_result:
|
||||||
|
rls_info = ReleaseInfo(
|
||||||
|
identifier = 'download_%s' % key,
|
||||||
|
value = toUnicode(download_result.get(key))
|
||||||
|
)
|
||||||
|
rls.info.append(rls_info)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
log_movie = '%s (%s) in %s' % (getTitle(media['library']), media['library']['year'], rls.quality.label)
|
||||||
|
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||||
|
log.info(snatch_message)
|
||||||
|
fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls.to_dict())
|
||||||
|
|
||||||
|
# If renamer isn't used, mark media done
|
||||||
|
if not renamer_enabled:
|
||||||
|
try:
|
||||||
|
if media['status_id'] == active_status.get('id'):
|
||||||
|
for profile_type in media['profile']['types']:
|
||||||
|
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||||
|
log.info('Renamer disabled, marking media as finished: %s', log_movie)
|
||||||
|
|
||||||
|
# Mark release done
|
||||||
|
self.updateStatus(rls.id, status = done_status)
|
||||||
|
|
||||||
|
# Mark media done
|
||||||
|
mdia = db.query(Media).filter_by(id = media['id']).first()
|
||||||
|
mdia.status_id = done_status.get('id')
|
||||||
|
mdia.last_edit = int(time.time())
|
||||||
|
db.commit()
|
||||||
|
except:
|
||||||
|
log.error('Failed marking media finished, renamer disabled: %s', traceback.format_exc())
|
||||||
|
else:
|
||||||
|
self.updateStatus(rls.id, status = snatched_status)
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed marking media finished: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('protocol')))
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def tryDownloadResult(self, results, media, quality_type, manual = False):
|
||||||
|
ignored_status, failed_status = fireEvent('status.get', ['ignored', 'failed'], single = True)
|
||||||
|
|
||||||
|
for rel in results:
|
||||||
|
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and rel.get('age') <= quality_type.get('wait_for', 0):
|
||||||
|
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), rel['name']))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rel['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
||||||
|
log.info('Ignored: %s', rel['name'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rel['score'] <= 0:
|
||||||
|
log.info('Ignored, score to low: %s', rel['name'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
downloaded = fireEvent('release.download', data = rel, media = media, manual = manual, single = True)
|
||||||
|
if downloaded is True:
|
||||||
|
return True
|
||||||
|
elif downloaded != 'try_next':
|
||||||
|
break
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def createFromSearch(self, search_results, media, quality_type):
|
||||||
|
|
||||||
|
available_status = fireEvent('status.get', ['available'], single = True)
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
found_releases = []
|
||||||
|
|
||||||
|
for rel in search_results:
|
||||||
|
|
||||||
|
rel_identifier = md5(rel['url'])
|
||||||
|
found_releases.append(rel_identifier)
|
||||||
|
|
||||||
|
rls = db.query(Relea).filter_by(identifier = rel_identifier).first()
|
||||||
|
if not rls:
|
||||||
|
rls = Relea(
|
||||||
|
identifier = rel_identifier,
|
||||||
|
movie_id = media.get('id'),
|
||||||
|
#media_id = media.get('id'),
|
||||||
|
quality_id = quality_type.get('quality_id'),
|
||||||
|
status_id = available_status.get('id')
|
||||||
|
)
|
||||||
|
db.add(rls)
|
||||||
|
else:
|
||||||
|
[db.delete(old_info) for old_info in rls.info]
|
||||||
|
rls.last_edit = int(time.time())
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
for info in rel:
|
||||||
|
try:
|
||||||
|
if not isinstance(rel[info], (str, unicode, int, long, float)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rls_info = ReleaseInfo(
|
||||||
|
identifier = info,
|
||||||
|
value = toUnicode(rel[info])
|
||||||
|
)
|
||||||
|
rls.info.append(rls_info)
|
||||||
|
except InterfaceError:
|
||||||
|
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
rel['status_id'] = rls.status_id
|
||||||
|
|
||||||
|
return found_releases
|
||||||
|
|
||||||
def forMovie(self, id = None):
|
def forMovie(self, id = None):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
@@ -241,3 +420,32 @@ class Release(Plugin):
|
|||||||
'success': True
|
'success': True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def updateStatus(self, id, status = None):
|
||||||
|
if not status: return False
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
rel = db.query(Relea).filter_by(id = id).first()
|
||||||
|
if rel and status and rel.status_id != status.get('id'):
|
||||||
|
|
||||||
|
item = {}
|
||||||
|
for info in rel.info:
|
||||||
|
item[info.identifier] = info.value
|
||||||
|
|
||||||
|
if rel.files:
|
||||||
|
for file_item in rel.files:
|
||||||
|
if file_item.type.identifier == 'movie':
|
||||||
|
release_name = os.path.basename(file_item.path)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
release_name = item['name']
|
||||||
|
#update status in Db
|
||||||
|
log.debug('Marking release %s as %s', (release_name, status.get("label")))
|
||||||
|
rel.status_id = status.get('id')
|
||||||
|
rel.last_edit = int(time.time())
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
#Update all movie info as there is no release update function
|
||||||
|
fireEvent('notify.frontend', type = 'release.update_status.%s' % rel.id, data = status.get('id'))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ rename_options = {
|
|||||||
'cd': 'CD number (cd1)',
|
'cd': 'CD number (cd1)',
|
||||||
'cd_nr': 'Just the cd nr. (1)',
|
'cd_nr': 'Just the cd nr. (1)',
|
||||||
'mpaa': 'MPAA Rating',
|
'mpaa': 'MPAA Rating',
|
||||||
|
'category': 'Category label',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
from couchpotato.core.helpers.encoding import toUnicode, ss, sp
|
||||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||||
getImdb, link, symlink, tryInt
|
getImdb, link, symlink, tryInt, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
||||||
@@ -31,8 +31,9 @@ class Renamer(Plugin):
|
|||||||
'params': {
|
'params': {
|
||||||
'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
|
'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
|
||||||
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
|
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
|
||||||
'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'},
|
'downloader' : {'desc': 'Optional: The downloader the release has been downloaded with. \'download_id\' is required with this option.'},
|
||||||
'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'},
|
'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in movie_folder. \'downloader\' is required with this option.'},
|
||||||
|
'status': {'desc': 'Optional: The status of the release: \'completed\' (default) or \'seeding\''},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -62,23 +63,24 @@ class Renamer(Plugin):
|
|||||||
def scanView(self, **kwargs):
|
def scanView(self, **kwargs):
|
||||||
|
|
||||||
async = tryInt(kwargs.get('async', 0))
|
async = tryInt(kwargs.get('async', 0))
|
||||||
movie_folder = kwargs.get('movie_folder')
|
movie_folder = sp(kwargs.get('movie_folder'))
|
||||||
downloader = kwargs.get('downloader')
|
downloader = kwargs.get('downloader')
|
||||||
download_id = kwargs.get('download_id')
|
download_id = kwargs.get('download_id')
|
||||||
|
status = kwargs.get('status', 'completed')
|
||||||
|
|
||||||
download_info = {'folder': movie_folder} if movie_folder else None
|
release_download = {'folder': movie_folder} if movie_folder else None
|
||||||
if download_info:
|
if release_download:
|
||||||
download_info.update({'id': download_id, 'downloader': downloader} if download_id else {})
|
release_download.update({'id': download_id, 'downloader': downloader, 'status': status} if download_id else {})
|
||||||
|
|
||||||
fire_handle = fireEvent if not async else fireEventAsync
|
fire_handle = fireEvent if not async else fireEventAsync
|
||||||
|
|
||||||
fire_handle('renamer.scan', download_info)
|
fire_handle('renamer.scan', release_download)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
}
|
}
|
||||||
|
|
||||||
def scan(self, download_info = None):
|
def scan(self, release_download = None):
|
||||||
|
|
||||||
if self.isDisabled():
|
if self.isDisabled():
|
||||||
return
|
return
|
||||||
@@ -87,22 +89,66 @@ class Renamer(Plugin):
|
|||||||
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
|
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
|
||||||
return
|
return
|
||||||
|
|
||||||
movie_folder = download_info and download_info.get('folder')
|
from_folder = sp(self.conf('from'))
|
||||||
|
to_folder = sp(self.conf('to'))
|
||||||
|
|
||||||
# Check to see if the "to" folder is inside the "from" folder.
|
# Get movie folder to process
|
||||||
if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
|
movie_folder = release_download and release_download.get('folder')
|
||||||
l = log.debug if movie_folder else log.error
|
|
||||||
l('Both the "To" and "From" have to exist.')
|
# Get all folders that should not be processed
|
||||||
return
|
no_process = [to_folder]
|
||||||
elif self.conf('from') in self.conf('to'):
|
cat_list = fireEvent('category.all', single = True) or []
|
||||||
log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.')
|
no_process.extend([item['destination'] for item in cat_list])
|
||||||
return
|
try:
|
||||||
elif movie_folder and movie_folder in [self.conf('to'), self.conf('from')]:
|
if Env.setting('library', section = 'manage').strip():
|
||||||
log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.')
|
no_process.extend(splitString(Env.setting('library', section = 'manage'), '::'))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check to see if the no_process folders are inside the "from" folder.
|
||||||
|
if not os.path.isdir(from_folder) or not os.path.isdir(to_folder):
|
||||||
|
log.error('Both the "To" and "From" have to exist.')
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
for item in no_process:
|
||||||
|
if from_folder in item:
|
||||||
|
log.error('To protect your data, the movie libraries can\'t be inside of or the same as the "from" folder.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check to see if the no_process folders are inside the provided movie_folder
|
||||||
|
if movie_folder and not os.path.isdir(movie_folder):
|
||||||
|
log.debug('The provided movie folder %s does not exist. Trying to find it in the \'from\' folder.', movie_folder)
|
||||||
|
|
||||||
|
# Update to the from folder
|
||||||
|
if len(release_download.get('files')) == 1:
|
||||||
|
new_movie_folder = from_folder
|
||||||
|
else:
|
||||||
|
new_movie_folder = sp(os.path.join(from_folder, os.path.basename(movie_folder)))
|
||||||
|
|
||||||
|
if not os.path.isdir(new_movie_folder):
|
||||||
|
log.error('The provided movie folder %s does not exist and could also not be found in the \'from\' folder.', movie_folder)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update the files
|
||||||
|
new_files = [os.path.join(new_movie_folder, os.path.relpath(filename, movie_folder)) for filename in splitString(release_download.get('files'), '|')]
|
||||||
|
if new_files and not os.path.isfile(new_files[0]):
|
||||||
|
log.error('The provided movie folder %s does not exist and its files could also not be found in the \'from\' folder.', movie_folder)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update release_download info to the from folder
|
||||||
|
log.debug('Release %s found in the \'from\' folder.', movie_folder)
|
||||||
|
release_download['folder'] = new_movie_folder
|
||||||
|
release_download['files'] = '|'.join(new_files)
|
||||||
|
movie_folder = new_movie_folder
|
||||||
|
|
||||||
|
if movie_folder:
|
||||||
|
for item in no_process:
|
||||||
|
if movie_folder in item:
|
||||||
|
log.error('To protect your data, the movie libraries can\'t be inside of or the same as the provided movie folder.')
|
||||||
|
return
|
||||||
|
|
||||||
# Make sure a checkSnatched marked all downloads/seeds as such
|
# Make sure a checkSnatched marked all downloads/seeds as such
|
||||||
if not download_info and self.conf('run_every') > 0:
|
if not release_download and self.conf('run_every') > 0:
|
||||||
fireEvent('renamer.check_snatched')
|
fireEvent('renamer.check_snatched')
|
||||||
|
|
||||||
self.renaming_started = True
|
self.renaming_started = True
|
||||||
@@ -112,29 +158,35 @@ class Renamer(Plugin):
|
|||||||
files = []
|
files = []
|
||||||
if movie_folder:
|
if movie_folder:
|
||||||
log.info('Scanning movie folder %s...', movie_folder)
|
log.info('Scanning movie folder %s...', movie_folder)
|
||||||
movie_folder = movie_folder.rstrip(os.path.sep)
|
|
||||||
folder = os.path.dirname(movie_folder)
|
folder = os.path.dirname(movie_folder)
|
||||||
|
|
||||||
# Get all files from the specified folder
|
if release_download.get('files', ''):
|
||||||
try:
|
files = splitString(release_download['files'], '|')
|
||||||
for root, folders, names in os.walk(movie_folder):
|
|
||||||
files.extend([os.path.join(root, name) for name in names])
|
# If there is only one file in the torrent, the downloader did not create a subfolder
|
||||||
except:
|
if len(files) == 1:
|
||||||
log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc()))
|
folder = movie_folder
|
||||||
|
else:
|
||||||
|
# Get all files from the specified folder
|
||||||
|
try:
|
||||||
|
for root, folders, names in os.walk(movie_folder):
|
||||||
|
files.extend([os.path.join(root, name) for name in names])
|
||||||
|
except:
|
||||||
|
log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc()))
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
# Extend the download info with info stored in the downloaded release
|
# Extend the download info with info stored in the downloaded release
|
||||||
download_info = self.extendDownloadInfo(download_info)
|
release_download = self.extendReleaseDownload(release_download)
|
||||||
|
|
||||||
# Unpack any archives
|
# Unpack any archives
|
||||||
extr_files = None
|
extr_files = None
|
||||||
if self.conf('unrar'):
|
if self.conf('unrar'):
|
||||||
folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files,
|
folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files,
|
||||||
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(download_info))
|
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(release_download))
|
||||||
|
|
||||||
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
|
groups = fireEvent('scanner.scan', folder = folder if folder else from_folder,
|
||||||
files = files, download_info = download_info, return_ignored = False, single = True)
|
files = files, release_download = release_download, return_ignored = False, single = True) or []
|
||||||
|
|
||||||
folder_name = self.conf('folder_name')
|
folder_name = self.conf('folder_name')
|
||||||
file_name = self.conf('file_name')
|
file_name = self.conf('file_name')
|
||||||
@@ -142,9 +194,9 @@ class Renamer(Plugin):
|
|||||||
nfo_name = self.conf('nfo_name')
|
nfo_name = self.conf('nfo_name')
|
||||||
separator = self.conf('separator')
|
separator = self.conf('separator')
|
||||||
|
|
||||||
# Statusses
|
# Statuses
|
||||||
done_status, active_status, downloaded_status, snatched_status = \
|
done_status, active_status, downloaded_status, snatched_status, seeding_status = \
|
||||||
fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched'], single = True)
|
fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched', 'seeding'], single = True)
|
||||||
|
|
||||||
for group_identifier in groups:
|
for group_identifier in groups:
|
||||||
|
|
||||||
@@ -157,7 +209,7 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
# Add _UNKNOWN_ if no library item is connected
|
# Add _UNKNOWN_ if no library item is connected
|
||||||
if not group['library'] or not movie_title:
|
if not group['library'] or not movie_title:
|
||||||
self.tagDir(group, 'unknown')
|
self.tagRelease(group = group, tag = 'unknown')
|
||||||
continue
|
continue
|
||||||
# Rename the files using the library data
|
# Rename the files using the library data
|
||||||
else:
|
else:
|
||||||
@@ -172,8 +224,13 @@ class Renamer(Plugin):
|
|||||||
movie_title = getTitle(library)
|
movie_title = getTitle(library)
|
||||||
|
|
||||||
# Overwrite destination when set in category
|
# Overwrite destination when set in category
|
||||||
destination = self.conf('to')
|
destination = to_folder
|
||||||
|
category_label = ''
|
||||||
for movie in library_ent.movies:
|
for movie in library_ent.movies:
|
||||||
|
|
||||||
|
if movie.category and movie.category.label:
|
||||||
|
category_label = movie.category.label
|
||||||
|
|
||||||
if movie.category and movie.category.destination and len(movie.category.destination) > 0 and movie.category.destination != 'None':
|
if movie.category and movie.category.destination and len(movie.category.destination) > 0 and movie.category.destination != 'None':
|
||||||
destination = movie.category.destination
|
destination = movie.category.destination
|
||||||
log.debug('Setting category destination for "%s": %s' % (movie_title, destination))
|
log.debug('Setting category destination for "%s": %s' % (movie_title, destination))
|
||||||
@@ -190,7 +247,7 @@ class Renamer(Plugin):
|
|||||||
if extr_files:
|
if extr_files:
|
||||||
group['before_rename'].extend(extr_files)
|
group['before_rename'].extend(extr_files)
|
||||||
|
|
||||||
# Remove weird chars from moviename
|
# Remove weird chars from movie name
|
||||||
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
|
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
|
||||||
|
|
||||||
# Put 'The' at the end
|
# Put 'The' at the end
|
||||||
@@ -217,6 +274,7 @@ class Renamer(Plugin):
|
|||||||
'cd': '',
|
'cd': '',
|
||||||
'cd_nr': '',
|
'cd_nr': '',
|
||||||
'mpaa': library['info'].get('mpaa', ''),
|
'mpaa': library['info'].get('mpaa', ''),
|
||||||
|
'category': category_label,
|
||||||
}
|
}
|
||||||
|
|
||||||
for file_type in group['files']:
|
for file_type in group['files']:
|
||||||
@@ -225,7 +283,7 @@ class Renamer(Plugin):
|
|||||||
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
||||||
log.debug('Skipping, renaming of %s disabled', file_type)
|
log.debug('Skipping, renaming of %s disabled', file_type)
|
||||||
for current_file in group['files'][file_type]:
|
for current_file in group['files'][file_type]:
|
||||||
if self.conf('cleanup') and (not self.downloadIsTorrent(download_info) or self.fileIsAdded(current_file, group)):
|
if self.conf('cleanup') and (not self.downloadIsTorrent(release_download) or self.fileIsAdded(current_file, group)):
|
||||||
remove_files.append(current_file)
|
remove_files.append(current_file)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -385,7 +443,7 @@ class Renamer(Plugin):
|
|||||||
log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
|
log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
|
||||||
|
|
||||||
# Add exists tag to the .ignore file
|
# Add exists tag to the .ignore file
|
||||||
self.tagDir(group, 'exists')
|
self.tagRelease(group = group, tag = 'exists')
|
||||||
|
|
||||||
# Notify on rename fail
|
# Notify on rename fail
|
||||||
download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label)
|
download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label)
|
||||||
@@ -393,16 +451,20 @@ class Renamer(Plugin):
|
|||||||
remove_leftovers = False
|
remove_leftovers = False
|
||||||
|
|
||||||
break
|
break
|
||||||
elif release.status_id is snatched_status.get('id'):
|
|
||||||
if release.quality.id is group['meta_data']['quality']['id']:
|
|
||||||
log.debug('Marking release as downloaded')
|
|
||||||
try:
|
|
||||||
release.status_id = downloaded_status.get('id')
|
|
||||||
release.last_edit = int(time.time())
|
|
||||||
except Exception, e:
|
|
||||||
log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
|
|
||||||
|
|
||||||
db.commit()
|
elif release.status_id in [snatched_status.get('id'), seeding_status.get('id')]:
|
||||||
|
if release_download and release_download.get('rls_id'):
|
||||||
|
if release_download['rls_id'] == release.id:
|
||||||
|
if release_download['status'] == 'completed':
|
||||||
|
# Set the release to downloaded
|
||||||
|
fireEvent('release.update_status', release.id, status = downloaded_status, single = True)
|
||||||
|
elif release_download['status'] == 'seeding':
|
||||||
|
# Set the release to seeding
|
||||||
|
fireEvent('release.update_status', release.id, status = seeding_status, single = True)
|
||||||
|
|
||||||
|
elif release.quality.id is group['meta_data']['quality']['id']:
|
||||||
|
# Set the release to downloaded
|
||||||
|
fireEvent('release.update_status', release.id, status = downloaded_status, single = True)
|
||||||
|
|
||||||
# Remove leftover files
|
# Remove leftover files
|
||||||
if not remove_leftovers: # Don't remove anything
|
if not remove_leftovers: # Don't remove anything
|
||||||
@@ -411,7 +473,7 @@ class Renamer(Plugin):
|
|||||||
log.debug('Removing leftover files')
|
log.debug('Removing leftover files')
|
||||||
for current_file in group['files']['leftover']:
|
for current_file in group['files']['leftover']:
|
||||||
if self.conf('cleanup') and not self.conf('move_leftover') and \
|
if self.conf('cleanup') and not self.conf('move_leftover') and \
|
||||||
(not self.downloadIsTorrent(download_info) or self.fileIsAdded(current_file, group)):
|
(not self.downloadIsTorrent(release_download) or self.fileIsAdded(current_file, group)):
|
||||||
remove_files.append(current_file)
|
remove_files.append(current_file)
|
||||||
|
|
||||||
# Remove files
|
# Remove files
|
||||||
@@ -427,17 +489,17 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
log.info('Removing "%s"', src)
|
log.info('Removing "%s"', src)
|
||||||
try:
|
try:
|
||||||
src = ss(src)
|
src = sp(src)
|
||||||
if os.path.isfile(src):
|
if os.path.isfile(src):
|
||||||
os.remove(src)
|
os.remove(src)
|
||||||
|
|
||||||
parent_dir = os.path.normpath(os.path.dirname(src))
|
parent_dir = os.path.dirname(src)
|
||||||
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not self.conf('from') in parent_dir:
|
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not from_folder in parent_dir:
|
||||||
delete_folders.append(parent_dir)
|
delete_folders.append(parent_dir)
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed removing %s: %s', (src, traceback.format_exc()))
|
log.error('Failed removing %s: %s', (src, traceback.format_exc()))
|
||||||
self.tagDir(group, 'failed_remove')
|
self.tagRelease(group = group, tag = 'failed_remove')
|
||||||
|
|
||||||
# Delete leftover folder from older releases
|
# Delete leftover folder from older releases
|
||||||
for delete_folder in delete_folders:
|
for delete_folder in delete_folders:
|
||||||
@@ -457,15 +519,15 @@ class Renamer(Plugin):
|
|||||||
self.makeDir(os.path.dirname(dst))
|
self.makeDir(os.path.dirname(dst))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info) or self.fileIsAdded(src, group))
|
self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(release_download) or self.fileIsAdded(src, group))
|
||||||
group['renamed_files'].append(dst)
|
group['renamed_files'].append(dst)
|
||||||
except:
|
except:
|
||||||
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
||||||
self.tagDir(group, 'failed_rename')
|
self.tagRelease(group = group, tag = 'failed_rename')
|
||||||
|
|
||||||
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
|
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
|
||||||
if self.movieInFromFolder(movie_folder) and self.downloadIsTorrent(download_info):
|
if self.movieInFromFolder(movie_folder) and self.downloadIsTorrent(release_download):
|
||||||
self.tagDir(group, 'renamed_already')
|
self.tagRelease(group = group, tag = 'renamed_already')
|
||||||
|
|
||||||
# Remove matching releases
|
# Remove matching releases
|
||||||
for release in remove_releases:
|
for release in remove_releases:
|
||||||
@@ -475,12 +537,19 @@ class Renamer(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
|
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
|
||||||
|
|
||||||
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(download_info):
|
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download):
|
||||||
|
if movie_folder:
|
||||||
|
# Delete the movie folder
|
||||||
|
group_folder = movie_folder
|
||||||
|
else:
|
||||||
|
# Delete the first empty subfolder in the tree relative to the 'from' folder
|
||||||
|
group_folder = sp(os.path.join(from_folder, os.path.relpath(group['parentdir'], from_folder)).split(os.path.sep)[0])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log.info('Deleting folder: %s', group['parentdir'])
|
log.info('Deleting folder: %s', group_folder)
|
||||||
self.deleteEmptyFolder(group['parentdir'])
|
self.deleteEmptyFolder(group_folder)
|
||||||
except:
|
except:
|
||||||
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
|
log.error('Failed removing %s: %s', (group_folder, traceback.format_exc()))
|
||||||
|
|
||||||
# Notify on download, search for trailers etc
|
# Notify on download, search for trailers etc
|
||||||
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
||||||
@@ -515,18 +584,9 @@ class Renamer(Plugin):
|
|||||||
return rename_files
|
return rename_files
|
||||||
|
|
||||||
# This adds a file to ignore / tag a release so it is ignored later
|
# This adds a file to ignore / tag a release so it is ignored later
|
||||||
def tagDir(self, group, tag):
|
def tagRelease(self, tag, group = None, release_download = None):
|
||||||
|
if not tag:
|
||||||
ignore_file = None
|
return
|
||||||
if isinstance(group, dict):
|
|
||||||
for movie_file in sorted(list(group['files']['movie'])):
|
|
||||||
ignore_file = '%s.%s.ignore' % (os.path.splitext(movie_file)[0], tag)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if not os.path.isdir(group) or not tag:
|
|
||||||
return
|
|
||||||
ignore_file = os.path.join(group, '%s.ignore' % tag)
|
|
||||||
|
|
||||||
|
|
||||||
text = """This file is from CouchPotato
|
text = """This file is from CouchPotato
|
||||||
It has marked this release as "%s"
|
It has marked this release as "%s"
|
||||||
@@ -534,25 +594,88 @@ This file hides the release from the renamer
|
|||||||
Remove it if you want it to be renamed (again, or at least let it try again)
|
Remove it if you want it to be renamed (again, or at least let it try again)
|
||||||
""" % tag
|
""" % tag
|
||||||
|
|
||||||
if ignore_file:
|
tag_files = []
|
||||||
self.createFile(ignore_file, text)
|
|
||||||
|
|
||||||
def untagDir(self, folder, tag = ''):
|
# Tag movie files if they are known
|
||||||
if not os.path.isdir(folder):
|
if isinstance(group, dict):
|
||||||
|
tag_files = [sorted(list(group['files']['movie']))[0]]
|
||||||
|
|
||||||
|
elif isinstance(release_download, dict):
|
||||||
|
# Tag download_files if they are known
|
||||||
|
if release_download['files']:
|
||||||
|
tag_files = release_download['files'].split('|')
|
||||||
|
|
||||||
|
# Tag all files in release folder
|
||||||
|
else:
|
||||||
|
for root, folders, names in os.walk(release_download['folder']):
|
||||||
|
tag_files.extend([os.path.join(root, name) for name in names])
|
||||||
|
|
||||||
|
for filename in tag_files:
|
||||||
|
tag_filename = '%s.%s.ignore' % (os.path.splitext(filename)[0], tag)
|
||||||
|
if not os.path.isfile(tag_filename):
|
||||||
|
self.createFile(tag_filename, text)
|
||||||
|
|
||||||
|
def untagRelease(self, release_download, tag = ''):
|
||||||
|
if not release_download:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Remove any .ignore files
|
tag_files = []
|
||||||
for root, dirnames, filenames in os.walk(folder):
|
|
||||||
for filename in fnmatch.filter(filenames, '*%s.ignore' % tag):
|
|
||||||
os.remove((os.path.join(root, filename)))
|
|
||||||
|
|
||||||
def hastagDir(self, folder, tag = ''):
|
folder = release_download['folder']
|
||||||
if not os.path.isdir(folder):
|
if not os.path.isdir(folder):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Find any .ignore files
|
# Untag download_files if they are known
|
||||||
|
if release_download['files']:
|
||||||
|
tag_files = release_download['files'].split('|')
|
||||||
|
|
||||||
|
# Untag all files in release folder
|
||||||
|
else:
|
||||||
|
for root, folders, names in os.walk(release_download['folder']):
|
||||||
|
tag_files.extend([os.path.join(root, name) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
||||||
|
|
||||||
|
# Find all .ignore files in folder
|
||||||
|
ignore_files = []
|
||||||
for root, dirnames, filenames in os.walk(folder):
|
for root, dirnames, filenames in os.walk(folder):
|
||||||
if fnmatch.filter(filenames, '*%s.ignore' % tag):
|
ignore_files.extend(fnmatch.filter([os.path.join(root, filename) for filename in filenames], '*%s.ignore' % tag))
|
||||||
|
|
||||||
|
# Match all found ignore files with the tag_files and delete if found
|
||||||
|
for tag_file in tag_files:
|
||||||
|
ignore_file = fnmatch.filter(ignore_files, '%s.%s.ignore' % (re.escape(os.path.splitext(tag_file)[0]), tag if tag else '*'))
|
||||||
|
for filename in ignore_file:
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
except:
|
||||||
|
log.debug('Unable to remove ignore file: %s. Error: %s.' % (filename, traceback.format_exc()))
|
||||||
|
|
||||||
|
def hastagRelease(self, release_download, tag = ''):
|
||||||
|
if not release_download:
|
||||||
|
return False
|
||||||
|
|
||||||
|
folder = release_download['folder']
|
||||||
|
if not os.path.isdir(folder):
|
||||||
|
return False
|
||||||
|
|
||||||
|
tag_files = []
|
||||||
|
ignore_files = []
|
||||||
|
|
||||||
|
# Find tag on download_files if they are known
|
||||||
|
if release_download['files']:
|
||||||
|
tag_files = release_download['files'].split('|')
|
||||||
|
|
||||||
|
# Find tag on all files in release folder
|
||||||
|
else:
|
||||||
|
for root, folders, names in os.walk(release_download['folder']):
|
||||||
|
tag_files.extend([os.path.join(root, name) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
||||||
|
|
||||||
|
# Find all .ignore files in folder
|
||||||
|
for root, dirnames, filenames in os.walk(folder):
|
||||||
|
ignore_files.extend(fnmatch.filter([os.path.join(root, filename) for filename in filenames], '*%s.ignore' % tag))
|
||||||
|
|
||||||
|
# Match all found ignore files with the tag_files and return True found
|
||||||
|
for tag_file in tag_files:
|
||||||
|
ignore_file = fnmatch.filter(ignore_files, '%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*'))
|
||||||
|
if ignore_file:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -571,7 +694,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
link(old, dest)
|
link(old, dest)
|
||||||
except:
|
except:
|
||||||
# Try to simlink next
|
# Try to simlink next
|
||||||
log.debug('Couldn\'t hardlink file "%s" to "%s". Simlinking instead. Error: %s. ', (old, dest, traceback.format_exc()))
|
log.debug('Couldn\'t hardlink file "%s" to "%s". Simlinking instead. Error: %s.', (old, dest, traceback.format_exc()))
|
||||||
shutil.copy(old, dest)
|
shutil.copy(old, dest)
|
||||||
try:
|
try:
|
||||||
symlink(dest, old + '.link')
|
symlink(dest, old + '.link')
|
||||||
@@ -615,19 +738,35 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
|
|
||||||
replaced = toUnicode(string)
|
replaced = toUnicode(string)
|
||||||
for x, r in replacements.iteritems():
|
for x, r in replacements.iteritems():
|
||||||
|
if x in ['thename', 'namethe']:
|
||||||
|
continue
|
||||||
if r is not None:
|
if r is not None:
|
||||||
replaced = replaced.replace(u'<%s>' % toUnicode(x), toUnicode(r))
|
replaced = replaced.replace(u'<%s>' % toUnicode(x), toUnicode(r))
|
||||||
else:
|
else:
|
||||||
#If information is not available, we don't want the tag in the filename
|
#If information is not available, we don't want the tag in the filename
|
||||||
replaced = replaced.replace('<' + x + '>', '')
|
replaced = replaced.replace('<' + x + '>', '')
|
||||||
|
|
||||||
|
replaced = self.replaceDoubles(replaced.lstrip('. '))
|
||||||
|
for x, r in replacements.iteritems():
|
||||||
|
if x in ['thename', 'namethe']:
|
||||||
|
replaced = replaced.replace(u'<%s>' % toUnicode(x), toUnicode(r))
|
||||||
replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
|
replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
|
||||||
|
|
||||||
sep = self.conf('foldersep') if folder else self.conf('separator')
|
sep = self.conf('foldersep') if folder else self.conf('separator')
|
||||||
return self.replaceDoubles(replaced.lstrip('. ')).replace(' ', ' ' if not sep else sep)
|
return replaced.replace(' ', ' ' if not sep else sep)
|
||||||
|
|
||||||
def replaceDoubles(self, string):
|
def replaceDoubles(self, string):
|
||||||
return string.replace(' ', ' ').replace(' .', '.')
|
|
||||||
|
replaces = [
|
||||||
|
('\.+', '.'), ('_+', '_'), ('-+', '-'), ('\s+', ' '),
|
||||||
|
('(\s\.)+', '.'), ('(-\.)+', '.'), ('(\s-)+', '-'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for r in replaces:
|
||||||
|
reg, replace_with = r
|
||||||
|
string = re.sub(reg, replace_with, string)
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
def deleteEmptyFolder(self, folder, show_error = True):
|
def deleteEmptyFolder(self, folder, show_error = True):
|
||||||
folder = ss(folder)
|
folder = ss(folder)
|
||||||
@@ -656,117 +795,115 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
|
|
||||||
self.checking_snatched = True
|
self.checking_snatched = True
|
||||||
|
|
||||||
snatched_status, ignored_status, failed_status, done_status, seeding_status, downloaded_status = \
|
snatched_status, ignored_status, failed_status, seeding_status, downloaded_status, missing_status = \
|
||||||
fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done', 'seeding', 'downloaded'], single = True)
|
fireEvent('status.get', ['snatched', 'ignored', 'failed', 'seeding', 'downloaded', 'missing'], single = True)
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
|
rels = db.query(Release).filter(
|
||||||
rels.extend(db.query(Release).filter_by(status_id = seeding_status.get('id')).all())
|
Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')])
|
||||||
|
).all()
|
||||||
|
|
||||||
scan_items = []
|
scan_releases = []
|
||||||
scan_required = False
|
scan_required = False
|
||||||
|
|
||||||
if rels:
|
if rels:
|
||||||
log.debug('Checking status snatched releases...')
|
log.debug('Checking status snatched releases...')
|
||||||
|
|
||||||
statuses = fireEvent('download.status', merge = True)
|
release_downloads = fireEvent('download.status', merge = True)
|
||||||
if not statuses:
|
if not release_downloads:
|
||||||
log.debug('Download status functionality is not implemented for active downloaders.')
|
log.debug('Download status functionality is not implemented for active downloaders.')
|
||||||
scan_required = True
|
scan_required = True
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
for rel in rels:
|
for rel in rels:
|
||||||
rel_dict = rel.to_dict({'info': {}})
|
rel_dict = rel.to_dict({'info': {}})
|
||||||
|
|
||||||
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
|
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
|
||||||
|
|
||||||
|
if not isinstance(rel_dict['info'], (dict)):
|
||||||
|
log.error('Faulty release found without any info, ignoring.')
|
||||||
|
fireEvent('release.update_status', rel.id, status = ignored_status, single = True)
|
||||||
|
continue
|
||||||
|
|
||||||
# check status
|
# check status
|
||||||
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
|
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
|
||||||
|
|
||||||
found = False
|
found = False
|
||||||
for item in statuses:
|
for release_download in release_downloads:
|
||||||
found_release = False
|
found_release = False
|
||||||
if rel_dict['info'].get('download_id'):
|
if rel_dict['info'].get('download_id'):
|
||||||
if item['id'] == rel_dict['info']['download_id'] and item['downloader'] == rel_dict['info']['download_downloader']:
|
if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']:
|
||||||
log.debug('Found release by id: %s', item['id'])
|
log.debug('Found release by id: %s', release_download['id'])
|
||||||
found_release = True
|
found_release = True
|
||||||
else:
|
else:
|
||||||
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
|
if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']:
|
||||||
found_release = True
|
found_release = True
|
||||||
|
|
||||||
if found_release:
|
if found_release:
|
||||||
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
|
timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft']
|
||||||
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
|
log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft))
|
||||||
|
|
||||||
|
if release_download['status'] == 'busy':
|
||||||
|
# Set the release to snatched if it was missing before
|
||||||
|
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
|
||||||
|
|
||||||
if item['status'] == 'busy':
|
|
||||||
# Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading
|
# Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading
|
||||||
if item['folder'] and self.conf('from') in item['folder']:
|
if self.movieInFromFolder(release_download['folder']):
|
||||||
self.tagDir(item['folder'], 'downloading')
|
self.tagRelease(release_download = release_download, tag = 'downloading')
|
||||||
|
|
||||||
elif item['status'] == 'seeding':
|
|
||||||
|
|
||||||
|
elif release_download['status'] == 'seeding':
|
||||||
#If linking setting is enabled, process release
|
#If linking setting is enabled, process release
|
||||||
if self.conf('file_action') != 'move' and not rel.movie.status_id == done_status.get('id') and self.statusInfoComplete(item):
|
if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download):
|
||||||
log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (item['name'], item['seed_ratio']))
|
log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio']))
|
||||||
|
|
||||||
# Remove the downloading tag
|
# Remove the downloading tag
|
||||||
self.untagDir(item['folder'], 'downloading')
|
self.untagRelease(release_download = release_download, tag = 'downloading')
|
||||||
|
|
||||||
rel.status_id = seeding_status.get('id')
|
|
||||||
rel.last_edit = int(time.time())
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Scan and set the torrent to paused if required
|
# Scan and set the torrent to paused if required
|
||||||
item.update({'pause': True, 'scan': True, 'process_complete': False})
|
release_download.update({'pause': True, 'scan': True, 'process_complete': False})
|
||||||
scan_items.append(item)
|
scan_releases.append(release_download)
|
||||||
else:
|
else:
|
||||||
if rel.status_id != seeding_status.get('id'):
|
|
||||||
rel.status_id = seeding_status.get('id')
|
|
||||||
rel.last_edit = int(time.time())
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
#let it seed
|
#let it seed
|
||||||
log.debug('%s is seeding with ratio: %s', (item['name'], item['seed_ratio']))
|
log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio']))
|
||||||
elif item['status'] == 'failed':
|
|
||||||
fireEvent('download.remove_failed', item, single = True)
|
# Set the release to seeding
|
||||||
rel.status_id = failed_status.get('id')
|
fireEvent('release.update_status', rel.id, status = seeding_status, single = True)
|
||||||
rel.last_edit = int(time.time())
|
|
||||||
db.commit()
|
elif release_download['status'] == 'failed':
|
||||||
|
# Set the release to failed
|
||||||
|
fireEvent('release.update_status', rel.id, status = failed_status, single = True)
|
||||||
|
|
||||||
|
fireEvent('download.remove_failed', release_download, single = True)
|
||||||
|
|
||||||
if self.conf('next_on_failed'):
|
if self.conf('next_on_failed'):
|
||||||
fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id)
|
fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id)
|
||||||
elif item['status'] == 'completed':
|
elif release_download['status'] == 'completed':
|
||||||
log.info('Download of %s completed!', item['name'])
|
log.info('Download of %s completed!', release_download['name'])
|
||||||
if self.statusInfoComplete(item):
|
if self.statusInfoComplete(release_download):
|
||||||
|
|
||||||
# If the release has been seeding, process now the seeding is done
|
# If the release has been seeding, process now the seeding is done
|
||||||
if rel.status_id == seeding_status.get('id'):
|
if rel.status_id == seeding_status.get('id'):
|
||||||
if rel.movie.status_id == done_status.get('id'):
|
if self.conf('file_action') != 'move':
|
||||||
# Set the release to done as the movie has already been renamed
|
# Set the release to done as the movie has already been renamed
|
||||||
rel.status_id = downloaded_status.get('id')
|
fireEvent('release.update_status', rel.id, status = downloaded_status, single = True)
|
||||||
rel.last_edit = int(time.time())
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Allow the downloader to clean-up
|
# Allow the downloader to clean-up
|
||||||
item.update({'pause': False, 'scan': False, 'process_complete': True})
|
release_download.update({'pause': False, 'scan': False, 'process_complete': True})
|
||||||
scan_items.append(item)
|
scan_releases.append(release_download)
|
||||||
else:
|
else:
|
||||||
# Set the release to snatched so that the renamer can process the release as if it was never seeding
|
|
||||||
rel.status_id = snatched_status.get('id')
|
|
||||||
rel.last_edit = int(time.time())
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Scan and Allow the downloader to clean-up
|
# Scan and Allow the downloader to clean-up
|
||||||
item.update({'pause': False, 'scan': True, 'process_complete': True})
|
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
|
||||||
scan_items.append(item)
|
scan_releases.append(release_download)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# Set the release to snatched if it was missing before
|
||||||
|
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
|
||||||
|
|
||||||
# Remove the downloading tag
|
# Remove the downloading tag
|
||||||
self.untagDir(item['folder'], 'downloading')
|
self.untagRelease(release_download = release_download, tag = 'downloading')
|
||||||
|
|
||||||
# Scan and Allow the downloader to clean-up
|
# Scan and Allow the downloader to clean-up
|
||||||
item.update({'pause': False, 'scan': True, 'process_complete': True})
|
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
|
||||||
scan_items.append(item)
|
scan_releases.append(release_download)
|
||||||
else:
|
else:
|
||||||
scan_required = True
|
scan_required = True
|
||||||
|
|
||||||
@@ -776,25 +913,33 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
if not found:
|
if not found:
|
||||||
log.info('%s not found in downloaders', nzbname)
|
log.info('%s not found in downloaders', nzbname)
|
||||||
|
|
||||||
|
#Check status if already missing and for how long, if > 1 week, set to ignored else to missing
|
||||||
|
if rel.status_id == missing_status.get('id'):
|
||||||
|
if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60:
|
||||||
|
fireEvent('release.update_status', rel.id, status = ignored_status, single = True)
|
||||||
|
else:
|
||||||
|
# Set the release to missing
|
||||||
|
fireEvent('release.update_status', rel.id, status = missing_status, single = True)
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
|
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
|
||||||
|
|
||||||
# The following can either be done here, or inside the scanner if we pass it scan_items in one go
|
# The following can either be done here, or inside the scanner if we pass it scan_items in one go
|
||||||
for item in scan_items:
|
for release_download in scan_releases:
|
||||||
# Ask the renamer to scan the item
|
# Ask the renamer to scan the item
|
||||||
if item['scan']:
|
if release_download['scan']:
|
||||||
if item['pause'] and self.conf('file_action') == 'link':
|
if release_download['pause'] and self.conf('file_action') == 'link':
|
||||||
fireEvent('download.pause', item = item, pause = True, single = True)
|
fireEvent('download.pause', release_download = release_download, pause = True, single = True)
|
||||||
fireEvent('renamer.scan', download_info = item)
|
fireEvent('renamer.scan', release_download = release_download)
|
||||||
if item['pause'] and self.conf('file_action') == 'link':
|
if release_download['pause'] and self.conf('file_action') == 'link':
|
||||||
fireEvent('download.pause', item = item, pause = False, single = True)
|
fireEvent('download.pause', release_download = release_download, pause = False, single = True)
|
||||||
if item['process_complete']:
|
if release_download['process_complete']:
|
||||||
#First make sure the files were succesfully processed
|
#First make sure the files were succesfully processed
|
||||||
if not self.hastagDir(item['folder'], 'failed_rename'):
|
if not self.hastagRelease(release_download = release_download, tag = 'failed_rename'):
|
||||||
# Remove the seeding tag if it exists
|
# Remove the seeding tag if it exists
|
||||||
self.untagDir(item['folder'], 'renamed_already')
|
self.untagRelease(release_download = release_download, tag = 'renamed_already')
|
||||||
# Ask the downloader to process the item
|
# Ask the downloader to process the item
|
||||||
fireEvent('download.process_complete', item = item, single = True)
|
fireEvent('download.process_complete', release_download = release_download, single = True)
|
||||||
|
|
||||||
if scan_required:
|
if scan_required:
|
||||||
fireEvent('renamer.scan')
|
fireEvent('renamer.scan')
|
||||||
@@ -803,16 +948,16 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def extendDownloadInfo(self, download_info):
|
def extendReleaseDownload(self, release_download):
|
||||||
|
|
||||||
rls = None
|
rls = None
|
||||||
|
|
||||||
if download_info and download_info.get('id') and download_info.get('downloader'):
|
if release_download and release_download.get('id') and release_download.get('downloader'):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = download_info.get('downloader')).all()
|
rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = release_download.get('downloader')).all()
|
||||||
rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_info.get('id')).all()
|
rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = release_download.get('id')).all()
|
||||||
|
|
||||||
for rlsnfo_dwnld in rlsnfo_dwnlds:
|
for rlsnfo_dwnld in rlsnfo_dwnlds:
|
||||||
for rlsnfo_id in rlsnfo_ids:
|
for rlsnfo_id in rlsnfo_ids:
|
||||||
@@ -822,32 +967,33 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
if rls: break
|
if rls: break
|
||||||
|
|
||||||
if not rls:
|
if not rls:
|
||||||
log.error('Download ID %s from downloader %s not found in releases', (download_info.get('id'), download_info.get('downloader')))
|
log.error('Download ID %s from downloader %s not found in releases', (release_download.get('id'), release_download.get('downloader')))
|
||||||
|
|
||||||
if rls:
|
if rls:
|
||||||
|
|
||||||
rls_dict = rls.to_dict({'info':{}})
|
rls_dict = rls.to_dict({'info':{}})
|
||||||
download_info.update({
|
release_download.update({
|
||||||
'imdb_id': rls.movie.library.identifier,
|
'imdb_id': rls.movie.library.identifier,
|
||||||
'quality': rls.quality.identifier,
|
'quality': rls.quality.identifier,
|
||||||
'protocol': rls_dict.get('info', {}).get('protocol') or rls_dict.get('info', {}).get('type'),
|
'protocol': rls_dict.get('info', {}).get('protocol') or rls_dict.get('info', {}).get('type'),
|
||||||
|
'rls_id': rls.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
return download_info
|
return release_download
|
||||||
|
|
||||||
def downloadIsTorrent(self, download_info):
|
def downloadIsTorrent(self, release_download):
|
||||||
return download_info and download_info.get('protocol') in ['torrent', 'torrent_magnet']
|
return release_download and release_download.get('protocol') in ['torrent', 'torrent_magnet']
|
||||||
|
|
||||||
def fileIsAdded(self, src, group):
|
def fileIsAdded(self, src, group):
|
||||||
if not group or not group.get('before_rename'):
|
if not group or not group.get('before_rename'):
|
||||||
return False
|
return False
|
||||||
return src in group['before_rename']
|
return src in group['before_rename']
|
||||||
|
|
||||||
def statusInfoComplete(self, item):
|
def statusInfoComplete(self, release_download):
|
||||||
return item['id'] and item['downloader'] and item['folder']
|
return release_download['id'] and release_download['downloader'] and release_download['folder']
|
||||||
|
|
||||||
def movieInFromFolder(self, movie_folder):
|
def movieInFromFolder(self, movie_folder):
|
||||||
return movie_folder and self.conf('from') in movie_folder or not movie_folder
|
return movie_folder and sp(self.conf('from')) in movie_folder or not movie_folder
|
||||||
|
|
||||||
def extractFiles(self, folder = None, movie_folder = None, files = None, cleanup = False):
|
def extractFiles(self, folder = None, movie_folder = None, files = None, cleanup = False):
|
||||||
if not files: files = []
|
if not files: files = []
|
||||||
@@ -859,7 +1005,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
|
|
||||||
# Check input variables
|
# Check input variables
|
||||||
if not folder:
|
if not folder:
|
||||||
folder = self.conf('from')
|
folder = sp(self.conf('from'))
|
||||||
|
|
||||||
check_file_date = True
|
check_file_date = True
|
||||||
if movie_folder:
|
if movie_folder:
|
||||||
@@ -875,7 +1021,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
#Extract all found archives
|
#Extract all found archives
|
||||||
for archive in archives:
|
for archive in archives:
|
||||||
# Check if it has already been processed by CPS
|
# Check if it has already been processed by CPS
|
||||||
if self.hastagDir(os.path.dirname(archive['file'])):
|
if self.hastagRelease(release_download = {'folder': os.path.dirname(archive['file']), 'files': archive['file']}):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Find all related archive files
|
# Find all related archive files
|
||||||
@@ -913,7 +1059,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
|
log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
|
||||||
try:
|
try:
|
||||||
rar_handle = RarFile(archive['file'])
|
rar_handle = RarFile(archive['file'])
|
||||||
extr_path = os.path.join(self.conf('from'), os.path.relpath(os.path.dirname(archive['file']), folder))
|
extr_path = os.path.join(sp(self.conf('from')), os.path.relpath(os.path.dirname(archive['file']), folder))
|
||||||
self.makeDir(extr_path)
|
self.makeDir(extr_path)
|
||||||
for packedinfo in rar_handle.infolist():
|
for packedinfo in rar_handle.infolist():
|
||||||
if not packedinfo.isdir and not os.path.isfile(os.path.join(extr_path, os.path.basename(packedinfo.filename))):
|
if not packedinfo.isdir and not os.path.isfile(os.path.join(extr_path, os.path.basename(packedinfo.filename))):
|
||||||
@@ -936,9 +1082,9 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
files.remove(filename)
|
files.remove(filename)
|
||||||
|
|
||||||
# Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided)
|
# Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided)
|
||||||
if extr_files and os.path.normpath(os.path.normcase(folder)) != os.path.normpath(os.path.normcase(self.conf('from'))):
|
if extr_files and folder != sp(self.conf('from')):
|
||||||
for leftoverfile in list(files):
|
for leftoverfile in list(files):
|
||||||
move_to = os.path.join(self.conf('from'), os.path.relpath(leftoverfile, folder))
|
move_to = os.path.join(sp(self.conf('from')), os.path.relpath(leftoverfile, folder))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.makeDir(os.path.dirname(move_to))
|
self.makeDir(os.path.dirname(move_to))
|
||||||
@@ -961,8 +1107,8 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
log.debug('Removing old movie folder %s...', movie_folder)
|
log.debug('Removing old movie folder %s...', movie_folder)
|
||||||
self.deleteEmptyFolder(movie_folder)
|
self.deleteEmptyFolder(movie_folder)
|
||||||
|
|
||||||
movie_folder = os.path.join(self.conf('from'), os.path.relpath(movie_folder, folder))
|
movie_folder = os.path.join(sp(self.conf('from')), os.path.relpath(movie_folder, folder))
|
||||||
folder = self.conf('from')
|
folder = sp(self.conf('from'))
|
||||||
|
|
||||||
if extr_files:
|
if extr_files:
|
||||||
files.extend(extr_files)
|
files.extend(extr_files)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString, ss
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString, ss, sp
|
||||||
from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
|
from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \
|
||||||
|
splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import File, Movie
|
from couchpotato.core.settings.model import File, Media
|
||||||
from enzyme.exceptions import NoParserError, ParseError
|
from enzyme.exceptions import NoParserError, ParseError
|
||||||
from guessit import guess_movie_info
|
from guessit import guess_movie_info
|
||||||
from subliminal.videos import Video
|
from subliminal.videos import Video
|
||||||
@@ -20,11 +21,9 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class Scanner(Plugin):
|
class Scanner(Plugin):
|
||||||
|
|
||||||
minimal_filesize = {
|
ignored_in_path = [os.path.sep + 'extracted' + os.path.sep, 'extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_',
|
||||||
'media': 314572800, # 300MB
|
'_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo',
|
||||||
'trailer': 1048576, # 1MB
|
'thumbs.db', 'ehthumbs.db', 'desktop.ini'] #unpacking, smb-crap, hidden files
|
||||||
}
|
|
||||||
ignored_in_path = [os.path.sep + 'extracted' + os.path.sep, 'extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
|
||||||
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
|
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
|
||||||
extensions = {
|
extensions = {
|
||||||
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
|
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
|
||||||
@@ -49,6 +48,12 @@ class Scanner(Plugin):
|
|||||||
'leftover': ('leftover', 'leftover'),
|
'leftover': ('leftover', 'leftover'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file_sizes = { # in MB
|
||||||
|
'movie': {'min': 300},
|
||||||
|
'trailer': {'min': 2, 'max': 250},
|
||||||
|
'backdrop': {'min': 0, 'max': 5},
|
||||||
|
}
|
||||||
|
|
||||||
codecs = {
|
codecs = {
|
||||||
'audio': ['dts', 'ac3', 'ac3d', 'mp3'],
|
'audio': ['dts', 'ac3', 'ac3d', 'mp3'],
|
||||||
'video': ['x264', 'h264', 'divx', 'xvid']
|
'video': ['x264', 'h264', 'divx', 'xvid']
|
||||||
@@ -101,9 +106,9 @@ class Scanner(Plugin):
|
|||||||
addEvent('scanner.name_year', self.getReleaseNameYear)
|
addEvent('scanner.name_year', self.getReleaseNameYear)
|
||||||
addEvent('scanner.partnumber', self.getPartNumber)
|
addEvent('scanner.partnumber', self.getPartNumber)
|
||||||
|
|
||||||
def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, return_ignored = True, on_found = None):
|
def scan(self, folder = None, files = None, release_download = None, simple = False, newer_than = 0, return_ignored = True, on_found = None):
|
||||||
|
|
||||||
folder = ss(os.path.normpath(folder))
|
folder = sp(folder)
|
||||||
|
|
||||||
if not folder or not os.path.isdir(folder):
|
if not folder or not os.path.isdir(folder):
|
||||||
log.error('Folder doesn\'t exists: %s', folder)
|
log.error('Folder doesn\'t exists: %s', folder)
|
||||||
@@ -119,7 +124,7 @@ class Scanner(Plugin):
|
|||||||
try:
|
try:
|
||||||
files = []
|
files = []
|
||||||
for root, dirs, walk_files in os.walk(folder):
|
for root, dirs, walk_files in os.walk(folder):
|
||||||
files.extend(os.path.join(root, filename) for filename in walk_files)
|
files.extend([os.path.join(root, filename) for filename in walk_files])
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
# Break if CP wants to shut down
|
||||||
if self.shuttingDown():
|
if self.shuttingDown():
|
||||||
@@ -145,7 +150,7 @@ class Scanner(Plugin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
is_dvd_file = self.isDVDFile(file_path)
|
is_dvd_file = self.isDVDFile(file_path)
|
||||||
if os.path.getsize(file_path) > self.minimal_filesize['media'] or is_dvd_file: # Minimal 300MB files or is DVD file
|
if self.filesizeBetween(file_path, self.file_sizes['movie']) or is_dvd_file: # Minimal 300MB files or is DVD file
|
||||||
|
|
||||||
# Normal identifier
|
# Normal identifier
|
||||||
identifier = self.createStringIdentifier(file_path, folder, exclude_filename = is_dvd_file)
|
identifier = self.createStringIdentifier(file_path, folder, exclude_filename = is_dvd_file)
|
||||||
@@ -179,7 +184,6 @@ class Scanner(Plugin):
|
|||||||
# files will be grouped first.
|
# files will be grouped first.
|
||||||
leftovers = set(sorted(leftovers, reverse = True))
|
leftovers = set(sorted(leftovers, reverse = True))
|
||||||
|
|
||||||
|
|
||||||
# Group files minus extension
|
# Group files minus extension
|
||||||
ignored_identifiers = []
|
ignored_identifiers = []
|
||||||
for identifier, group in movie_files.iteritems():
|
for identifier, group in movie_files.iteritems():
|
||||||
@@ -188,7 +192,7 @@ class Scanner(Plugin):
|
|||||||
log.debug('Grouping files: %s', identifier)
|
log.debug('Grouping files: %s', identifier)
|
||||||
|
|
||||||
has_ignored = 0
|
has_ignored = 0
|
||||||
for file_path in group['unsorted_files']:
|
for file_path in list(group['unsorted_files']):
|
||||||
ext = getExt(file_path)
|
ext = getExt(file_path)
|
||||||
wo_ext = file_path[:-(len(ext) + 1)]
|
wo_ext = file_path[:-(len(ext) + 1)]
|
||||||
found_files = set([i for i in leftovers if wo_ext in i])
|
found_files = set([i for i in leftovers if wo_ext in i])
|
||||||
@@ -197,6 +201,11 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
has_ignored += 1 if ext == 'ignore' else 0
|
has_ignored += 1 if ext == 'ignore' else 0
|
||||||
|
|
||||||
|
if has_ignored == 0:
|
||||||
|
for file_path in list(group['unsorted_files']):
|
||||||
|
ext = getExt(file_path)
|
||||||
|
has_ignored += 1 if ext == 'ignore' else 0
|
||||||
|
|
||||||
if has_ignored > 0:
|
if has_ignored > 0:
|
||||||
ignored_identifiers.append(identifier)
|
ignored_identifiers.append(identifier)
|
||||||
|
|
||||||
@@ -229,10 +238,6 @@ class Scanner(Plugin):
|
|||||||
# Remove the found files from the leftover stack
|
# Remove the found files from the leftover stack
|
||||||
leftovers = leftovers - set(found_files)
|
leftovers = leftovers - set(found_files)
|
||||||
|
|
||||||
exts = [getExt(ff) for ff in found_files]
|
|
||||||
if 'ignore' in exts:
|
|
||||||
ignored_identifiers.append(identifier)
|
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
# Break if CP wants to shut down
|
||||||
if self.shuttingDown():
|
if self.shuttingDown():
|
||||||
break
|
break
|
||||||
@@ -259,14 +264,14 @@ class Scanner(Plugin):
|
|||||||
# Remove the found files from the leftover stack
|
# Remove the found files from the leftover stack
|
||||||
leftovers = leftovers - set([ff])
|
leftovers = leftovers - set([ff])
|
||||||
|
|
||||||
ext = getExt(ff)
|
|
||||||
if ext == 'ignore':
|
|
||||||
ignored_identifiers.append(new_identifier)
|
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
# Break if CP wants to shut down
|
||||||
if self.shuttingDown():
|
if self.shuttingDown():
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# leftovers should be empty
|
||||||
|
if leftovers:
|
||||||
|
log.debug('Some files are still left over: %s', leftovers)
|
||||||
|
|
||||||
# Cleaning up used
|
# Cleaning up used
|
||||||
for identifier in delete_identifiers:
|
for identifier in delete_identifiers:
|
||||||
if path_identifiers.get(identifier):
|
if path_identifiers.get(identifier):
|
||||||
@@ -336,11 +341,11 @@ class Scanner(Plugin):
|
|||||||
total_found = len(valid_files)
|
total_found = len(valid_files)
|
||||||
|
|
||||||
# Make sure only one movie was found if a download ID is provided
|
# Make sure only one movie was found if a download ID is provided
|
||||||
if download_info and total_found == 0:
|
if release_download and total_found == 0:
|
||||||
log.info('Download ID provided (%s), but no groups found! Make sure the download contains valid media files (fully extracted).', download_info.get('imdb_id'))
|
log.info('Download ID provided (%s), but no groups found! Make sure the download contains valid media files (fully extracted).', release_download.get('imdb_id'))
|
||||||
elif download_info and total_found > 1:
|
elif release_download and total_found > 1:
|
||||||
log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files)))
|
log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (release_download.get('imdb_id'), len(valid_files)))
|
||||||
download_info = None
|
release_download = None
|
||||||
|
|
||||||
# Determine file types
|
# Determine file types
|
||||||
db = get_session()
|
db = get_session()
|
||||||
@@ -376,7 +381,7 @@ class Scanner(Plugin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
log.debug('Getting metadata for %s', identifier)
|
log.debug('Getting metadata for %s', identifier)
|
||||||
group['meta_data'] = self.getMetaData(group, folder = folder, download_info = download_info)
|
group['meta_data'] = self.getMetaData(group, folder = folder, release_download = release_download)
|
||||||
|
|
||||||
# Subtitle meta
|
# Subtitle meta
|
||||||
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
|
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
|
||||||
@@ -408,11 +413,11 @@ class Scanner(Plugin):
|
|||||||
del group['unsorted_files']
|
del group['unsorted_files']
|
||||||
|
|
||||||
# Determine movie
|
# Determine movie
|
||||||
group['library'] = self.determineMovie(group, download_info = download_info)
|
group['library'] = self.determineMovie(group, release_download = release_download)
|
||||||
if not group['library']:
|
if not group['library']:
|
||||||
log.error('Unable to determine movie: %s', group['identifiers'])
|
log.error('Unable to determine movie: %s', group['identifiers'])
|
||||||
else:
|
else:
|
||||||
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
|
movie = db.query(Media).filter_by(library_id = group['library']['id']).first()
|
||||||
group['movie_id'] = None if not movie else movie.id
|
group['movie_id'] = None if not movie else movie.id
|
||||||
|
|
||||||
processed_movies[identifier] = group
|
processed_movies[identifier] = group
|
||||||
@@ -433,13 +438,13 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
return processed_movies
|
return processed_movies
|
||||||
|
|
||||||
def getMetaData(self, group, folder = '', download_info = None):
|
def getMetaData(self, group, folder = '', release_download = None):
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
files = list(group['files']['movie'])
|
files = list(group['files']['movie'])
|
||||||
|
|
||||||
for cur_file in files:
|
for cur_file in files:
|
||||||
if os.path.getsize(cur_file) < self.minimal_filesize['media']: continue # Ignore smaller files
|
if not self.filesizeBetween(cur_file, self.file_sizes['movie']): continue # Ignore smaller files
|
||||||
|
|
||||||
meta = self.getMeta(cur_file)
|
meta = self.getMeta(cur_file)
|
||||||
|
|
||||||
@@ -458,8 +463,8 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
# Use the quality guess first, if that failes use the quality we wanted to download
|
# Use the quality guess first, if that failes use the quality we wanted to download
|
||||||
data['quality'] = None
|
data['quality'] = None
|
||||||
if download_info and download_info.get('quality'):
|
if release_download and release_download.get('quality'):
|
||||||
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
|
data['quality'] = fireEvent('quality.single', release_download.get('quality'), single = True)
|
||||||
|
|
||||||
if not data['quality']:
|
if not data['quality']:
|
||||||
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
|
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
|
||||||
@@ -543,12 +548,12 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
return detected_languages
|
return detected_languages
|
||||||
|
|
||||||
def determineMovie(self, group, download_info = None):
|
def determineMovie(self, group, release_download = None):
|
||||||
|
|
||||||
# Get imdb id from downloader
|
# Get imdb id from downloader
|
||||||
imdb_id = download_info and download_info.get('imdb_id')
|
imdb_id = release_download and release_download.get('imdb_id')
|
||||||
if imdb_id:
|
if imdb_id:
|
||||||
log.debug('Found movie via imdb id from it\'s download id: %s', download_info.get('imdb_id'))
|
log.debug('Found movie via imdb id from it\'s download id: %s', release_download.get('imdb_id'))
|
||||||
|
|
||||||
files = group['files']
|
files = group['files']
|
||||||
|
|
||||||
@@ -649,7 +654,7 @@ class Scanner(Plugin):
|
|||||||
def getMediaFiles(self, files):
|
def getMediaFiles(self, files):
|
||||||
|
|
||||||
def test(s):
|
def test(s):
|
||||||
return self.filesizeBetween(s, 300, 100000) and getExt(s.lower()) in self.extensions['movie'] and not self.isSampleFile(s)
|
return self.filesizeBetween(s, self.file_sizes['movie']) and getExt(s.lower()) in self.extensions['movie'] and not self.isSampleFile(s)
|
||||||
|
|
||||||
return set(filter(test, files))
|
return set(filter(test, files))
|
||||||
|
|
||||||
@@ -674,7 +679,7 @@ class Scanner(Plugin):
|
|||||||
def getTrailers(self, files):
|
def getTrailers(self, files):
|
||||||
|
|
||||||
def test(s):
|
def test(s):
|
||||||
return re.search('(^|[\W_])trailer\d*[\W_]', s.lower()) and self.filesizeBetween(s, 2, 250)
|
return re.search('(^|[\W_])trailer\d*[\W_]', s.lower()) and self.filesizeBetween(s, self.file_sizes['trailer'])
|
||||||
|
|
||||||
return set(filter(test, files))
|
return set(filter(test, files))
|
||||||
|
|
||||||
@@ -685,7 +690,7 @@ class Scanner(Plugin):
|
|||||||
files = set(filter(test, files))
|
files = set(filter(test, files))
|
||||||
|
|
||||||
images = {
|
images = {
|
||||||
'backdrop': set(filter(lambda s: re.search('(^|[\W_])fanart|backdrop\d*[\W_]', s.lower()) and self.filesizeBetween(s, 0, 5), files))
|
'backdrop': set(filter(lambda s: re.search('(^|[\W_])fanart|backdrop\d*[\W_]', s.lower()) and self.filesizeBetween(s, self.file_sizes['backdrop']), files))
|
||||||
}
|
}
|
||||||
|
|
||||||
# Rest
|
# Rest
|
||||||
@@ -713,16 +718,6 @@ class Scanner(Plugin):
|
|||||||
log.debug('Ignored "%s" contains "%s".', (filename, i))
|
log.debug('Ignored "%s" contains "%s".', (filename, i))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Sample file
|
|
||||||
if self.isSampleFile(filename):
|
|
||||||
log.debug('Is sample file "%s".', filename)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Minimal size
|
|
||||||
if self.filesizeBetween(filename, self.minimal_filesize['media']):
|
|
||||||
log.debug('File to small: %s', filename)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# All is OK
|
# All is OK
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -731,9 +726,9 @@ class Scanner(Plugin):
|
|||||||
if is_sample: log.debug('Is sample file: %s', filename)
|
if is_sample: log.debug('Is sample file: %s', filename)
|
||||||
return is_sample
|
return is_sample
|
||||||
|
|
||||||
def filesizeBetween(self, file, min = 0, max = 100000):
|
def filesizeBetween(self, file, file_size = []):
|
||||||
try:
|
try:
|
||||||
return (min * 1048576) < os.path.getsize(file) < (max * 1048576)
|
return (file_size.get('min', 0) * 1048576) < os.path.getsize(file) < (file_size.get('max', 100000) * 1048576)
|
||||||
except:
|
except:
|
||||||
log.error('Couldn\'t get filesize of %s.', file)
|
log.error('Couldn\'t get filesize of %s.', file)
|
||||||
|
|
||||||
@@ -741,9 +736,16 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
def createStringIdentifier(self, file_path, folder = '', exclude_filename = False):
|
def createStringIdentifier(self, file_path, folder = '', exclude_filename = False):
|
||||||
|
|
||||||
identifier = file_path.replace(folder, '') # root folder
|
year = self.findYear(file_path)
|
||||||
|
|
||||||
|
identifier = file_path.replace(folder, '').lstrip(os.path.sep) # root folder
|
||||||
identifier = os.path.splitext(identifier)[0] # ext
|
identifier = os.path.splitext(identifier)[0] # ext
|
||||||
|
|
||||||
|
try:
|
||||||
|
path_split = splitString(identifier, os.path.sep)
|
||||||
|
identifier = path_split[-2] if len(path_split) > 1 and len(path_split[-2]) > len(path_split[-1]) else path_split[-1] # Only get filename
|
||||||
|
except: pass
|
||||||
|
|
||||||
if exclude_filename:
|
if exclude_filename:
|
||||||
identifier = identifier[:len(identifier) - len(os.path.split(identifier)[-1])]
|
identifier = identifier[:len(identifier) - len(os.path.split(identifier)[-1])]
|
||||||
|
|
||||||
@@ -757,7 +759,6 @@ class Scanner(Plugin):
|
|||||||
identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':')
|
identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':')
|
||||||
|
|
||||||
# Year
|
# Year
|
||||||
year = self.findYear(identifier)
|
|
||||||
if year and identifier[:4] != year:
|
if year and identifier[:4] != year:
|
||||||
identifier = '%s %s' % (identifier.split(year)[0].strip(), year)
|
identifier = '%s %s' % (identifier.split(year)[0].strip(), year)
|
||||||
else:
|
else:
|
||||||
@@ -821,19 +822,21 @@ class Scanner(Plugin):
|
|||||||
def findYear(self, text):
|
def findYear(self, text):
|
||||||
|
|
||||||
# Search year inside () or [] first
|
# Search year inside () or [] first
|
||||||
matches = re.search('(\(|\[)(?P<year>19[0-9]{2}|20[0-9]{2})(\]|\))', text)
|
matches = re.findall('(\(|\[)(?P<year>19[0-9]{2}|20[0-9]{2})(\]|\))', text)
|
||||||
if matches:
|
if matches:
|
||||||
return matches.group('year')
|
return matches[-1][1]
|
||||||
|
|
||||||
# Search normal
|
# Search normal
|
||||||
matches = re.search('(?P<year>19[0-9]{2}|20[0-9]{2})', text)
|
matches = re.findall('(?P<year>19[0-9]{2}|20[0-9]{2})', text)
|
||||||
if matches:
|
if matches:
|
||||||
return matches.group('year')
|
return matches[-1]
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def getReleaseNameYear(self, release_name, file_name = None):
|
def getReleaseNameYear(self, release_name, file_name = None):
|
||||||
|
|
||||||
|
release_name = release_name.strip(' .-_')
|
||||||
|
|
||||||
# Use guessit first
|
# Use guessit first
|
||||||
guess = {}
|
guess = {}
|
||||||
if file_name:
|
if file_name:
|
||||||
@@ -851,7 +854,7 @@ class Scanner(Plugin):
|
|||||||
cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
|
cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
|
||||||
cleaned = re.sub(self.clean, ' ', cleaned)
|
cleaned = re.sub(self.clean, ' ', cleaned)
|
||||||
|
|
||||||
for year_str in [file_name, cleaned]:
|
for year_str in [file_name, release_name, cleaned]:
|
||||||
if not year_str: continue
|
if not year_str: continue
|
||||||
year = self.findYear(year_str)
|
year = self.findYear(year_str)
|
||||||
if year:
|
if year:
|
||||||
@@ -861,19 +864,21 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
if year: # Split name on year
|
if year: # Split name on year
|
||||||
try:
|
try:
|
||||||
movie_name = cleaned.split(year).pop(0).strip()
|
movie_name = cleaned.rsplit(year, 1).pop(0).strip()
|
||||||
cp_guess = {
|
if movie_name:
|
||||||
'name': movie_name,
|
cp_guess = {
|
||||||
'year': int(year),
|
'name': movie_name,
|
||||||
}
|
'year': int(year),
|
||||||
|
}
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
else: # Split name on multiple spaces
|
|
||||||
|
if not cp_guess: # Split name on multiple spaces
|
||||||
try:
|
try:
|
||||||
movie_name = cleaned.split(' ').pop(0).strip()
|
movie_name = cleaned.split(' ').pop(0).strip()
|
||||||
cp_guess = {
|
cp_guess = {
|
||||||
'name': movie_name,
|
'name': movie_name,
|
||||||
'year': int(year),
|
'year': int(year) if movie_name[:4] != year else 0,
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import getTitle, splitString
|
from couchpotato.core.helpers.variable import getTitle, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
|
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
|
||||||
sizeScore, providerScore, duplicateScore, partialIgnoredScore, namePositionScore, \
|
sizeScore, providerScore, duplicateScore, partialIgnoredScore, namePositionScore, \
|
||||||
halfMultipartScore
|
halfMultipartScore, sceneScore
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -62,4 +62,7 @@ class Score(Plugin):
|
|||||||
if extra_score:
|
if extra_score:
|
||||||
score += extra_score(nzb)
|
score += extra_score(nzb)
|
||||||
|
|
||||||
|
# Scene / Nuke scoring
|
||||||
|
score += sceneScore(nzb['name'])
|
||||||
|
|
||||||
return score
|
return score
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.helpers.encoding import simplifyString
|
from couchpotato.core.helpers.encoding import simplifyString
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import re
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
name_scores = [
|
name_scores = [
|
||||||
# Tags
|
# Tags
|
||||||
@@ -160,3 +165,38 @@ def halfMultipartScore(nzb_name):
|
|||||||
return -30
|
return -30
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def sceneScore(nzb_name):
|
||||||
|
|
||||||
|
check_names = [nzb_name]
|
||||||
|
|
||||||
|
# Match names between "
|
||||||
|
try: check_names.append(re.search(r'([\'"])[^\1]*\1', nzb_name).group(0))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Match longest name between []
|
||||||
|
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', nzb_name), key = len).strip())
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
for name in check_names:
|
||||||
|
|
||||||
|
# Strip twice, remove possible file extensions
|
||||||
|
name = name.lower().strip(' "\'\.-_\[\]')
|
||||||
|
name = re.sub('\.([a-z0-9]{0,4})$', '', name)
|
||||||
|
name = name.strip(' "\'\.-_\[\]')
|
||||||
|
|
||||||
|
# Make sure year and groupname is in there
|
||||||
|
year = re.findall('(?P<year>19[0-9]{2}|20[0-9]{2})', name)
|
||||||
|
group = re.findall('\-([a-z0-9]+)$', name)
|
||||||
|
|
||||||
|
if len(year) > 0 and len(group) > 0:
|
||||||
|
try:
|
||||||
|
validate = fireEvent('release.validate', name, single = True)
|
||||||
|
if validate and tryInt(validate.get('score')) != 0:
|
||||||
|
log.debug('Release "%s" scored %s, reason: %s', (nzb_name, validate['score'], validate['reasons']))
|
||||||
|
return tryInt(validate.get('score'))
|
||||||
|
except:
|
||||||
|
log.error('Failed scoring scene: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class StatusPlugin(Plugin):
|
|||||||
'available': 'Available',
|
'available': 'Available',
|
||||||
'suggest': 'Suggest',
|
'suggest': 'Suggest',
|
||||||
'seeding': 'Seeding',
|
'seeding': 'Seeding',
|
||||||
|
'missing': 'Missing',
|
||||||
}
|
}
|
||||||
status_cached = {}
|
status_cached = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
.suggestions {
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions > h2 {
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result {
|
|
||||||
display: inline-block;
|
|
||||||
width: 33.333%;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 960px) {
|
|
||||||
.suggestions .movie_result {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 600px) {
|
|
||||||
.suggestions .movie_result {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .data {
|
|
||||||
left: 100px;
|
|
||||||
background: #4e5969;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .data .info {
|
|
||||||
top: 15px;
|
|
||||||
left: 15px;
|
|
||||||
right: 15px;
|
|
||||||
bottom: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .data .info h2 {
|
|
||||||
white-space: normal;
|
|
||||||
max-height: 120px;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .data .info .rating,
|
|
||||||
.suggestions .movie_result .data .info .genres,
|
|
||||||
.suggestions .movie_result .data .info .year {
|
|
||||||
position: static;
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
opacity: .6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .data .info .year {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .data .info .rating {
|
|
||||||
font-size: 20px;
|
|
||||||
float: right;
|
|
||||||
margin-top: -20px;
|
|
||||||
}
|
|
||||||
.suggestions .movie_result .data .info .rating:before {
|
|
||||||
content: "\e031";
|
|
||||||
font-family: 'Elusive-Icons';
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0 5px 0 0;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .data .info .genres {
|
|
||||||
font-size: 11px;
|
|
||||||
font-style: italic;
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .data {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .options {
|
|
||||||
left: 100px;
|
|
||||||
}
|
|
||||||
.suggestions .movie_result .options select[name=title] { width: 100%; }
|
|
||||||
.suggestions .movie_result .options select[name=profile] { width: 100%; }
|
|
||||||
.suggestions .movie_result .options select[name=category] { width: 100%; }
|
|
||||||
|
|
||||||
.suggestions .movie_result .button {
|
|
||||||
position: absolute;
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
right: 15px;
|
|
||||||
bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.suggestions .movie_result .thumbnail {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .actions {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
display: none;
|
|
||||||
width: 140px;
|
|
||||||
}
|
|
||||||
.suggestions .movie_result:hover .actions {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.suggestions .movie_result .data.open .actions {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestions .movie_result .actions a {
|
|
||||||
margin-left: 10px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -14,25 +14,25 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.userscript .movie_result {
|
.page.userscript .media_result {
|
||||||
height: 140px;
|
height: 140px;
|
||||||
}
|
}
|
||||||
.page.userscript .movie_result .thumbnail {
|
.page.userscript .media_result .thumbnail {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
}
|
}
|
||||||
.page.userscript .movie_result .options {
|
.page.userscript .media_result .options {
|
||||||
left: 90px;
|
left: 90px;
|
||||||
padding: 54px 15px;
|
padding: 54px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.userscript .movie_result .year {
|
.page.userscript .media_result .year {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.userscript .movie_result .options select[name="title"] {
|
.page.userscript .media_result .options select[name="title"] {
|
||||||
width: 190px;
|
width: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.userscript .movie_result .options select[name="profile"] {
|
.page.userscript .media_result .options select[name="profile"] {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Page.Userscript = new Class({
|
|||||||
if(json.error)
|
if(json.error)
|
||||||
self.frame.set('html', json.error);
|
self.frame.set('html', json.error);
|
||||||
else {
|
else {
|
||||||
var item = new Block.Search.Item(json.movie);
|
var item = new Block.Search.MovieItem(json.movie);
|
||||||
self.frame.adopt(item);
|
self.frame.adopt(item);
|
||||||
item.showOptions();
|
item.showOptions();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
config = {
|
config = [{
|
||||||
'name': 'automation_providers',
|
'name': 'automation_providers',
|
||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
@@ -18,4 +18,4 @@ config = {
|
|||||||
'options': [],
|
'options': [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}]
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ config = [{
|
|||||||
'default': False,
|
'default': False,
|
||||||
'type': 'enabler',
|
'type': 'enabler',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'backlog',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Parses the history until the minimum movie year is reached. (Will be disabled once it has completed)',
|
||||||
|
'default': False,
|
||||||
|
'type': 'bool',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
from couchpotato.core.helpers.rss import RSS
|
from couchpotato.core.helpers.rss import RSS
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
@@ -10,11 +11,49 @@ class Bluray(Automation, RSS):
|
|||||||
|
|
||||||
interval = 1800
|
interval = 1800
|
||||||
rss_url = 'http://www.blu-ray.com/rss/newreleasesfeed.xml'
|
rss_url = 'http://www.blu-ray.com/rss/newreleasesfeed.xml'
|
||||||
|
backlog_url = 'http://www.blu-ray.com/movies/movies.php?show=newreleases&page=%s'
|
||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
|
|
||||||
|
if self.conf('backlog'):
|
||||||
|
|
||||||
|
page = 0
|
||||||
|
while True:
|
||||||
|
page = page + 1
|
||||||
|
|
||||||
|
url = self.backlog_url % page
|
||||||
|
data = self.getHTMLData(url)
|
||||||
|
soup = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Stop if the release year is before the minimal year
|
||||||
|
page_year = soup.body.find_all('center')[3].table.tr.find_all('td', recursive = False)[3].h3.get_text().split(', ')[1]
|
||||||
|
if tryInt(page_year) < self.getMinimal('year'):
|
||||||
|
break
|
||||||
|
|
||||||
|
for table in soup.body.find_all('center')[3].table.tr.find_all('td', recursive = False)[3].find_all('table')[1:20]:
|
||||||
|
name = table.h3.get_text().lower().split('blu-ray')[0].strip()
|
||||||
|
year = table.small.get_text().split('|')[1].strip()
|
||||||
|
|
||||||
|
if not name.find('/') == -1: # make sure it is not a double movie release
|
||||||
|
continue
|
||||||
|
|
||||||
|
if tryInt(year) < self.getMinimal('year'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
imdb = self.search(name, year)
|
||||||
|
|
||||||
|
if imdb:
|
||||||
|
if self.isMinimalMovie(imdb):
|
||||||
|
movies.append(imdb['imdb'])
|
||||||
|
except:
|
||||||
|
log.debug('Error loading page: %s', page)
|
||||||
|
break
|
||||||
|
|
||||||
|
self.conf('backlog', value = False)
|
||||||
|
|
||||||
rss_movies = self.getRSSData(self.rss_url)
|
rss_movies = self.getRSSData(self.rss_url)
|
||||||
|
|
||||||
for movie in rss_movies:
|
for movie in rss_movies:
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from .main import Flixster
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Flixster()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'flixster',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'automation',
|
||||||
|
'list': 'watchlist_providers',
|
||||||
|
'name': 'flixster_automation',
|
||||||
|
'label': 'Flixster',
|
||||||
|
'description': 'Import movies from any public <a href="http://www.flixster.com/">Flixster</a> watchlist',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'automation_enabled',
|
||||||
|
'default': False,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'automation_ids_use',
|
||||||
|
'label': 'Use',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'automation_ids',
|
||||||
|
'label': 'User ID',
|
||||||
|
'type': 'combined',
|
||||||
|
'combine': ['automation_ids_use', 'automation_ids'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.providers.automation.base import Automation
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Flixster(Automation):
|
||||||
|
|
||||||
|
url = 'http://www.flixster.com/api/users/%s/movies/ratings?scoreTypes=wts'
|
||||||
|
|
||||||
|
interval = 60
|
||||||
|
|
||||||
|
def getIMDBids(self):
|
||||||
|
|
||||||
|
ids = splitString(self.conf('automation_ids'))
|
||||||
|
|
||||||
|
if len(ids) == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
movies = []
|
||||||
|
|
||||||
|
for movie in self.getWatchlist():
|
||||||
|
imdb_id = self.search(movie.get('title'), movie.get('year'), imdb_only = True)
|
||||||
|
movies.append(imdb_id)
|
||||||
|
|
||||||
|
return movies
|
||||||
|
|
||||||
|
def getWatchlist(self):
|
||||||
|
|
||||||
|
enablers = [tryInt(x) for x in splitString(self.conf('automation_ids_use'))]
|
||||||
|
ids = splitString(self.conf('automation_ids'))
|
||||||
|
|
||||||
|
index = -1
|
||||||
|
movies = []
|
||||||
|
for user_id in ids:
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
if not enablers[index]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = self.getJsonData(self.url % user_id, decode_from = 'iso-8859-1')
|
||||||
|
|
||||||
|
for movie in data:
|
||||||
|
movies.append({'title': movie['movie']['title'], 'year': movie['movie']['year'] })
|
||||||
|
|
||||||
|
return movies
|
||||||
@@ -55,7 +55,14 @@ config = [{
|
|||||||
'label': 'TOP 250',
|
'label': 'TOP 250',
|
||||||
'description': 'IMDB <a href="http://www.imdb.com/chart/top/">TOP 250</a> chart',
|
'description': 'IMDB <a href="http://www.imdb.com/chart/top/">TOP 250</a> chart',
|
||||||
'default': True,
|
'default': True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'automation_charts_boxoffice',
|
||||||
|
'type': 'bool',
|
||||||
|
'label': 'Box offce TOP 10',
|
||||||
|
'description': 'IMDB Box office <a href="http://www.imdb.com/chart/">TOP 10</a> chart',
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -70,8 +70,11 @@ class IMDBAutomation(IMDBBase):
|
|||||||
chart_urls = {
|
chart_urls = {
|
||||||
'theater': 'http://www.imdb.com/movies-in-theaters/',
|
'theater': 'http://www.imdb.com/movies-in-theaters/',
|
||||||
'top250': 'http://www.imdb.com/chart/top',
|
'top250': 'http://www.imdb.com/chart/top',
|
||||||
|
'boxoffice': 'http://www.imdb.com/chart/',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
first_table = ['boxoffice']
|
||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
@@ -84,6 +87,14 @@ class IMDBAutomation(IMDBBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result_div = html.find('div', attrs = {'id': 'main'})
|
result_div = html.find('div', attrs = {'id': 'main'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
if url in self.first_table:
|
||||||
|
table = result_div.find('table')
|
||||||
|
result_div = table if table else result_div
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
imdb_ids = getImdb(str(result_div), multiple = True)
|
imdb_ids = getImdb(str(result_div), multiple = True)
|
||||||
|
|
||||||
for imdb_id in imdb_ids:
|
for imdb_id in imdb_ids:
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ class ITunes(Automation, RSS):
|
|||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
if self.isDisabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
|
|
||||||
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import xml.etree.ElementTree as XMLTree
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MultiProvider(Plugin):
|
class MultiProvider(Plugin):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -63,13 +62,17 @@ class Provider(Plugin):
|
|||||||
|
|
||||||
return self.is_available.get(host, False)
|
return self.is_available.get(host, False)
|
||||||
|
|
||||||
def getJsonData(self, url, **kwargs):
|
def getJsonData(self, url, decode_from = None, **kwargs):
|
||||||
|
|
||||||
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
|
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
|
||||||
data = self.getCache(cache_key, url, **kwargs)
|
data = self.getCache(cache_key, url, **kwargs)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
try:
|
try:
|
||||||
|
data = data.strip()
|
||||||
|
if decode_from:
|
||||||
|
data = data.decode(decode_from)
|
||||||
|
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
except:
|
except:
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
@@ -251,7 +254,10 @@ class YarrProvider(Provider):
|
|||||||
if identifier in qualities:
|
if identifier in qualities:
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
return [self.cat_backup_id]
|
if self.cat_backup_id:
|
||||||
|
return [self.cat_backup_id]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class ResultList(list):
|
class ResultList(list):
|
||||||
@@ -279,13 +285,23 @@ class ResultList(list):
|
|||||||
|
|
||||||
new_result = self.fillResult(result)
|
new_result = self.fillResult(result)
|
||||||
|
|
||||||
is_correct_movie = fireEvent('movie.searcher.correct_movie',
|
is_correct = fireEvent('searcher.correct_release', new_result, self.movie, self.quality,
|
||||||
nzb = new_result, movie = self.movie, quality = self.quality,
|
|
||||||
imdb_results = self.kwargs.get('imdb_results', False), single = True)
|
imdb_results = self.kwargs.get('imdb_results', False), single = True)
|
||||||
|
|
||||||
if is_correct_movie and new_result['id'] not in self.result_ids:
|
if is_correct and new_result['id'] not in self.result_ids:
|
||||||
|
is_correct_weight = float(is_correct)
|
||||||
|
|
||||||
new_result['score'] += fireEvent('score.calculate', new_result, self.movie, single = True)
|
new_result['score'] += fireEvent('score.calculate', new_result, self.movie, single = True)
|
||||||
|
|
||||||
|
old_score = new_result['score']
|
||||||
|
new_result['score'] = int(old_score * is_correct_weight)
|
||||||
|
|
||||||
|
log.info('Found correct release with weight %.02f, old_score(%d) now scaled to score(%d)', (
|
||||||
|
is_correct_weight,
|
||||||
|
old_score,
|
||||||
|
new_result['score']
|
||||||
|
))
|
||||||
|
|
||||||
self.found(new_result)
|
self.found(new_result)
|
||||||
self.result_ids.append(result['id'])
|
self.result_ids.append(result['id'])
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,26 @@ class MovieResultModifier(Plugin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
addEvent('result.modify.info.search', self.returnByType)
|
||||||
addEvent('result.modify.movie.search', self.combineOnIMDB)
|
addEvent('result.modify.movie.search', self.combineOnIMDB)
|
||||||
addEvent('result.modify.movie.info', self.checkLibrary)
|
addEvent('result.modify.movie.info', self.checkLibrary)
|
||||||
|
|
||||||
|
def returnByType(self, results):
|
||||||
|
|
||||||
|
new_results = {}
|
||||||
|
for r in results:
|
||||||
|
type_name = r.get('type', 'movie') + 's'
|
||||||
|
if not new_results.has_key(type_name):
|
||||||
|
new_results[type_name] = []
|
||||||
|
|
||||||
|
new_results[type_name].append(r)
|
||||||
|
|
||||||
|
# Combine movies, needs a cleaner way..
|
||||||
|
if new_results.has_key('movies'):
|
||||||
|
new_results['movies'] = self.combineOnIMDB(new_results['movies'])
|
||||||
|
|
||||||
|
return new_results
|
||||||
|
|
||||||
def combineOnIMDB(self, results):
|
def combineOnIMDB(self, results):
|
||||||
|
|
||||||
temp = {}
|
temp = {}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.info.base import MovieProvider
|
from couchpotato.core.providers.info.base import MovieProvider
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
import base64
|
||||||
import time
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -11,6 +12,7 @@ log = CPLog(__name__)
|
|||||||
class CouchPotatoApi(MovieProvider):
|
class CouchPotatoApi(MovieProvider):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
|
'validate': 'https://api.couchpota.to/validate/%s/',
|
||||||
'search': 'https://api.couchpota.to/search/%s/',
|
'search': 'https://api.couchpota.to/search/%s/',
|
||||||
'info': 'https://api.couchpota.to/info/%s/',
|
'info': 'https://api.couchpota.to/info/%s/',
|
||||||
'is_movie': 'https://api.couchpota.to/ismovie/%s/',
|
'is_movie': 'https://api.couchpota.to/ismovie/%s/',
|
||||||
@@ -24,11 +26,14 @@ class CouchPotatoApi(MovieProvider):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('movie.info', self.getInfo, priority = 1)
|
addEvent('movie.info', self.getInfo, priority = 1)
|
||||||
|
addEvent('info.search', self.search, priority = 1)
|
||||||
addEvent('movie.search', self.search, priority = 1)
|
addEvent('movie.search', self.search, priority = 1)
|
||||||
addEvent('movie.release_date', self.getReleaseDate)
|
addEvent('movie.release_date', self.getReleaseDate)
|
||||||
addEvent('movie.suggest', self.getSuggestions)
|
addEvent('movie.suggest', self.getSuggestions)
|
||||||
addEvent('movie.is_movie', self.isMovie)
|
addEvent('movie.is_movie', self.isMovie)
|
||||||
|
|
||||||
|
addEvent('release.validate', self.validate)
|
||||||
|
|
||||||
addEvent('cp.source_url', self.getSourceUrl)
|
addEvent('cp.source_url', self.getSourceUrl)
|
||||||
addEvent('cp.messages', self.getMessages)
|
addEvent('cp.messages', self.getMessages)
|
||||||
|
|
||||||
@@ -50,6 +55,14 @@ class CouchPotatoApi(MovieProvider):
|
|||||||
def search(self, q, limit = 5):
|
def search(self, q, limit = 5):
|
||||||
return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders())
|
return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders())
|
||||||
|
|
||||||
|
def validate(self, name = None):
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
|
||||||
|
name_enc = base64.b64encode(name)
|
||||||
|
return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders())
|
||||||
|
|
||||||
def isMovie(self, identifier = None):
|
def isMovie(self, identifier = None):
|
||||||
|
|
||||||
if not identifier:
|
if not identifier:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class OMDBAPI(MovieProvider):
|
|||||||
http_time_between_calls = 0
|
http_time_between_calls = 0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
addEvent('info.search', self.search)
|
||||||
addEvent('movie.search', self.search)
|
addEvent('movie.search', self.search)
|
||||||
addEvent('movie.info', self.getInfo)
|
addEvent('movie.info', self.getInfo)
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ class OMDBAPI(MovieProvider):
|
|||||||
year = tryInt(movie.get('Year', ''))
|
year = tryInt(movie.get('Year', ''))
|
||||||
|
|
||||||
movie_data = {
|
movie_data = {
|
||||||
|
'type': 'movie',
|
||||||
'via_imdb': True,
|
'via_imdb': True,
|
||||||
'titles': [movie.get('Title')] if movie.get('Title') else [],
|
'titles': [movie.get('Title')] if movie.get('Title') else [],
|
||||||
'original_title': movie.get('Title'),
|
'original_title': movie.get('Title'),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ log = CPLog(__name__)
|
|||||||
class TheMovieDb(MovieProvider):
|
class TheMovieDb(MovieProvider):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
addEvent('info.search', self.search, priority = 2)
|
||||||
addEvent('movie.search', self.search, priority = 2)
|
addEvent('movie.search', self.search, priority = 2)
|
||||||
addEvent('movie.info', self.getInfo, priority = 2)
|
addEvent('movie.info', self.getInfo, priority = 2)
|
||||||
addEvent('movie.info_by_tmdb', self.getInfo)
|
addEvent('movie.info_by_tmdb', self.getInfo)
|
||||||
@@ -103,6 +104,7 @@ class TheMovieDb(MovieProvider):
|
|||||||
year = None
|
year = None
|
||||||
|
|
||||||
movie_data = {
|
movie_data = {
|
||||||
|
'type': 'movie',
|
||||||
'via_tmdb': True,
|
'via_tmdb': True,
|
||||||
'tmdb_id': movie.id,
|
'tmdb_id': movie.id,
|
||||||
'titles': [toUnicode(movie.title)],
|
'titles': [toUnicode(movie.title)],
|
||||||
@@ -119,6 +121,7 @@ class TheMovieDb(MovieProvider):
|
|||||||
'year': year,
|
'year': year,
|
||||||
'plot': movie.overview,
|
'plot': movie.overview,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
|
'collection': getattr(movie.collection, 'name', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
|
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
|
||||||
|
|||||||
@@ -104,6 +104,13 @@ class XBMC(MetaDataBase):
|
|||||||
writers = SubElement(nfoxml, 'credits')
|
writers = SubElement(nfoxml, 'credits')
|
||||||
writers.text = toUnicode(writer)
|
writers.text = toUnicode(writer)
|
||||||
|
|
||||||
|
# Sets or collections
|
||||||
|
collection_name = movie_info.get('collection')
|
||||||
|
if collection_name:
|
||||||
|
collection = SubElement(nfoxml, 'set')
|
||||||
|
collection.text = toUnicode(collection_name)
|
||||||
|
sorttitle = SubElement(nfoxml, 'sorttitle')
|
||||||
|
sorttitle.text = '%s %s' % (toUnicode(collection_name), movie_info.get('year'))
|
||||||
|
|
||||||
# Clean up the xml and return it
|
# Clean up the xml and return it
|
||||||
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
|
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
config = {
|
config = [{
|
||||||
'name': 'nzb_providers',
|
'name': 'nzb_providers',
|
||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
@@ -11,4 +11,4 @@ config = {
|
|||||||
'options': [],
|
'options': [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}]
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class BinSearch(NZBProvider):
|
|||||||
total = tryInt(parts.group('total'))
|
total = tryInt(parts.group('total'))
|
||||||
parts = tryInt(parts.group('parts'))
|
parts = tryInt(parts.group('parts'))
|
||||||
|
|
||||||
if (total / parts) < 0.95 or ((total / parts) >= 0.95 and not 'par2' in info.text.lower()):
|
if (total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower())):
|
||||||
log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total))
|
log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||||
from couchpotato.core.helpers.rss import RSS
|
from couchpotato.core.helpers.rss import RSS
|
||||||
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
|
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
@@ -83,7 +83,7 @@ class Newznab(NZBProvider, RSS):
|
|||||||
results.append({
|
results.append({
|
||||||
'id': nzb_id,
|
'id': nzb_id,
|
||||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
||||||
'name': name,
|
'name': toUnicode(name),
|
||||||
'name_extra': name_extra,
|
'name_extra': name_extra,
|
||||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ log = CPLog(__name__)
|
|||||||
class OMGWTFNZBs(NZBProvider, RSS):
|
class OMGWTFNZBs(NZBProvider, RSS):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'search': 'http://rss.omgwtfnzbs.org/rss-search.php?%s',
|
'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s',
|
||||||
|
'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s',
|
||||||
}
|
}
|
||||||
|
|
||||||
http_time_between_calls = 1 #seconds
|
http_time_between_calls = 1 #seconds
|
||||||
@@ -49,13 +50,14 @@ class OMGWTFNZBs(NZBProvider, RSS):
|
|||||||
for nzb in nzbs:
|
for nzb in nzbs:
|
||||||
|
|
||||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||||
|
nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
'id': parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0],
|
'id': nzb_id,
|
||||||
'name': toUnicode(self.getTextElement(nzb, 'title')),
|
'name': toUnicode(self.getTextElement(nzb, 'title')),
|
||||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
|
||||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||||
'url': enclosure['url'],
|
'url': enclosure['url'],
|
||||||
'detail_url': self.getTextElement(nzb, 'link'),
|
'detail_url': self.urls['detail_url'] % nzb_id,
|
||||||
'description': self.getTextElement(nzb, 'description')
|
'description': self.getTextElement(nzb, 'description')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
config = {
|
config = [{
|
||||||
'name': 'torrent_providers',
|
'name': 'torrent_providers',
|
||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
@@ -11,4 +11,4 @@ config = {
|
|||||||
'options': [],
|
'options': [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from couchpotato.core.helpers.variable import getImdb, md5
|
from couchpotato.core.helpers.variable import getImdb, md5, cleanHost
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.base import YarrProvider
|
from couchpotato.core.providers.base import YarrProvider
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -9,6 +11,9 @@ class TorrentProvider(YarrProvider):
|
|||||||
|
|
||||||
protocol = 'torrent'
|
protocol = 'torrent'
|
||||||
|
|
||||||
|
proxy_domain = None
|
||||||
|
proxy_list = []
|
||||||
|
|
||||||
def imdbMatch(self, url, imdbId):
|
def imdbMatch(self, url, imdbId):
|
||||||
if getImdb(url) == imdbId:
|
if getImdb(url) == imdbId:
|
||||||
return True
|
return True
|
||||||
@@ -25,6 +30,42 @@ class TorrentProvider(YarrProvider):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getDomain(self, url = ''):
|
||||||
|
|
||||||
|
forced_domain = self.conf('domain')
|
||||||
|
if forced_domain:
|
||||||
|
return cleanHost(forced_domain).rstrip('/') + url
|
||||||
|
|
||||||
|
if not self.proxy_domain:
|
||||||
|
for proxy in self.proxy_list:
|
||||||
|
|
||||||
|
prop_name = 'proxy.%s' % proxy
|
||||||
|
last_check = float(Env.prop(prop_name, default = 0))
|
||||||
|
if last_check > time.time() - 1209600:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = ''
|
||||||
|
try:
|
||||||
|
data = self.urlopen(proxy, timeout = 3, show_error = False)
|
||||||
|
except:
|
||||||
|
log.debug('Failed %s proxy %s', (self.getName(), proxy))
|
||||||
|
|
||||||
|
if self.correctProxy(data):
|
||||||
|
log.debug('Using proxy for %s: %s', (self.getName(), proxy))
|
||||||
|
self.proxy_domain = proxy
|
||||||
|
break
|
||||||
|
|
||||||
|
Env.prop(prop_name, time.time())
|
||||||
|
|
||||||
|
if not self.proxy_domain:
|
||||||
|
log.error('No %s proxies left, please add one in settings, or let us know which one to add on the forum.', self.getName())
|
||||||
|
return None
|
||||||
|
|
||||||
|
return cleanHost(self.proxy_domain).rstrip('/') + url
|
||||||
|
|
||||||
|
def correctProxy(self):
|
||||||
|
return True
|
||||||
|
|
||||||
class TorrentMagnetProvider(TorrentProvider):
|
class TorrentMagnetProvider(TorrentProvider):
|
||||||
|
|
||||||
protocol = 'torrent_magnet'
|
protocol = 'torrent_magnet'
|
||||||
|
|||||||
+6
-6
@@ -1,16 +1,16 @@
|
|||||||
from .main import SceneHD
|
from .main import BiTHDTV
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
return SceneHD()
|
return BiTHDTV()
|
||||||
|
|
||||||
config = [{
|
config = [{
|
||||||
'name': 'scenehd',
|
'name': 'bithdtv',
|
||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'searcher',
|
'tab': 'searcher',
|
||||||
'list': 'torrent_providers',
|
'list': 'torrent_providers',
|
||||||
'name': 'SceneHD',
|
'name': 'BiT-HDTV',
|
||||||
'description': 'See <a href="https://scenehd.org">SceneHD</a>',
|
'description': 'See <a href="http://bit-hdtv.com">BiT-HDTV</a>',
|
||||||
'wizard': True,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
@@ -46,7 +46,7 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'label': 'Extra Score',
|
'label': 'Extra Score',
|
||||||
'type': 'int',
|
'type': 'int',
|
||||||
'default': 0,
|
'default': 20,
|
||||||
'description': 'Starting score for each release found via this provider.',
|
'description': 'Starting score for each release found via this provider.',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class BiTHDTV(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test' : 'http://www.bit-hdtv.com/',
|
||||||
|
'login' : 'http://www.bit-hdtv.com/takelogin.php',
|
||||||
|
'login_check': 'http://www.bit-hdtv.com/messages.php',
|
||||||
|
'detail' : 'http://www.bit-hdtv.com/details.php?id=%s',
|
||||||
|
'search' : 'http://www.bit-hdtv.com/torrents.php?',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken
|
||||||
|
cat_id_movies = 7
|
||||||
|
|
||||||
|
http_time_between_calls = 1 #seconds
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
|
arguments = tryUrlencode({
|
||||||
|
'search': '%s %s' % (title.replace(':', ''), movie['library']['year']),
|
||||||
|
'cat': self.cat_id_movies
|
||||||
|
})
|
||||||
|
|
||||||
|
url = "%s&%s" % (self.urls['search'], arguments)
|
||||||
|
|
||||||
|
data = self.getHTMLData(url, opener = self.login_opener)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
# Remove BiT-HDTV's output garbage so outdated BS4 versions successfully parse the HTML
|
||||||
|
split_data = data.partition('-->')
|
||||||
|
if '## SELECT COUNT(' in split_data[0]:
|
||||||
|
data = split_data[2]
|
||||||
|
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_table = html.find('table', attrs = {'width' : '750', 'class' : ''})
|
||||||
|
if result_table is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = result_table.find_all('tr')
|
||||||
|
for result in entries[1:]:
|
||||||
|
|
||||||
|
cells = result.find_all('td')
|
||||||
|
link = cells[2].find('a')
|
||||||
|
torrent_id = link['href'].replace('/details.php?id=', '')
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': link.contents[0].get_text(),
|
||||||
|
'url': cells[0].find('a')['href'],
|
||||||
|
'detail_url': self.urls['detail'] % torrent_id,
|
||||||
|
'size': self.parseSize(cells[6].get_text()),
|
||||||
|
'seeders': tryInt(cells[8].string),
|
||||||
|
'leechers': tryInt(cells[9].string),
|
||||||
|
'get_more_info': self.getMoreInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return tryUrlencode({
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
})
|
||||||
|
|
||||||
|
def getMoreInfo(self, item):
|
||||||
|
full_description = self.getCache('bithdtv.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||||
|
html = BeautifulSoup(full_description)
|
||||||
|
nfo_pre = html.find('table', attrs = {'class':'detail'})
|
||||||
|
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||||
|
|
||||||
|
item['description'] = description
|
||||||
|
return item
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
from main import ILoveTorrents
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return ILoveTorrents()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'ilovetorrents',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'ILoveTorrents',
|
||||||
|
'description': 'Where the Love of Torrents is Born',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'label': 'Username',
|
||||||
|
'type': 'string',
|
||||||
|
'default': '',
|
||||||
|
'description': 'The user name for your ILT account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'label': 'Password',
|
||||||
|
'type': 'password',
|
||||||
|
'default': '',
|
||||||
|
'description': 'The password for your ILT account.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'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': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ILoveTorrents(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'download': 'http://www.ilovetorrents.me/%s',
|
||||||
|
'detail': 'http://www.ilovetorrents.me/%s',
|
||||||
|
'search': 'http://www.ilovetorrents.me/browse.php?search=%s&page=%s&cat=%s',
|
||||||
|
'test' : 'http://www.ilovetorrents.me/',
|
||||||
|
'login' : 'http://www.ilovetorrents.me/takelogin.php',
|
||||||
|
'login_check' : 'http://www.ilovetorrents.me'
|
||||||
|
}
|
||||||
|
|
||||||
|
cat_ids = [
|
||||||
|
(['41'], ['720p', '1080p', 'brrip']),
|
||||||
|
(['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
||||||
|
(['20'], ['dvdr'])
|
||||||
|
]
|
||||||
|
|
||||||
|
cat_backup_id = 200
|
||||||
|
disable_provider = False
|
||||||
|
http_time_between_calls = 1
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
|
page = 0
|
||||||
|
total_pages = 1
|
||||||
|
cats = self.getCatId(quality['identifier'])
|
||||||
|
|
||||||
|
while page < total_pages:
|
||||||
|
|
||||||
|
movieTitle = tryUrlencode('"%s" %s' % (title, movie['library']['year']))
|
||||||
|
search_url = self.urls['search'] % (movieTitle, page, cats[0])
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
data = self.getHTMLData(search_url, opener = self.login_opener)
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
soup = BeautifulSoup(data)
|
||||||
|
|
||||||
|
results_table = soup.find('table', attrs = {'class': 'koptekst'})
|
||||||
|
if not results_table:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
pagelinks = soup.findAll(href = re.compile('page'))
|
||||||
|
pageNumbers = [int(re.search('page=(?P<pageNumber>.+'')', i['href']).group('pageNumber')) for i in pagelinks]
|
||||||
|
total_pages = max(pageNumbers)
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
entries = results_table.find_all('tr')
|
||||||
|
|
||||||
|
for result in entries[1:]:
|
||||||
|
prelink = result.find(href = re.compile('details.php'))
|
||||||
|
link = prelink['href']
|
||||||
|
download = result.find('a', href = re.compile('download.php'))['href']
|
||||||
|
|
||||||
|
if link and download:
|
||||||
|
|
||||||
|
def extra_score(item):
|
||||||
|
trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) is not None]
|
||||||
|
vip = (0, 20)[result.find('img', alt = re.compile('VIP')) is not None]
|
||||||
|
confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) is not None]
|
||||||
|
moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) is not None]
|
||||||
|
|
||||||
|
return confirmed + trusted + vip + moderated
|
||||||
|
|
||||||
|
id = re.search('id=(?P<id>\d+)&', link).group('id')
|
||||||
|
url = self.urls['download'] % (download)
|
||||||
|
|
||||||
|
fileSize = self.parseSize(result.select('td.rowhead')[5].text)
|
||||||
|
results.append({
|
||||||
|
'id': id,
|
||||||
|
'name': toUnicode(prelink.find('b').text),
|
||||||
|
'url': url,
|
||||||
|
'detail_url': self.urls['detail'] % link,
|
||||||
|
'size': fileSize,
|
||||||
|
'seeders': tryInt(result.find_all('td')[2].string),
|
||||||
|
'leechers': tryInt(result.find_all('td')[3].string),
|
||||||
|
'extra_score': extra_score,
|
||||||
|
'get_more_info': self.getMoreInfo
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return tryUrlencode({
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
'submit': 'Welcome to ILT',
|
||||||
|
})
|
||||||
|
|
||||||
|
def getMoreInfo(self, item):
|
||||||
|
cache_key = 'ilt.%s' % item['id']
|
||||||
|
description = self.getCache(cache_key)
|
||||||
|
|
||||||
|
if not description:
|
||||||
|
|
||||||
|
try:
|
||||||
|
full_description = self.getHTMLData(item['detail_url'], opener = self.login_opener)
|
||||||
|
html = BeautifulSoup(full_description)
|
||||||
|
nfo_pre = html.find('td', attrs = {'class':'main'}).findAll('table')[1]
|
||||||
|
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||||
|
except:
|
||||||
|
log.error('Failed getting more info for %s', item['name'])
|
||||||
|
description = ''
|
||||||
|
|
||||||
|
self.setCache(cache_key, description, timeout = 25920000)
|
||||||
|
|
||||||
|
item['description'] = description
|
||||||
|
return item
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
@@ -18,6 +18,12 @@ config = [{
|
|||||||
'type': 'enabler',
|
'type': 'enabler',
|
||||||
'default': True,
|
'default': True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'domain',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Proxy server',
|
||||||
|
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'seed_ratio',
|
'name': 'seed_ratio',
|
||||||
'label': 'Seed ratio',
|
'label': 'Seed ratio',
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ log = CPLog(__name__)
|
|||||||
class KickAssTorrents(TorrentMagnetProvider):
|
class KickAssTorrents(TorrentMagnetProvider):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'test': 'https://kickass.to/',
|
'detail': '%s/%s',
|
||||||
'detail': 'https://kickass.to/%s',
|
'search': '%s/%s-i%s/',
|
||||||
'search': 'https://kickass.to/%s-i%s/',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cat_ids = [
|
cat_ids = [
|
||||||
@@ -28,9 +27,16 @@ class KickAssTorrents(TorrentMagnetProvider):
|
|||||||
http_time_between_calls = 1 #seconds
|
http_time_between_calls = 1 #seconds
|
||||||
cat_backup_id = None
|
cat_backup_id = None
|
||||||
|
|
||||||
|
proxy_list = [
|
||||||
|
'https://kickass.to',
|
||||||
|
'http://kickass.pw',
|
||||||
|
'http://www.kickassunblock.info',
|
||||||
|
'http://www.kickassproxy.info',
|
||||||
|
]
|
||||||
|
|
||||||
def _search(self, movie, quality, results):
|
def _search(self, movie, quality, results):
|
||||||
|
|
||||||
data = self.getHTMLData(self.urls['search'] % ('m', movie['library']['identifier'].replace('tt', '')))
|
data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', movie['library']['identifier'].replace('tt', '')))
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
|
|
||||||
@@ -41,7 +47,7 @@ class KickAssTorrents(TorrentMagnetProvider):
|
|||||||
html = BeautifulSoup(data)
|
html = BeautifulSoup(data)
|
||||||
resultdiv = html.find('div', attrs = {'class':'tabs'})
|
resultdiv = html.find('div', attrs = {'class':'tabs'})
|
||||||
for result in resultdiv.find_all('div', recursive = False):
|
for result in resultdiv.find_all('div', recursive = False):
|
||||||
if result.get('id').lower() not in cat_ids:
|
if result.get('id').lower().strip('tab-') not in cat_ids:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -56,12 +62,12 @@ class KickAssTorrents(TorrentMagnetProvider):
|
|||||||
column_name = table_order[nr]
|
column_name = table_order[nr]
|
||||||
if column_name:
|
if column_name:
|
||||||
|
|
||||||
if column_name is 'name':
|
if column_name == 'name':
|
||||||
link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
|
link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
|
||||||
new['id'] = temp.get('id')[-8:]
|
new['id'] = temp.get('id')[-8:]
|
||||||
new['name'] = link.text
|
new['name'] = link.text
|
||||||
new['url'] = td.find('a', 'imagnet')['href']
|
new['url'] = td.find('a', 'imagnet')['href']
|
||||||
new['detail_url'] = self.urls['detail'] % link['href'][1:]
|
new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:])
|
||||||
new['score'] = 20 if td.find('a', 'iverif') else 0
|
new['score'] = 20 if td.find('a', 'iverif') else 0
|
||||||
elif column_name is 'size':
|
elif column_name is 'size':
|
||||||
new['size'] = self.parseSize(td.text)
|
new['size'] = self.parseSize(td.text)
|
||||||
@@ -100,3 +106,10 @@ class KickAssTorrents(TorrentMagnetProvider):
|
|||||||
age += tryInt(nr) * mult
|
age += tryInt(nr) * mult
|
||||||
|
|
||||||
return tryInt(age)
|
return tryInt(age)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabled(self):
|
||||||
|
return super(KickAssTorrents, self).isEnabled() and self.getDomain()
|
||||||
|
|
||||||
|
def correctProxy(self, data):
|
||||||
|
return 'search query' in data.lower()
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
from bs4 import BeautifulSoup
|
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SceneHD(TorrentProvider):
|
|
||||||
|
|
||||||
urls = {
|
|
||||||
'test': 'https://scenehd.org/',
|
|
||||||
'login' : 'https://scenehd.org/takelogin.php',
|
|
||||||
'login_check': 'https://scenehd.org/my.php',
|
|
||||||
'detail': 'https://scenehd.org/details.php?id=%s',
|
|
||||||
'search': 'https://scenehd.org/browse.php?ajax',
|
|
||||||
'download': 'https://scenehd.org/download.php?id=%s',
|
|
||||||
}
|
|
||||||
|
|
||||||
http_time_between_calls = 1 #seconds
|
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
|
||||||
|
|
||||||
q = '"%s %s"' % (simplifyString(title), movie['library']['year'])
|
|
||||||
arguments = tryUrlencode({
|
|
||||||
'search': q,
|
|
||||||
})
|
|
||||||
url = "%s&%s" % (self.urls['search'], arguments)
|
|
||||||
|
|
||||||
data = self.getHTMLData(url, opener = self.login_opener)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
html = BeautifulSoup(data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
resultsTable = html.find_all('table')[6]
|
|
||||||
entries = resultsTable.find_all('tr')
|
|
||||||
for result in entries[1:]:
|
|
||||||
|
|
||||||
all_cells = result.find_all('td')
|
|
||||||
|
|
||||||
detail_link = all_cells[2].find('a')
|
|
||||||
details = detail_link['href']
|
|
||||||
torrent_id = details.replace('details.php?id=', '')
|
|
||||||
|
|
||||||
leechers = all_cells[11].find('a')
|
|
||||||
if leechers:
|
|
||||||
leechers = leechers.string
|
|
||||||
else:
|
|
||||||
leechers = all_cells[11].string
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': torrent_id,
|
|
||||||
'name': detail_link['title'],
|
|
||||||
'size': self.parseSize(all_cells[7].string),
|
|
||||||
'seeders': tryInt(all_cells[10].find('a').string),
|
|
||||||
'leechers': tryInt(leechers),
|
|
||||||
'url': self.urls['download'] % torrent_id,
|
|
||||||
'description': all_cells[1].find('a')['href'],
|
|
||||||
})
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
|
|
||||||
def getLoginParams(self):
|
|
||||||
return tryUrlencode({
|
|
||||||
'username': self.conf('username'),
|
|
||||||
'password': self.conf('password'),
|
|
||||||
'ssl': 'yes',
|
|
||||||
})
|
|
||||||
|
|
||||||
def loginSuccess(self, output):
|
|
||||||
return 'logout.php' in output.lower()
|
|
||||||
|
|
||||||
loginCheckSuccess = loginSuccess
|
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
'type': 'enabler',
|
'type': 'enabler',
|
||||||
'default': True
|
'default': False
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'domain',
|
'name': 'domain',
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||||
from couchpotato.core.helpers.variable import tryInt, cleanHost
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.torrent.base import TorrentMagnetProvider
|
from couchpotato.core.providers.torrent.base import TorrentMagnetProvider
|
||||||
from couchpotato.environment import Env
|
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -30,8 +28,8 @@ class ThePirateBay(TorrentMagnetProvider):
|
|||||||
http_time_between_calls = 0
|
http_time_between_calls = 0
|
||||||
|
|
||||||
proxy_list = [
|
proxy_list = [
|
||||||
'https://thepiratebay.se',
|
|
||||||
'https://tpb.ipredator.se',
|
'https://tpb.ipredator.se',
|
||||||
|
'https://thepiratebay.se',
|
||||||
'https://depiraatbaai.be',
|
'https://depiraatbaai.be',
|
||||||
'https://piratereverse.info',
|
'https://piratereverse.info',
|
||||||
'https://tpb.pirateparty.org.uk',
|
'https://tpb.pirateparty.org.uk',
|
||||||
@@ -43,10 +41,6 @@ class ThePirateBay(TorrentMagnetProvider):
|
|||||||
'https://kuiken.co',
|
'https://kuiken.co',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.domain = self.conf('domain')
|
|
||||||
super(ThePirateBay, self).__init__()
|
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
page = 0
|
page = 0
|
||||||
@@ -108,38 +102,11 @@ class ThePirateBay(TorrentMagnetProvider):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
def isEnabled(self):
|
def isEnabled(self):
|
||||||
return super(ThePirateBay, self).isEnabled() and self.getDomain()
|
return super(ThePirateBay, self).isEnabled() and self.getDomain()
|
||||||
|
|
||||||
def getDomain(self, url = ''):
|
def correctProxy(self, data):
|
||||||
|
return 'title="Pirate Search"' in data
|
||||||
if not self.domain:
|
|
||||||
for proxy in self.proxy_list:
|
|
||||||
|
|
||||||
prop_name = 'tpb_proxy.%s' % proxy
|
|
||||||
last_check = float(Env.prop(prop_name, default = 0))
|
|
||||||
if last_check > time.time() - 1209600:
|
|
||||||
continue
|
|
||||||
|
|
||||||
data = ''
|
|
||||||
try:
|
|
||||||
data = self.urlopen(proxy, timeout = 3, show_error = False)
|
|
||||||
except:
|
|
||||||
log.debug('Failed tpb proxy %s', proxy)
|
|
||||||
|
|
||||||
if 'title="Pirate Search"' in data:
|
|
||||||
log.debug('Using proxy: %s', proxy)
|
|
||||||
self.domain = proxy
|
|
||||||
break
|
|
||||||
|
|
||||||
Env.prop(prop_name, time.time())
|
|
||||||
|
|
||||||
if not self.domain:
|
|
||||||
log.error('No TPB proxies left, please add one in settings, or let us know which one to add on the forum.')
|
|
||||||
return None
|
|
||||||
|
|
||||||
return cleanHost(self.domain).rstrip('/') + url
|
|
||||||
|
|
||||||
def getMoreInfo(self, item):
|
def getMoreInfo(self, item):
|
||||||
full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class TorrentShack(TorrentProvider):
|
|||||||
'login' : 'https://torrentshack.net/login.php',
|
'login' : 'https://torrentshack.net/login.php',
|
||||||
'login_check': 'https://torrentshack.net/inbox.php',
|
'login_check': 'https://torrentshack.net/inbox.php',
|
||||||
'detail' : 'https://torrentshack.net/torrent/%s',
|
'detail' : 'https://torrentshack.net/torrent/%s',
|
||||||
'search' : 'https://torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1',
|
'search' : 'https://torrentshack.net/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
|
||||||
'download' : 'https://torrentshack.net/%s',
|
'download' : 'https://torrentshack.net/%s',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,9 @@ class TorrentShack(TorrentProvider):
|
|||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
url = self.urls['search'] % (tryUrlencode('"%s" %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
|
scene_only = '1' if self.conf('scene_only') else ''
|
||||||
|
|
||||||
|
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), scene_only, self.getCatId(quality['identifier'])[0])
|
||||||
data = self.getHTMLData(url, opener = self.login_opener)
|
data = self.getHTMLData(url, opener = self.login_opener)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
@@ -49,22 +51,15 @@ class TorrentShack(TorrentProvider):
|
|||||||
link = result.find('span', attrs = {'class' : 'torrent_name_link'}).parent
|
link = result.find('span', attrs = {'class' : 'torrent_name_link'}).parent
|
||||||
url = result.find('td', attrs = {'class' : 'torrent_td'}).find('a')
|
url = result.find('td', attrs = {'class' : 'torrent_td'}).find('a')
|
||||||
|
|
||||||
extra_info = ''
|
results.append({
|
||||||
if result.find('span', attrs = {'class' : 'torrent_extra_info'}):
|
'id': link['href'].replace('torrents.php?torrentid=', ''),
|
||||||
extra_info = result.find('span', attrs = {'class' : 'torrent_extra_info'}).text
|
'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
|
||||||
|
'url': self.urls['download'] % url['href'],
|
||||||
if not self.conf('scene_only') or extra_info != '[NotScene]':
|
'detail_url': self.urls['download'] % link['href'],
|
||||||
results.append({
|
'size': self.parseSize(result.find_all('td')[4].string),
|
||||||
'id': link['href'].replace('torrents.php?torrentid=', ''),
|
'seeders': tryInt(result.find_all('td')[6].string),
|
||||||
'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
|
'leechers': tryInt(result.find_all('td')[7].string),
|
||||||
'url': self.urls['download'] % url['href'],
|
})
|
||||||
'detail_url': self.urls['download'] % link['href'],
|
|
||||||
'size': self.parseSize(result.find_all('td')[4].string),
|
|
||||||
'seeders': tryInt(result.find_all('td')[6].string),
|
|
||||||
'leechers': tryInt(result.find_all('td')[7].string),
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
log.info('Not adding release %s [NotScene]' % unicode(link.span.string).translate({ord(u'\xad'): None}))
|
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ class Yify(TorrentProvider):
|
|||||||
|
|
||||||
return super(Yify, self).search(movie, quality)
|
return super(Yify, self).search(movie, quality)
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _search(self, movie, quality, results):
|
||||||
|
|
||||||
data = self.getJsonData(self.urls['search'] % (title, quality['identifier']))
|
data = self.getJsonData(self.urls['search'] % (movie['library']['identifier'], quality['identifier']))
|
||||||
|
|
||||||
if data and data.get('MovieList'):
|
if data and data.get('MovieList'):
|
||||||
try:
|
try:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user