Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2013-10-07 09:20:57 +02:00
18 changed files with 336 additions and 87 deletions
@@ -12,6 +12,7 @@ config = [{
'name': 'nzbget',
'label': 'NZBGet',
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
'wizard': True,
'options': [
{
'name': 'enabled',
@@ -35,6 +35,11 @@ config = [{
'name': 'label',
'description': 'Label to apply on added torrents.',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Directory where rtorrent should download the files too.',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
@@ -43,6 +48,14 @@ config = [{
'type': 'bool',
'description': 'Remove the torrent after it finishes seeding.',
},
{
'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': 'delete_files',
'label': 'Remove files',
+11 -1
View File
@@ -7,7 +7,7 @@ from datetime import timedelta
from hashlib import sha1
from rtorrent import RTorrent
from rtorrent.err import MethodError
import shutil
import shutil, os
log = CPLog(__name__)
@@ -91,6 +91,7 @@ class rTorrent(Downloader):
if self.conf('label'):
torrent_params['label'] = self.conf('label')
if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data')
return False
@@ -116,10 +117,19 @@ class rTorrent(Downloader):
# Send torrent to rTorrent
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
if 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
torrent.set_visible(group_name)
@@ -90,9 +90,14 @@ class Sabnzbd(Downloader):
# Get busy releases
for item in queue.get('slots', []):
status = 'busy'
if 'ENCRYPTED / ' in item['filename']:
status = 'failed'
statuses.append({
'id': item['nzo_id'],
'name': item['filename'],
'status': status,
'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
})
@@ -122,6 +127,12 @@ class Sabnzbd(Downloader):
log.info('%s failed downloading, deleting...', item['name'])
try:
self.call({
'mode': 'queue',
'name': 'delete',
'del_files': '1',
'value': item['id']
}, use_json = False)
self.call({
'mode': 'history',
'name': 'delete',
+3 -13
View File
@@ -60,7 +60,7 @@ class Searcher(SearcherBase):
if downloader_enabled:
snatched_status = fireEvent('status.get', 'snatched', single = True)
snatched_status, done_status, active_status = fireEvent('status.get', ['snatched', 'done', 'active'], single = True)
# Download movie to temp
filedata = None
@@ -79,9 +79,7 @@ class Searcher(SearcherBase):
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')
fireEvent('release.update_status', rls.id, status = done_status if not renamer_enabled else snatched_status, single = True)
# Save download-id info if returned
if isinstance(download_result, dict):
@@ -100,20 +98,12 @@ class Searcher(SearcherBase):
# 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
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
mvie.last_edit = int(time.time())
@@ -241,7 +241,6 @@ MA.Release = new Class({
}
})
).inject(self.release_container);
release['el'] = item;
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'){
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)
@@ -397,17 +420,6 @@ MA.Release = new Class({
'data': {
'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');
}
}
})
},
@@ -419,22 +419,25 @@
}
.movies .data .quality .available,
.movies .data .quality .snatched {
.movies .data .quality .snatched,
.movies .data .quality .seeding {
opacity: 1;
cursor: pointer;
}
.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 .seeding { background-color: #0a6819; }
.movies .data .quality .done {
background-color: #369545;
opacity: 1;
}
.movies .data .quality .seeding { background-color: #0a6819; }
.movies .data .quality .finish {
background-image: url('../images/sprite.png');
background-repeat: no-repeat;
background-repeat: no-repeat;
background-position: 0 2px;
padding-left: 14px;
background-size: 14px
@@ -646,7 +649,7 @@
margin-top: 25px;
}
}
.trailer_container.hide {
height: 0 !important;
}
@@ -1029,7 +1032,7 @@
.movies .progress > div .folder {
display: inline-block;
padding: 5px 20px 5px 0;
white-space: nowrap;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 85%;
@@ -185,7 +185,7 @@ var Movie = new Class({
var q = self.quality.getElement('.q_id'+ release.quality_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)
if (status && q && !q.hasClass(status.identifier)){
+2 -2
View File
@@ -52,7 +52,7 @@ class Plex(Notification):
clients.remove(server.get('name').lower())
protocol = server.get('protocol', 'xbmchttp')
if protocol in ['xbmcjson', 'xbmchttp']:
if protocol in ['plex', 'xbmcjson', 'xbmchttp']:
self.clients[server.get('name')] = {
'name': server.get('name'),
'address': server.get('address'),
@@ -164,7 +164,7 @@ class Plex(Notification):
}
for name, client in self.clients.items():
if client['protocol'] == 'xbmcjson':
if client['protocol'] in ['xbmcjson', 'plex']:
total += 1
if self.sendJSON('GUI.ShowNotification', params, client):
successful += 1
+24 -8
View File
@@ -10,6 +10,7 @@ from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import and_, or_
import os
import traceback
import time
log = CPLog(__name__)
@@ -47,6 +48,7 @@ class Release(Plugin):
addEvent('release.for_movie', self.forMovie)
addEvent('release.delete', self.delete)
addEvent('release.clean', self.clean)
addEvent('release.update_status', self.updateStatus)
def add(self, group):
@@ -159,8 +161,7 @@ class Release(Plugin):
rel = db.query(Relea).filter_by(id = id).first()
if rel:
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')
db.commit()
self.updateStatus(id, available_status if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status)
return {
'success': True
@@ -199,14 +200,9 @@ class Release(Plugin):
if success:
db.expunge_all()
rel = db.query(Relea).filter_by(id = id).first() # Get release again
if rel.status_id != done_status.get('id'):
rel.status_id = snatched_status.get('id')
db.commit()
rel = db.query(Relea).filter_by(id = id).first() # Get release again @RuudBurger why do we need to get it again??
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return {
'success': success
}
@@ -241,3 +237,23 @@ class Release(Plugin):
'success': True
}
def updateStatus(self, id, status = None):
if not status: return
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
#update status in Db
log.debug('Marking release %s as %s', (item['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'))
+40 -35
View File
@@ -395,14 +395,8 @@ class Renamer(Plugin):
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()
# Set the release to downloaded
fireEvent('release.update_status', release.id, status = downloaded_status, single = True)
# Remove leftover files
if not remove_leftovers: # Don't remove anything
@@ -476,11 +470,18 @@ class Renamer(Plugin):
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(download_info):
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 = os.path.join(self.conf('from'), os.path.relpath(group['parentdir'], self.conf('from')).split(os.path.sep)[0])
try:
log.info('Deleting folder: %s', group['parentdir'])
self.deleteEmptyFolder(group['parentdir'])
log.info('Deleting folder: %s', group_folder)
self.deleteEmptyFolder(group_folder)
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
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
@@ -656,12 +657,13 @@ Remove it if you want it to be renamed (again, or at least let it try again)
self.checking_snatched = True
snatched_status, ignored_status, failed_status, done_status, seeding_status, downloaded_status = \
fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done', 'seeding', 'downloaded'], single = True)
snatched_status, ignored_status, failed_status, done_status, seeding_status, downloaded_status, missing_status = \
fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done', 'seeding', 'downloaded', 'missing'], single = True)
db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
rels.extend(db.query(Release).filter_by(status_id = seeding_status.get('id')).all())
rels = db.query(Release).filter(
Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')])
).all()
scan_items = []
scan_required = False
@@ -677,7 +679,6 @@ Remove it if you want it to be renamed (again, or at least let it try again)
try:
for rel in rels:
rel_dict = rel.to_dict({'info': {}})
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
# check status
@@ -699,39 +700,36 @@ Remove it if you want it to be renamed (again, or at least let it try again)
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
if item['status'] == 'busy':
# Set the release to snatched if it was missing before
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
# 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']:
self.tagDir(item['folder'], 'downloading')
elif item['status'] == 'seeding':
# Set the release to seeding
fireEvent('release.update_status', rel.id, status = seeding_status, single = True)
#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(item):
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']))
# Remove the downloading tag
self.untagDir(item['folder'], '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
item.update({'pause': True, 'scan': True, 'process_complete': False})
scan_items.append(item)
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
log.debug('%s is seeding with ratio: %s', (item['name'], item['seed_ratio']))
elif item['status'] == 'failed':
# Set the release to failed
fireEvent('release.update_status', rel.id, status = failed_status, single = True)
fireEvent('download.remove_failed', item, single = True)
rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time())
db.commit()
if self.conf('next_on_failed'):
fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id)
@@ -743,24 +741,23 @@ Remove it if you want it to be renamed (again, or at least let it try again)
if rel.status_id == seeding_status.get('id'):
if rel.movie.status_id == done_status.get('id'):
# Set the release to done as the movie has already been renamed
rel.status_id = downloaded_status.get('id')
rel.last_edit = int(time.time())
db.commit()
fireEvent('release.update_status', rel.id, status = downloaded_status, single = True)
# Allow the downloader to clean-up
item.update({'pause': False, 'scan': False, 'process_complete': True})
scan_items.append(item)
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()
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
# Scan and Allow the downloader to clean-up
item.update({'pause': False, 'scan': True, 'process_complete': True})
scan_items.append(item)
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
self.untagDir(item['folder'], 'downloading')
@@ -776,6 +773,14 @@ Remove it if you want it to be renamed (again, or at least let it try again)
if not found:
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:
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
+1
View File
@@ -24,6 +24,7 @@ class StatusPlugin(Plugin):
'available': 'Available',
'suggest': 'Suggest',
'seeding': 'Seeding',
'missing': 'Missing',
}
status_cached = {}
@@ -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
@@ -16,7 +16,7 @@ config = [{
{
'name': 'enabled',
'type': 'enabler',
'default': True
'default': False
},
{
'name': 'domain',
@@ -12,7 +12,7 @@ class TorrentBytes(TorrentProvider):
urls = {
'test' : 'https://www.torrentbytes.net/',
'login' : 'https://www.torrentbytes.net/takelogin.php',
'login' : 'https://www.torrentbytes.net/login.php',
'login_check' : 'https://www.torrentbytes.net/inbox.php',
'detail' : 'https://www.torrentbytes.net/details.php?id=%s',
'search' : 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d',
+2 -4
View File
@@ -71,12 +71,10 @@ class RTorrent:
def _verify_conn(self):
# check for rpc methods that should be available
assert {"system.client_version",
"system.library_version"}.issubset(set(self._get_rpc_methods())),\
"Required RPC methods not available."
assert "system.client_version" in self._get_rpc_methods(), "Required RPC method not available."
assert "system.library_version" in self._get_rpc_methods(), "Required RPC method not available."
# minimum rTorrent version check
assert self._meets_version_requirement() is True,\
"Error: Minimum rTorrent version required is {0}".format(
MIN_RTORRENT_VERSION_STR)
+4 -3
View File
@@ -90,9 +90,10 @@ class TorrentParser():
def _calc_info_hash(self):
self.info_hash = None
if "info" in self._torrent_decoded.keys():
info_dict = self._torrent_decoded["info"]
self.info_hash = hashlib.sha1(bencode.encode(
info_dict)).hexdigest().upper()
info_encoded = bencode.encode(self._torrent_decoded["info"])
if info_encoded:
self.info_hash = hashlib.sha1(info_encoded).hexdigest().upper()
return(self.info_hash)