Merge branch 'refs/heads/develop' into desktop

Conflicts:
	CouchPotato.py
This commit is contained in:
Ruud
2012-02-19 13:14:56 +01:00
47 changed files with 3425 additions and 360 deletions
-23
View File
@@ -8,7 +8,6 @@ from couchpotato.environment import Env
from flask import request
from uuid import uuid4
import os
import thread
import time
import traceback
import webbrowser
@@ -32,7 +31,6 @@ class Core(Plugin):
addEvent('app.crappy_shutdown', self.crappyShutdown)
addEvent('app.crappy_restart', self.crappyRestart)
addEvent('app.load', self.launchBrowser, priority = 1)
addEvent('app.load', self.monitorParent)
addEvent('app.base_url', self.createBaseUrl)
addEvent('app.api_url', self.createApiUrl)
@@ -41,27 +39,6 @@ class Core(Plugin):
self.removeRestartFile()
def monitorParent(self):
def looping():
do_shutdown = False
while 1 and not self.shuttingDown():
if os.name == 'nt':
if os.getppid(os.getpid()) <= 1:
do_shutdown = True
break
else:
if os.getppid() <= 1:
do_shutdown = True
break
time.sleep(1)
if do_shutdown:
log.info('Starterscript has shutdown, shutdown subprocess')
fireEvent('app.crappy_shutdown')
thread.start_new_thread(looping, ())
def md5Password(self, value):
return md5(value) if value else ''
+9 -1
View File
@@ -64,7 +64,7 @@ class Updater(Plugin):
return
log.info('Checking for new version on github for %s' % self.repo_name)
if not Env.setting('development'):
if not Env.get('dev'):
self.repo.fetch()
current_branch = self.repo.getCurrentBranch().name
@@ -135,5 +135,13 @@ class Updater(Plugin):
except:
log.error('Couldn\'t remove %s: %s' % (full_path, traceback.format_exc()))
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
if len(os.listdir(full_path)) == 0:
try:
os.rmdir(full_path)
except:
log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc()))
def isEnabled(self):
return super(Updater, self).isEnabled() and Env.get('uses_git')
+1 -1
View File
@@ -1,8 +1,8 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from couchpotato.core.helpers.encoding import toSafeString
import os
log = CPLog(__name__)
@@ -42,6 +42,6 @@ class Blackhole(Downloader):
pass
except:
log.debug('Failed to download file: %s' % data.get('name'))
log.debug('Failed to download file %s: %s' % (data.get('name'), traceback.format_exc()))
return False
return False
@@ -29,6 +29,7 @@ config = [{
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'paused',
@@ -43,6 +44,7 @@ config = [{
{
'name': 'ratio',
'default': 10,
'type': 'int',
'advanced': True,
'description': 'Stop transfer when reaching ratio',
},
@@ -1,3 +1,4 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog
@@ -17,7 +18,6 @@ class Transmission(Downloader):
log.info('Sending "%s" to Transmission.' % data.get('name'))
# Load host from config and split out port.
host = self.conf('host').split(':')
if not isInt(host[1]):
@@ -26,24 +26,21 @@ class Transmission(Downloader):
# Set parameters for Transmission
params = {
'paused': self.conf('paused', 0),
'download_dir': self.conf('directory', None)
}
change_params = {
'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': 1 if self.conf('ratio') else 0
'paused': self.conf('paused', default = 0),
'download_dir': self.conf('directory', default = None)
}
try:
tc = transmissionrpc.Client(host[0], port = host[1], user = self.conf('username'), password = self.conf('password'))
tr_id = tc.add_uri(data.get('url'), **params)
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
torrent = tc.add_torrent(b64encode(filedata), **params)
# Change settings of added torrents
for item in tr_id:
try:
tc.change(item, timeout = None, **change_params)
except transmissionrpc.TransmissionError, e:
log.error('Failed to change settings for transfer in transmission: %s' % e)
try:
torrent.seed_ratio_limit = self.conf('ratio')
torrent.seed_ratio_mode = 'single' if self.conf('ratio') else 'global'
except transmissionrpc.TransmissionError, e:
log.error('Failed to change settings for transfer in transmission: %s' % e)
return True
+2
View File
@@ -34,6 +34,8 @@ def mergeDicts(a, b):
else:
if isDict(current_src[key]) and isDict(current_dst[key]):
stack.append((current_dst[key], current_src[key]))
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
current_dst[key].extend(current_src[key])
else:
current_dst[key] = current_src[key]
return dst
+13 -11
View File
@@ -32,9 +32,9 @@ class Loader(object):
self.paths[provider + '_provider'] = (25, 'couchpotato.core.providers.' + provider, path)
for type, tuple in self.paths.iteritems():
priority, module, dir = tuple
self.addFromDir(type, priority, module, dir)
for plugin_type, plugin_tuple in self.paths.iteritems():
priority, module, dir_name = plugin_tuple
self.addFromDir(plugin_type, priority, module, dir_name)
def run(self):
did_save = 0
@@ -51,19 +51,21 @@ class Loader(object):
did_save += self.loadSettings(m, module_name, save = False)
self.loadPlugins(m, plugin.get('name'))
except Exception, e:
except ImportError:
log.debug('Import error, remove the empty folder: %s' % plugin.get('module'))
except:
log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc()))
if did_save:
fireEvent('settings.save')
def addFromDir(self, type, priority, module, dir):
def addFromDir(self, plugin_type, priority, module, dir_name):
for cur_file in glob.glob(os.path.join(dir, '*')):
for cur_file in glob.glob(os.path.join(dir_name, '*')):
name = os.path.basename(cur_file)
if os.path.isdir(os.path.join(dir, name)):
if os.path.isdir(os.path.join(dir_name, name)):
module_name = '%s.%s' % (module, name)
self.addModule(priority, type, module_name, name)
self.addModule(priority, plugin_type, module_name, name)
def loadSettings(self, module, name, save = True):
try:
@@ -75,7 +77,7 @@ class Loader(object):
options[option['name']] = option
fireEvent('settings.register', section_name = section['name'], options = options, save = save)
return True
except Exception, e:
except:
log.debug("Failed loading settings for '%s': %s" % (name, traceback.format_exc()))
return False
@@ -92,7 +94,7 @@ class Loader(object):
log.error("Failed loading plugin '%s': %s" % (module.__file__, traceback.format_exc()))
return False
def addModule(self, priority, type, module, name):
def addModule(self, priority, plugin_type, module, name):
if not self.modules.get(priority):
self.modules[priority] = {}
@@ -100,7 +102,7 @@ class Loader(object):
self.modules[priority][module] = {
'priority': priority,
'module': module,
'type': type,
'type': plugin_type,
'name': name,
}
+6 -2
View File
@@ -22,6 +22,9 @@ class CPLog(object):
def error(self, msg):
self.logger.error(self.addContext(msg))
def warning(self, msg):
self.logger.warning(self.addContext(msg))
def critical(self, msg):
self.logger.critical(self.addContext(msg), exc_info = 1)
@@ -35,7 +38,7 @@ class CPLog(object):
pass
from couchpotato.environment import Env
if not Env.setting('development'):
if not Env.get('dev'):
for replace in self.replace_private:
msg = re.sub('(%s=)[^\&]+' % replace, '%s=xxx' % replace, msg)
@@ -43,7 +46,8 @@ class CPLog(object):
# Replace api key
try:
api_key = Env.setting('api_key')
msg = msg.replace(api_key, 'API_KEY')
if api_key:
msg = msg.replace(api_key, 'API_KEY')
except:
pass
@@ -23,7 +23,7 @@ var NotificationBase = new Class({
self.request = Api.request('core_notifier.listener', {
'initialDelay': 100,
'delay': 3000,
'onComplete': self.processData.bind(self)
'onSuccess': self.processData.bind(self)
})
self.request.startTimer()
+18 -16
View File
@@ -3,8 +3,6 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from gntp import notifier
import logging
import thread
import time
import traceback
log = CPLog(__name__)
@@ -12,31 +10,35 @@ log = CPLog(__name__)
class Growl(Notification):
registered = False
def __init__(self):
super(Growl, self).__init__()
logging.getLogger('gntp').setLevel(logging.WARNING)
if self.isEnabled():
self.register()
def register(self):
if self.registered: return
try:
def startGrowl():
time.sleep(2)
try:
self.growl = notifier.GrowlNotifier(
applicationName = 'CouchPotato',
notifications = ["Updates"],
defaultNotifications = ["Updates"],
applicationIcon = '%s/static/images/couch.png' % fireEvent('app.api_url', single = True),
)
self.growl.register()
except:
log.error('Failed register of growl: %s' % traceback.format_exc())
thread.start_new_thread(startGrowl, ())
self.growl = notifier.GrowlNotifier(
applicationName = 'CouchPotato',
notifications = ["Updates"],
defaultNotifications = ["Updates"],
applicationIcon = '%s/static/images/couch.png' % fireEvent('app.api_url', single = True),
)
self.growl.register()
self.registered = True
except:
pass
log.error('Failed register of growl: %s' % traceback.format_exc())
def notify(self, type = '', message = '', data = {}):
if self.isDisabled(): return
self.register()
try:
self.growl.notify(
noteType = "Updates",
+1 -1
View File
@@ -10,7 +10,7 @@ class Automation(Plugin):
def __init__(self):
if not Env.setting('development'):
if not Env.get('dev'):
addEvent('app.load', self.addMovies)
def addMovies(self):
+1 -1
View File
@@ -174,7 +174,7 @@ class Plugin(object):
def getCache(self, cache_key, url = None, timeout = 300):
cache = Env.get('cache').get(cache_key)
if cache:
if not Env.setting('development'): log.debug('Getting cache %s' % cache_key)
if not Env.get('dev'): log.debug('Getting cache %s' % cache_key)
return cache
if url:
+16 -14
View File
@@ -29,7 +29,7 @@ class FileManager(Plugin):
from flask.helpers import send_from_directory
return send_from_directory(cache_dir, filename)
def download(self, url = '', dest = None, overwrite = False):
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
if not dest: # to Cache
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
@@ -38,14 +38,15 @@ class FileManager(Plugin):
return dest
try:
filedata = self.urlopen(url)
filedata = self.urlopen(url, **urlopen_kwargs)
except:
return False
self.createFile(dest, filedata, binary = True)
return dest
def add(self, path = '', part = 1, type = (), available = 1, properties = {}):
def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = {}):
type_id = self.getType(type_tuple).get('id')
db = get_session()
f = db.query(File).filter(File.path == toUnicode(path)).first()
@@ -56,7 +57,7 @@ class FileManager(Plugin):
f.path = toUnicode(path)
f.part = part
f.available = available
f.type_id = self.getType(type).id
f.type_id = type_id
db.commit()
@@ -64,23 +65,24 @@ class FileManager(Plugin):
return file_dict
def getType(self, type):
def getType(self, type_tuple):
db = get_session()
type, identifier = type
type_type, type_identifier = type_tuple
ft = db.query(FileType).filter_by(identifier = identifier).first()
ft = db.query(FileType).filter_by(identifier = type_identifier).first()
if not ft:
ft = FileType(
type = toUnicode(type),
identifier = identifier,
name = toUnicode(identifier[0].capitalize() + identifier[1:])
type = toUnicode(type_type),
identifier = type_identifier,
name = toUnicode(type_identifier[0].capitalize() + type_identifier[1:])
)
db.add(ft)
db.commit()
return ft
type_dict = ft.to_dict()
db.remove()
return type_dict
def getTypes(self):
@@ -89,7 +91,7 @@ class FileManager(Plugin):
results = db.query(FileType).all()
types = []
for type in results:
types.append(type.to_dict())
for type_object in results:
types.append(type_object.to_dict())
return types
+1 -1
View File
@@ -102,7 +102,7 @@ class LibraryPlugin(Plugin):
continue
file_path = fireEvent('file.download', url = image, single = True)
file_obj = fireEvent('file.add', path = file_path, type = ('image', type), single = True)
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
@@ -66,6 +66,7 @@ Page.Log = new Class({
text = text.replace(/\u001b\[31m/gi, '</span><span class="error">')
text = text.replace(/\u001b\[36m/gi, '</span><span class="debug">')
text = text.replace(/\u001b\[33m/gi, '</span><span class="debug">')
text = text.replace(/\u001b\[0m\n/gi, '</span><span class="time">')
text = text.replace(/\u001b\[0m/gi, '</span><span>')
+1 -1
View File
@@ -23,7 +23,7 @@ class Manage(Plugin):
addEvent('manage.update', self.updateLibrary)
addApiView('manage.update', self.updateLibraryView)
if not Env.setting('development'):
if not Env.get('dev'):
addEvent('app.load', self.updateLibrary)
def updateLibraryView(self):
@@ -183,7 +183,7 @@ Block.Search.Item = new Class({
'click': self.showOptions.bind(self)
}
}).adopt(
self.thumbnail = info.images.poster.length > 0 ? new Element('img.thumbnail', {
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0]
}) : null,
new Element('div.info').adopt(
@@ -288,7 +288,7 @@ Block.Search.Item = new Class({
self.options.adopt(
new Element('div').adopt(
self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.poster[0]
}) : null,
self.info.in_wanted ? new Element('span.in_wanted', {
+1 -1
View File
@@ -79,7 +79,7 @@ class Release(Plugin):
properties = {}
# Check database and update/insert if necessary
return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type = Scanner.file_types.get(type), properties = properties, single = True)
return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True)
def delete(self):
+48 -14
View File
@@ -3,12 +3,12 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.helpers.variable import getExt, mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library
from couchpotato.core.settings.model import Library, File
from couchpotato.environment import Env
import os.path
import os
import re
import shutil
import traceback
@@ -123,6 +123,9 @@ class Renamer(Plugin):
# Move nfo depending on settings
if file_type is 'nfo' and not self.conf('rename_nfo'):
log.debug('Skipping, renaming of %s disabled' % file_type)
if self.conf('clean_up'):
for current_file in group['files'][file_type]:
remove_files.append(current_file)
continue
# Subtitle extra
@@ -190,19 +193,30 @@ class Renamer(Plugin):
# Check for extra subtitle files
if file_type is 'subtitle':
def test(s):
return current_file[:-len(replacements['ext'])] in s
for subtitle_extra in set(filter(test, group['files']['subtitle_extra'])):
replacements['ext'] = getExt(subtitle_extra)
final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements)
rename_files[subtitle_extra] = os.path.join(destination, final_folder_name, final_file_name)
rename_extras = self.getRenameExtras(
extra_type = 'subtitle_extra',
replacements = replacements,
folder_name = folder_name,
file_name = file_name,
destination = destination,
group = group,
current_file = current_file
)
rename_files = mergeDicts(rename_files, rename_extras)
# Filename without cd etc
if file_type is 'movie':
rename_extras = self.getRenameExtras(
extra_type = 'movie_extra',
replacements = replacements,
folder_name = folder_name,
file_name = file_name,
destination = destination,
group = group,
current_file = current_file
)
rename_files = mergeDicts(rename_files, rename_extras)
group['destination_dir'] = os.path.join(destination, final_folder_name)
if multiple:
@@ -290,11 +304,15 @@ class Renamer(Plugin):
# Remove files
for src in remove_files:
if isinstance(src, File):
src = src.path
log.info('(fake) Removing "%s"' % src)
# Remove matching releases
for release in remove_releases:
log.info('(fake) Removing release %s' % release)
log.info('(fake) Removing release %s' % release.identifier)
# Search for trailers etc
fireEventAsync('renamer.after', group)
@@ -309,6 +327,22 @@ class Renamer(Plugin):
self.renaming_started = False
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''):
rename_files = {}
def test(s):
return current_file[:-len(replacements['ext'])] in s
for extra in set(filter(test, group['files'][extra_type])):
replacements['ext'] = getExt(extra)
final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements)
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
return rename_files
def moveFile(self, old, dest):
try:
shutil.move(old, dest)
+11 -1
View File
@@ -29,7 +29,8 @@ class Scanner(Plugin):
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.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']
extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'],
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf'],
'movie_extra': ['mds'],
'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'],
'subtitle': ['sub', 'srt', 'ssa', 'ass'],
@@ -42,6 +43,7 @@ class Scanner(Plugin):
'trailer': ('video', 'trailer'),
'nfo': ('nfo', 'nfo'),
'movie': ('video', 'movie'),
'movie': ('movie', 'movie_extra'),
'backdrop': ('image', 'backdrop'),
'leftover': ('leftover', 'leftover'),
}
@@ -267,6 +269,7 @@ class Scanner(Plugin):
# Group extra (and easy) files first
# images = self.getImages(group['unsorted_files'])
group['files'] = {
'movie_extra': self.getMovieExtras(group['unsorted_files']),
'subtitle': self.getSubtitles(group['unsorted_files']),
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']),
@@ -281,6 +284,10 @@ class Scanner(Plugin):
else:
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
if len(group['files']['movie']) == 0:
log.error('Couldn\t find any movie files for %s' % identifier)
continue
log.debug('Getting metadata for %s' % identifier)
group['meta_data'] = self.getMetaData(group)
@@ -470,6 +477,9 @@ class Scanner(Plugin):
return set(filter(test, files))
def getMovieExtras(self, files):
return set(filter(lambda s: getExt(s.lower()) in self.extensions['movie_extra'], files))
def getDVDFiles(self, files):
def test(s):
return self.isDVDFile(s)
+8
View File
@@ -23,4 +23,12 @@ class Score(Plugin):
score += sizeScore(nzb['size'])
# Torrents only
if nzb.get('seeds'):
try:
score += nzb.get('seeds') / 5
score += nzb.get('leechers') / 10
except:
pass
return score
@@ -28,7 +28,7 @@ config = [{
{
'name': 'ignored_words',
'label': 'Ignored words',
'default': 'german, dutch, french, danish, swedish, dubbed, swesub, korsub',
'default': 'german, dutch, french, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
},
],
}, {
+1 -1
View File
@@ -161,7 +161,7 @@ class Searcher(Plugin):
single_category = kwargs.get('single_category', False)
retention = Env.setting('retention', section = 'nzb')
if retention < nzb.get('age', 0):
if nzb.get('seeds') is None and retention < nzb.get('age', 0):
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name']))
return False
@@ -0,0 +1,34 @@
from .main import Trailer
def start():
return Trailer()
config = [{
'name': 'trailer',
'groups': [
{
'tab': 'metadata',
'name': 'trailer',
'options': [
{
'name': 'enabled',
'label': 'Search and download trailers',
'default': False,
'type': 'enabler',
},
{
'name': 'quality',
'default': '720p',
'type': 'dropdown',
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
},
{
'name': 'automatic',
'default': False,
'type': 'bool',
'description': 'Automaticly search & download for movies in library',
},
],
},
],
}]
+32
View File
@@ -0,0 +1,32 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
import os
log = CPLog(__name__)
class Trailer(Plugin):
def __init__(self):
addEvent('renamer.after', self.searchSingle)
def searchSingle(self, group):
if self.isDisabled() or len(group['files']['trailer']) > 0: return
trailers = fireEvent('trailer.search', group = group, merge = True)
for trailer in trailers.get(self.conf('quality'), []):
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
if not os.path.isfile(destination):
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
else:
log.debug('Trailer already exists: %s' % destination)
# Download first and break
break
def getRootName(self, data = {}):
return os.path.join(data['destination_dir'], data['filename'])
+14
View File
@@ -48,6 +48,20 @@ class YarrProvider(Provider):
def __init__(self):
addEvent('provider.belongs_to', self.belongsTo)
addEvent('%s.search' % self.type, self.search)
addEvent('yarr.search', self.search)
addEvent('nzb.feed', self.feed)
def download(self, url = '', nzb_id = ''):
return self.urlopen(url)
def feed(self):
return []
def search(self, movie, quality):
return []
def belongsTo(self, url, host = None):
try:
hostname = urlparse(url).hostname
+1 -1
View File
@@ -3,7 +3,7 @@ from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
import json
import os.path
import os
import shutil
import traceback
@@ -30,20 +30,24 @@ class IMDBAPI(MovieProvider):
cache_key = 'imdbapi.cache.%s' % q
cached = self.getCache(cache_key, self.urls['search'] % urlencode({'t': name_year.get('name'), 'y': name_year.get('year')}))
result = self.parseMovie(cached)
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
if cached:
result = self.parseMovie(cached)
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
return [result]
return [result]
return []
def getInfo(self, identifier = None):
cache_key = 'imdbapi.cache.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % identifier)
result = self.parseMovie(cached)
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
if cached:
result = self.parseMovie(cached)
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
return result
return result
return {}
def parseMovie(self, movie):
-17
View File
@@ -6,22 +6,5 @@ import time
class NZBProvider(YarrProvider):
type = 'nzb'
def __init__(self):
super(NZBProvider, self).__init__()
addEvent('nzb.search', self.search)
addEvent('yarr.search', self.search)
addEvent('nzb.feed', self.feed)
def download(self, url = '', nzb_id = ''):
return self.urlopen(url)
def feed(self):
return []
def search(self, movie, quality):
return []
def calculateAge(self, unix):
return int(time.time() - unix) / 24 / 60 / 60
@@ -1,4 +1,4 @@
from BeautifulSoup import BeautifulSoup, SoupStrainer
from BeautifulSoup import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryInt
@@ -20,6 +20,7 @@ class NZBMatrix(NZBProvider, RSS):
}
cat_ids = [
([50], ['bd50']),
([42, 53], ['720p', '1080p']),
([2], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
([54], ['brrip']),
@@ -3,4 +3,19 @@ from .main import KickAssTorrents
def start():
return KickAssTorrents()
config = []
config = [{
'name': 'kickasstorrents',
'groups': [
{
'tab': 'providers',
'name': 'KickAssTorrents',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
],
},
],
}]
@@ -1,5 +1,12 @@
from BeautifulSoup import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import StringIO
import gzip
import re
import traceback
log = CPLog(__name__)
@@ -7,8 +14,124 @@ log = CPLog(__name__)
class KickAssTorrents(TorrentProvider):
urls = {
'download': 'http://torrents.thepiratebay.org/%s/%s.torrent',
'test': 'http://www.kat.ph/',
'detail': 'http://www.kat.ph/%s-t%s.html',
'search': 'http://www.kat.ph/%s-i%s/',
}
cat_ids = [
(['cam'], ['cam']),
(['telesync'], ['ts', 'tc']),
(['screener', 'tvrip'], ['screener']),
(['x264', '720p', '1080p', 'blu-ray', 'hdrip'], ['bd50', '1080p', '720p', 'brrip']),
(['dvdrip'], ['dvdrip']),
(['dvd'], ['dvdr']),
]
http_time_between_calls = 1 #seconds
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['test']):
return results
cache_key = 'kickasstorrents.%s' % movie['library']['identifier']
data = self.getCache(cache_key, self.urls['search'] % (movie['library']['titles'][0]['title'], movie['library']['identifier'].replace('tt', '')))
if data:
cat_ids = self.getCatId(quality['identifier'])
table_order = ['name', 'size', None, 'age', 'seeds', 'leechers']
try:
html = BeautifulSoup(data)
resultdiv = html.find('div', attrs = {'class':'tabs'})
for result in resultdiv.findAll('div', recursive = False):
if result.get('id').lower() not in cat_ids:
continue
try:
try:
for temp in result.findAll('tr'):
if temp['class'] is 'firstr' or not temp.get('id'):
continue
new = {
'type': 'torrent',
'check_nzb': False,
'description': '',
'provider': self.getName(),
'download': self.download,
'score': 0,
}
nr = 0
for td in temp.findAll('td'):
column_name = table_order[nr]
if column_name:
if column_name is 'name':
link = td.find('div', {'class': 'torrentname'}).findAll('a')[1]
new['id'] = temp.get('id')[-8:]
new['name'] = link.text
new['url'] = td.findAll('a', 'idownload')[1]['href']
new['score'] = 20 if td.find('a', 'iverif') else 0
elif column_name is 'size':
new['size'] = self.parseSize(td.text)
elif column_name is 'age':
new['age'] = self.ageToDays(td.text)
elif column_name is 'seeds':
new['seeds'] = tryInt(td.text)
elif column_name is 'leechers':
new['leechers'] = tryInt(td.text)
nr += 1
new['score'] += fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
except:
log.error('Failed parsing KickAssTorrents: %s' % traceback.format_exc())
except:
pass
return results
except AttributeError:
log.debug('No search results found.')
return results
def ageToDays(self, age_str):
age = 0
age_str = age_str.replace('&nbsp;', ' ')
regex = '(\d*.?\d+).(sec|hour|day|week|month|year)+'
matches = re.findall(regex, age_str)
for match in matches:
nr, size = match
mult = 1
if size == 'week':
mult = 7
elif size == 'month':
mult = 30.5
elif size == 'year':
mult = 365
age += tryInt(nr) * mult
return tryInt(age)
def download(self, url = '', nzb_id = ''):
compressed_data = super(KickAssTorrents, self).download(url = url, nzb_id = nzb_id)
compressedstream = StringIO.StringIO(compressed_data)
gzipper = gzip.GzipFile(fileobj = compressedstream)
data = gzipper.read()
return data
@@ -1,5 +1,13 @@
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
log = CPLog(__name__)
class TrailerProvider(Provider):
type = 'trailer'
def __init__(self):
addEvent('trailer.search', self.search)
@@ -1,4 +1,5 @@
from BeautifulSoup import SoupStrainer, BeautifulSoup
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.trailer.base import TrailerProvider
from string import letters, digits
@@ -16,47 +17,40 @@ class HDTrailers(TrailerProvider):
}
providers = ['apple.ico', 'yahoo.ico', 'moviefone.ico', 'myspace.ico', 'favicon.ico']
def find(self, movie):
def search(self, group):
movie_name = movie['library']['titles'][0]['title']
movie_name = group['library']['titles'][0]['title']
url = self.url['api'] % self.movieUrlName(movie_name)
try:
data = self.urlopen(url)
except:
return {}
url = self.urls['api'] % self.movieUrlName(movie_name)
data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url)
result_data = {}
p480 = []
p720 = []
p1080 = []
did_alternative = False
for provider in self.providers:
results = self.findByProvider(data, provider)
# Find alternative
if results.get('404') and not did_alternative:
results = self.findViaAlternative(movie_name)
results = self.findViaAlternative(group)
did_alternative = True
p480.extend(results.get('480p'))
p720.extend(results.get('720p'))
p1080.extend(results.get('1080p'))
result_data = mergeDicts(result_data, results)
return {'480p':p480, '720p':p720, '1080p':p1080}
return result_data
def findViaAlternative(self, movie):
def findViaAlternative(self, group):
results = {'480p':[], '720p':[], '1080p':[]}
url = "%s?%s" % (self.url['backup'], urlencode({'s':movie}))
try:
data = self.urlopen(url)
except:
return results
movie_name = group['library']['titles'][0]['title']
url = "%s?%s" % (self.url['backup'], urlencode({'s':movie_name}))
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
try:
tables = SoupStrainer('div')
html = BeautifulSoup(data, parseOnlyThese = tables)
result_table = html.findAll('h2', text = re.compile(movie))
result_table = html.findAll('h2', text = re.compile(movie_name))
for h2 in result_table:
if 'trailer' in h2.lower():
@@ -88,6 +82,8 @@ class HDTrailers(TrailerProvider):
break
if 'trailer' in trtext and not 'clip' in trtext and provider in trtext:
nr = 0
if 'trailer' not in tr.find('span', 'standardTrailerName').text.lower():
continue
resolutions = tr.findAll('td', attrs = {'class':'bottomTableResolution'})
for res in resolutions:
results[str(res.a.contents[0])].insert(0, res.a['href'])
+1
View File
@@ -8,6 +8,7 @@ class Env(object):
_encoding = ''
_uses_git = False
_debug = False
_dev = False
_settings = Settings()
_loader = Loader()
_cache = None
+15 -30
View File
@@ -11,7 +11,7 @@ import logging
import os.path
import sys
import time
import traceback
import warnings
def getOptions(base_path, args):
@@ -46,25 +46,7 @@ def cleanup():
time.sleep(1)
def runCouchPotato(options, base_path, args, desktop = None):
# Load settings
from couchpotato.environment import Env
settings = Env.get('settings')
settings.setFile(options.config_file)
# Create data dir if needed
data_dir = os.path.expanduser(Env.setting('data_dir'))
if data_dir == '':
data_dir = getDataDir()
if not os.path.isdir(data_dir):
os.makedirs(data_dir)
# Create logging dir
log_dir = os.path.join(data_dir, 'logs');
if not os.path.isdir(log_dir):
os.mkdir(log_dir)
def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, Env = None, desktop = None):
try:
locale.setlocale(locale.LC_ALL, "")
@@ -95,15 +77,21 @@ def runCouchPotato(options, base_path, args, desktop = None):
debug = options.debug or Env.setting('debug', default = False, type = 'bool')
Env.set('debug', debug)
if not Env.setting('development'):
# Development
development = Env.setting('development', default = False, type = 'bool')
Env.set('dev', development)
if not development:
atexit.register(cleanup)
# Use reloader
reloader = debug is True and development and not Env.get('desktop') and not options.daemon
# Disable server access log
logging.getLogger('werkzeug').setLevel(logging.WARNING)
# Only run once when debugging
fire_load = False
if os.environ.get('WERKZEUG_RUN_MAIN') or not debug or Env.get('desktop') or options.daemon:
if os.environ.get('WERKZEUG_RUN_MAIN') or not reloader:
# Logger
logger = logging.getLogger()
@@ -128,6 +116,10 @@ def runCouchPotato(options, base_path, args, desktop = None):
log = CPLog(__name__)
log.debug('Started with options %s' % options)
def customwarn(message, category, filename, lineno, file = None, line = None):
log.warning('%s %s %s line:%s' % (category, message, filename, lineno))
warnings.showwarning = customwarn
# Load configs & plugins
loader = Env.get('loader')
@@ -168,7 +160,6 @@ def runCouchPotato(options, base_path, args, desktop = None):
from couchpotato import app
api_key = Env.setting('api_key')
url_base = '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else ''
reloader = debug is True and Env.setting('development') and not Env.get('desktop') and not options.daemon
# Basic config
app.secret_key = api_key
@@ -194,10 +185,4 @@ def runCouchPotato(options, base_path, args, desktop = None):
if fire_load: fireEventAsync('app.load')
# Go go go!
try:
app.run(**config)
except (KeyboardInterrupt, SystemExit):
raise
except:
log.error('Failed starting: %s' % traceback.format_exc())
raise
app.run(**config)
+15 -12
View File
@@ -14,13 +14,17 @@
############### EDIT ME ##################
# path to app
APP_PATH=/usr/local/sbin/couchpotato
APP_PATH=/usr/local/sbin/CouchPotatoServer/
# user
RUN_AS=YOUR_USERNAME_HERE
# path to python bin
DAEMON=/usr/bin/python
# startup args
DAEMON_OPTS=" CouchPotato.py"
# Path to store PID file
PID_FILE=/var/run/couchpotato/server.pid
PID_PATH=$(dirname $PID_FILE)
# script name
NAME=couchpotato
@@ -28,10 +32,8 @@ NAME=couchpotato
# app name
DESC=CouchPotato
# user
RUN_AS=root
PID_FILE=/var/run/couchpotato.pid
# startup args
DAEMON_OPTS=" CouchPotato.py --daemon --pid_file=${PID_FILE}"
############### END EDIT ME ##################
@@ -42,18 +44,19 @@ set -e
case "$1" in
start)
echo "Starting $DESC"
start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --make-pidfile --exec $DAEMON -- $DAEMON_OPTS
rm -rf $PID_PATH || return 1
install -d --mode=0755 -o $RUN_AS -g $RUN_AS $PID_PATH || return 1
start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
;;
stop)
echo "Stopping $DESC"
start-stop-daemon --stop --pidfile $PID_FILE
start-stop-daemon --stop --pidfile $PID_FILE --retry 15
;;
restart|force-reload)
echo "Restarting $DESC"
start-stop-daemon --stop --pidfile $PID_FILE
sleep 15
start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --make-pidfile --exec $DAEMON -- $DAEMON_OPTS
start-stop-daemon --stop --pidfile $PID_FILE --retry 15
start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
;;
*)
N=/etc/init.d/$NAME
+2362
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -64,7 +64,8 @@ class Daemon():
file(self.pidfile, 'w+').write("%s\n" % pid)
def delpid(self):
os.remove(self.pidfile)
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
def start(self):
"""
@@ -112,8 +113,7 @@ class Daemon():
except OSError, err:
err = str(err)
if err.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
self.delpid()
else:
print str(err)
sys.exit(1)
+1 -1
View File
@@ -11,6 +11,6 @@ from transmissionrpc.client import Client
from transmissionrpc.utils import add_stdout_logger
__author__ = u'Erik Svensson <erik.public@gmail.com>'
__version__ = u'0.8'
__version__ = u'0.9'
__copyright__ = u'Copyright (c) 2008-2011 Erik Svensson'
__license__ = u'MIT'
+228 -88
View File
@@ -2,7 +2,7 @@
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license.
import re, time
import re, time, operator, warnings
import urllib2, urlparse, base64
try:
@@ -36,7 +36,7 @@ def debug_httperror(error):
'data': data,
}
},
indent=2
indent = 2
)
)
@@ -59,7 +59,7 @@ class Client(object):
Client is the class handling the Transmission JSON-RPC client protocol.
"""
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None, http_handler=None, timeout=None):
def __init__(self, address = 'localhost', port = DEFAULT_PORT, user = None, password = None, http_handler = None, timeout = None):
if isinstance(timeout, (int, long, float)):
self._query_timeout = float(timeout)
else:
@@ -91,7 +91,7 @@ class Client(object):
elif user or password:
LOGGER.warning('Either user or password missing, not using authentication.')
self._sequence = 0
self.session = Session()
self.session = None
self.session_id = 0
self.server_version = None
self.protocol_version = None
@@ -117,9 +117,9 @@ class Client(object):
"""
self._query_timeout = DEFAULT_TIMEOUT
timeout = property(_get_timeout, _set_timeout, _del_timeout, doc="HTTP query timeout.")
timeout = property(_get_timeout, _set_timeout, _del_timeout, doc = "HTTP query timeout.")
def _http_query(self, query, timeout=None):
def _http_query(self, query, timeout = None):
"""
Query Transmission through HTTP.
"""
@@ -129,7 +129,7 @@ class Client(object):
if timeout is None:
timeout = self._query_timeout
while True:
LOGGER.debug(json.dumps({'url': self.url, 'headers': headers, 'query': query, 'timeout': timeout}, indent=2))
LOGGER.debug(json.dumps({'url': self.url, 'headers': headers, 'query': query, 'timeout': timeout}, indent = 2))
try:
result = self.http_handler.request(self.url, query, headers, timeout)
break
@@ -150,7 +150,7 @@ class Client(object):
request_count += 1
return result
def _request(self, method, arguments=None, ids=None, require_ids=False, timeout=None):
def _request(self, method, arguments = None, ids = None, require_ids = False, timeout = None):
"""
Send json-rpc request to Transmission using http POST
"""
@@ -182,7 +182,7 @@ class Client(object):
LOGGER.error('HTTP data: \"%s\"' % (http_data))
raise
LOGGER.debug(json.dumps(data, indent=2))
LOGGER.debug(json.dumps(data, indent = 2))
if 'result' in data:
if data['result'] != 'success':
raise TransmissionError('Query failed with result \"%s\".' % (data['result']))
@@ -264,7 +264,10 @@ class Client(object):
"""
Update session data.
"""
self.session.update(data)
if self.session:
self.session.from_request(data)
else:
self.session = Session(self, data)
def _update_server_version(self):
if self.server_version is None:
@@ -305,11 +308,12 @@ class Client(object):
Add a warning to the log if the Transmission RPC version is lower then the provided version.
"""
if self.rpc_version < version:
LOGGER.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version))
LOGGER.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.'
% (self.rpc_version, version))
def add(self, data, timeout=None, **kwargs):
def add_torrent(self, torrent, timeout = None, **kwargs):
"""
Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
Add torrent to transfers list. Takes a uri to a torrent or base64 encoded torrent data.
Additional arguments are:
===================== ===== =========== =============================================================
@@ -327,6 +331,41 @@ class Client(object):
``priority_normal`` 1 - A list of file id's that should have normal priority.
===================== ===== =========== =============================================================
Returns a Torrent object with limited fields.
"""
if torrent is None:
raise ValueError('add_torrent requires data or a URI.')
torrent_data = None
try:
# check if this is base64 data
base64.b64decode(torrent)
torrent_data = torrent
except Exception:
torrent_data = None
if not torrent_data:
parsed_uri = urlparse.urlparse(torrent)
if parsed_uri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']:
# there has been some problem with T's built in torrent fetcher,
# use a python one instead
torrent_file = urllib2.urlopen(torrent)
torrent_data = torrent_file.read()
torrent_data = base64.b64encode(torrent_data)
args = {}
if torrent_data:
args = {'metainfo': torrent_data}
else:
args = {'filename': torrent}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version)
args[arg] = val
return self._request('torrent-add', args, timeout = timeout).values()[0]
def add(self, data, timeout = None, **kwargs):
"""
.. WARNING::
Deprecated, please use add_torrent.
"""
args = {}
if data:
@@ -335,31 +374,16 @@ class Client(object):
raise ValueError('No torrent data or torrent uri.')
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add',
argument, value, self.rpc_version)
(arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version)
args[arg] = val
return self._request('torrent-add', args, timeout=timeout)
warnings.warn('add has been deprecated, please use add_torrent instead.', DeprecationWarning)
return self._request('torrent-add', args, timeout = timeout)
def add_uri(self, uri, **kwargs):
"""
Add torrent to transfers list. Takes a uri to a torrent, supporting
all uri's supported by Transmissions torrent-add 'filename'
argument. Additional arguments are:
===================== ===== =========== =============================================================
Argument RPC Replaced by Description
===================== ===== =========== =============================================================
``bandwidthPriority`` 8 - Priority for this transfer.
``cookies`` 13 - One or more HTTP cookie(s).
``download_dir`` 1 - The directory where the downloaded contents will be saved in.
``files_unwanted`` 1 - A list of file id's that shouldn't be downloaded.
``files_wanted`` 1 - A list of file id's that should be downloaded.
``paused`` 1 - If True, does not start the transfer when added.
``peer_limit`` 1 - Maximum number of peers allowed.
``priority_high`` 1 - A list of file id's that should have high priority.
``priority_low`` 1 - A list of file id's that should have low priority.
``priority_normal`` 1 - A list of file id's that should have normal priority.
===================== ===== =========== =============================================================
.. WARNING::
Deprecated, please use add_torrent.
"""
if uri is None:
raise ValueError('add_uri requires a URI.')
@@ -370,47 +394,145 @@ class Client(object):
if parsed_uri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']:
torrent_file = urllib2.urlopen(uri)
torrent_data = base64.b64encode(torrent_file.read())
warnings.warn('add_uri has been deprecated, please use add_torrent instead.', DeprecationWarning)
if torrent_data:
return self.add(torrent_data, **kwargs)
else:
return self.add(None, filename=uri, **kwargs)
return self.add(None, filename = uri, **kwargs)
def remove(self, ids, delete_data=False, timeout=None):
def remove_torrent(self, ids, delete_data = False, timeout = None):
"""
remove torrent(s) with provided id(s). Local data is removed if
delete_data is True, otherwise not.
"""
self._rpc_version_warning(3)
self._request('torrent-remove',
{'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout=timeout)
{'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout = timeout)
def start(self, ids, bypass_queue=False, timeout=None):
"""start torrent(s) with provided id(s)"""
def remove(self, ids, delete_data = False, timeout = None):
"""
.. WARNING::
Deprecated, please use remove_torrent.
"""
warnings.warn('remove has been deprecated, please use remove_torrent instead.', DeprecationWarning)
self.remove_torrent(ids, delete_data, timeout)
def start_torrent(self, ids, bypass_queue = False, timeout = None):
"""Start torrent(s) with provided id(s)"""
method = 'torrent-start'
if bypass_queue and self.rpc_version >= 14:
method = 'torrent-start-now'
self._request(method, {}, ids, True, timeout=timeout)
self._request(method, {}, ids, True, timeout = timeout)
def stop(self, ids, timeout=None):
def start(self, ids, bypass_queue = False, timeout = None):
"""
.. WARNING::
Deprecated, please use start_torrent.
"""
warnings.warn('start has been deprecated, please use start_torrent instead.', DeprecationWarning)
self.start_torrent(ids, bypass_queue, timeout)
def start_all(self, bypass_queue = False, timeout = None):
"""Start all torrents respecting the queue order"""
torrent_list = self.get_torrents()
method = 'torrent-start'
if self.rpc_version >= 14:
if bypass_queue:
method = 'torrent-start-now'
torrent_list = sorted(torrent_list, key = operator.attrgetter('queuePosition'))
ids = [x.id for x in torrent_list]
self._request(method, {}, ids, True, timeout = timeout)
def stop_torrent(self, ids, timeout = None):
"""stop torrent(s) with provided id(s)"""
self._request('torrent-stop', {}, ids, True, timeout=timeout)
self._request('torrent-stop', {}, ids, True, timeout = timeout)
def verify(self, ids, timeout=None):
def stop(self, ids, timeout = None):
"""
.. WARNING::
Deprecated, please use stop_torrent.
"""
warnings.warn('stop has been deprecated, please use stop_torrent instead.', DeprecationWarning)
self.stop_torrent(ids, timeout)
def verify_torrent(self, ids, timeout = None):
"""verify torrent(s) with provided id(s)"""
self._request('torrent-verify', {}, ids, True, timeout=timeout)
self._request('torrent-verify', {}, ids, True, timeout = timeout)
def reannounce(self, ids, timeout=None):
def verify(self, ids, timeout = None):
"""
.. WARNING::
Deprecated, please use verify_torrent.
"""
warnings.warn('verify has been deprecated, please use verify_torrent instead.', DeprecationWarning)
self.verify_torrent(ids, timeout)
def reannounce_torrent(self, ids, timeout = None):
"""Reannounce torrent(s) with provided id(s)"""
self._rpc_version_warning(5)
self._request('torrent-reannounce', {}, ids, True, timeout=timeout)
self._request('torrent-reannounce', {}, ids, True, timeout = timeout)
def info(self, ids=None, arguments=None, timeout=None):
"""Get detailed information for torrent(s) with provided id(s)."""
def reannounce(self, ids, timeout = None):
"""
.. WARNING::
Deprecated, please use reannounce_torrent.
"""
warnings.warn('reannounce has been deprecated, please use reannounce_torrent instead.', DeprecationWarning)
self.reannounce_torrent(ids, timeout)
def get_torrent(self, id, arguments = None, timeout = None):
"""
Get information for torrent with provided id.
Returns a Torrent object.
"""
if not arguments:
arguments = self.torrent_get_arguments
return self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout)
if not isinstance(id, (int, long, str, unicode)):
raise ValueError("Invalid id")
return self._request('torrent-get', {'fields': arguments}, id, require_ids = True, timeout = timeout)[id]
def get_files(self, ids=None, timeout=None):
def get_torrents(self, ids = None, arguments = None, timeout = None):
"""
Get information for torrents with provided ids.
Returns a list of Torrent object.
"""
if not arguments:
arguments = self.torrent_get_arguments
return self._request('torrent-get', {'fields': arguments}, ids, timeout = timeout).values()
def info(self, ids = None, arguments = None, timeout = None):
"""
.. WARNING::
Deprecated, please use get_torrent or get_torrents. Please note that the return argument has changed in
the new methods. info returns a dictionary indexed by torrent id.
"""
warnings.warn('info has been deprecated, please use get_torrent or get_torrents instead.', DeprecationWarning)
if not arguments:
arguments = self.torrent_get_arguments
return self._request('torrent-get', {'fields': arguments}, ids, timeout = timeout)
def list(self, timeout = None):
"""
.. WARNING::
Deprecated, please use get_torrent or get_torrents. Please note that the return argument has changed in
the new methods. list returns a dictionary indexed by torrent id.
"""
warnings.warn('list has been deprecated, please use get_torrent or get_torrents instead.', DeprecationWarning)
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
, 'downloadedEver', 'uploadRatio', 'queuePosition']
return self._request('torrent-get', {'fields': fields}, timeout = timeout)
def get_files(self, ids = None, timeout = None):
"""
Get list of files for provided torrent id(s). If ids is empty,
information for all torrents are fetched. This function returns a dictionary
@@ -435,13 +557,13 @@ class Client(object):
}
"""
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
request_result = self._request('torrent-get', {'fields': fields}, ids, timeout=timeout)
request_result = self._request('torrent-get', {'fields': fields}, ids, timeout = timeout)
result = {}
for tid, torrent in request_result.iteritems():
result[tid] = torrent.files()
return result
def set_files(self, items, timeout=None):
def set_files(self, items, timeout = None):
"""
Set file properties. Takes a dictionary with similar contents as the result
of `get_files`.
@@ -498,14 +620,7 @@ class Client(object):
args['priority_low'] = low
self.change([tid], **args)
def list(self, timeout=None):
"""list all torrents"""
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
, 'downloadedEver', 'uploadRatio']
return self._request('torrent-get', {'fields': fields}, timeout=timeout)
def change(self, ids, timeout=None, **kwargs):
def change_torrent(self, ids, timeout = None, **kwargs):
"""
Change torrent parameters for the torrent(s) with the supplied id's. The
parameters are:
@@ -546,54 +661,80 @@ class Client(object):
args = {}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-set'
, argument, value, self.rpc_version)
(arg, val) = argument_value_convert('torrent-set' , argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('torrent-set', args, ids, True, timeout=timeout)
self._request('torrent-set', args, ids, True, timeout = timeout)
else:
ValueError("No arguments to set")
def move(self, ids, location, timeout=None):
def change(self, ids, timeout = None, **kwargs):
"""
.. WARNING::
Deprecated, please use change_torrent.
"""
warnings.warn('change has been deprecated, please use change_torrent instead.', DeprecationWarning)
self.change_torrent(ids, timeout, **kwargs)
def move_torrent_data(self, ids, location, timeout = None):
"""Move torrent data to the new location."""
self._rpc_version_warning(6)
args = {'location': location, 'move': True}
self._request('torrent-set-location', args, ids, True, timeout=timeout)
self._request('torrent-set-location', args, ids, True, timeout = timeout)
def locate(self, ids, location, timeout=None):
"""Locate torrent data at the location."""
def move(self, ids, location, timeout = None):
"""
.. WARNING::
Deprecated, please use move_torrent_data.
"""
warnings.warn('move has been deprecated, please use move_torrent_data instead.', DeprecationWarning)
self.move_torrent_data(ids, location, timeout)
def locate_torrent_data(self, ids, location, timeout = None):
"""Locate torrent data at the provided location."""
self._rpc_version_warning(6)
args = {'location': location, 'move': False}
self._request('torrent-set-location', args, ids, True, timeout=timeout)
self._request('torrent-set-location', args, ids, True, timeout = timeout)
def queue_top(self, ids, timeout=None):
def locate(self, ids, location, timeout = None):
"""
.. WARNING::
Deprecated, please use locate_torrent_data.
"""
warnings.warn('locate has been deprecated, please use locate_torrent_data instead.', DeprecationWarning)
self.locate_torrent_data(ids, location, timeout)
def queue_top(self, ids, timeout = None):
"""Move transfer to the top of the queue."""
self._rpc_version_warning(14)
self._request('queue-move-top', ids=ids, require_ids=True, timeout=timeout)
self._request('queue-move-top', ids = ids, require_ids = True, timeout = timeout)
def queue_bottom(self, ids, timeout=None):
def queue_bottom(self, ids, timeout = None):
"""Move transfer to the bottom of the queue."""
self._rpc_version_warning(14)
self._request('queue-move-bottom', ids=ids, require_ids=True, timeout=timeout)
def queue_up(self, ids, timeout=None):
self._request('queue-move-bottom', ids = ids, require_ids = True, timeout = timeout)
def queue_up(self, ids, timeout = None):
"""Move transfer up in the queue."""
self._rpc_version_warning(14)
self._request('queue-move-up', ids=ids, require_ids=True, timeout=timeout)
self._request('queue-move-up', ids = ids, require_ids = True, timeout = timeout)
def queue_down(self, ids, timeout=None):
def queue_down(self, ids, timeout = None):
"""Move transfer down in the queue."""
self._rpc_version_warning(14)
self._request('queue-move-down', ids=ids, require_ids=True, timeout=timeout)
self._request('queue-move-down', ids = ids, require_ids = True, timeout = timeout)
def get_session(self, timeout=None):
def get_session(self, timeout = None):
"""Get session parameters"""
self._request('session-get', timeout=timeout)
self._request('session-get', timeout = timeout)
self._update_server_version()
return self.session
def set_session(self, timeout=None, **kwargs):
def set_session(self, timeout = None, **kwargs):
"""
Set session parameters. The parameters are:
@@ -655,32 +796,31 @@ class Client(object):
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
raise ValueError('Invalid encryption value')
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('session-set'
, argument, value, self.rpc_version)
(arg, val) = argument_value_convert('session-set' , argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('session-set', args, timeout=timeout)
self._request('session-set', args, timeout = timeout)
def blocklist_update(self, timeout=None):
def blocklist_update(self, timeout = None):
"""Update block list. Returns the size of the block list."""
self._rpc_version_warning(5)
result = self._request('blocklist-update', timeout=timeout)
result = self._request('blocklist-update', timeout = timeout)
if 'blocklist-size' in result:
return result['blocklist-size']
return None
def port_test(self, timeout=None):
def port_test(self, timeout = None):
"""
Tests to see if your incoming peer port is accessible from the
outside world.
"""
self._rpc_version_warning(5)
result = self._request('port-test', timeout=timeout)
result = self._request('port-test', timeout = timeout)
if 'port-is-open' in result:
return result['port-is-open']
return None
def session_stats(self, timeout=None):
def session_stats(self, timeout = None):
"""Get session statistics"""
self._request('session-stats', timeout=timeout)
self._request('session-stats', timeout = timeout)
return self.session
+2 -2
View File
@@ -15,9 +15,9 @@ class TransmissionError(Exception):
def __str__(self):
if self.original:
original_name = type(self.original).__name__
return '%s Original exception: %s, "%s"' % (self.message, original_name, str(self.original))
return '%s Original exception: %s, "%s"' % (self._message, original_name, str(self.original))
else:
return self.message
return self._message
class HTTPHandlerError(Exception):
"""
+84 -20
View File
@@ -2,6 +2,8 @@
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license.
from transmissionrpc.utils import Field
class Session(object):
"""
Session is a class holding the session data for a Transmission daemon.
@@ -12,33 +14,95 @@ class Session(object):
``download-dir`` -> ``download_dir``.
"""
def __init__(self, fields=None):
self.fields = {}
def __init__(self, client=None, fields=None):
self._client = client
self._fields = {}
if fields is not None:
self.update(fields)
def update(self, other):
"""Update the session data from a session arguments dictionary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Session):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for key, value in fields.iteritems():
self.fields[key.replace('-', '_')] = value
self._update_fields(fields)
def __getattr__(self, name):
try:
return self.fields[name]
return self._fields[name].value
except KeyError:
raise AttributeError('No attribute %s' % name)
def __str__(self):
text = ''
for key in sorted(self.fields.keys()):
text += "% 32s: %s\n" % (key[-32:], self.fields[key])
for key in sorted(self._fields.keys()):
text += "% 32s: %s\n" % (key[-32:], self._fields[key].value)
return text
def _update_fields(self, other):
"""
Update the session data from a Transmission JSON-RPC arguments dictionary
"""
fields = None
if isinstance(other, dict):
for key, value in other.iteritems():
self._fields[key.replace('-', '_')] = Field(value, False)
elif isinstance(other, Session):
for key in other._fields.keys():
self._fields[key] = Field(other._fields[key].value, False)
else:
raise ValueError('Cannot update with supplied data')
def _dirty_fields(self):
"""Enumerate changed fields"""
outgoing_keys = ['peer_port', 'pex_enabled']
fields = []
for key in outgoing_keys:
if key in self._fields and self._fields[key].dirty:
fields.append(key)
return fields
def _push(self):
"""Push changed fields to the server"""
dirty = self._dirty_fields()
args = {}
for key in dirty:
args[key] = self._fields[key].value
self._fields[key] = self._fields[key]._replace(dirty=False)
if len(args) > 0:
self._client.set_session(**args)
def update(self, timeout=None):
"""Update the session information."""
self._push()
session = self._client.get_session(timeout=timeout)
self._update_fields(session)
session = self._client.session_stats(timeout=timeout)
self._update_fields(session)
def from_request(self, data):
"""Update the session information."""
self._update_fields(data)
def _get_peer_port(self):
"""
Get the peer port.
"""
return self._fields['peer_port'].value
def _set_peer_port(self, port):
"""
Set the peer port.
"""
if isinstance(port, (int, long)):
self._fields['peer_port'] = Field(port, True)
self._push()
else:
raise ValueError("Not a valid limit")
peer_port = property(_get_peer_port, _set_peer_port, None, "Peer port. This is a mutator.")
def _get_pex_enabled(self):
return self._fields['pex_enabled'].value
def _set_pex_enabled(self, enabled):
if isinstance(enabled, bool):
self._fields['pex_enabled'] = Field(enabled, True)
self._push()
else:
raise TypeError("Not a valid type")
pex_enabled = property(_get_pex_enabled, _set_pex_enabled, None, "Enable PEX. This is a mutator.")
+296 -48
View File
@@ -4,8 +4,8 @@
import sys, datetime
from transmissionrpc.constants import PRIORITY
from transmissionrpc.utils import format_timedelta
from transmissionrpc.constants import PRIORITY, RATIO_LIMIT, IDLE_LIMIT
from transmissionrpc.utils import Field, format_timedelta
class Torrent(object):
"""
@@ -17,17 +17,19 @@ class Torrent(object):
def __init__(self, client, fields):
if 'id' not in fields:
raise ValueError('Torrent requires an id')
self.fields = {}
self.update(fields)
self.client = client
self._fields = {}
self._update_fields(fields)
self._incoming_pending= False
self._outgoing_pending= False
self._client = client
def _getNameString(self, codec=None):
if codec is None:
codec = sys.getdefaultencoding()
name = None
# try to find name
if 'name' in self.fields:
name = self.fields['name']
if 'name' in self._fields:
name = self._fields['name'].value
# if name is unicode, try to decode
if isinstance(name, unicode):
try:
@@ -37,7 +39,7 @@ class Torrent(object):
return name
def __repr__(self):
tid = self.fields['id']
tid = self._fields['id'].value
name = self._getNameString()
if isinstance(name, str):
return '<Torrent %d \"%s\">' % (tid, name)
@@ -52,12 +54,53 @@ class Torrent(object):
return 'Torrent'
def __copy__(self):
return Torrent(self.client, self.fields)
return Torrent(self._client, self._fields)
def __getattr__(self, name):
try:
return self._fields[name].value
except KeyError:
raise AttributeError('No attribute %s' % name)
def _rpc_version(self):
if self.client:
return self.client.rpc_version
if self._client:
return self._client.rpc_version
return 2
def _dirty_fields(self):
"""Enumerate changed fields"""
outgoing_keys = ['bandwidthPriority', 'downloadLimit', 'downloadLimited', 'peer_limit', 'queuePosition'
, 'seedIdleLimit', 'seedIdleMode', 'seedRatioLimit', 'seedRatioMode', 'uploadLimit', 'uploadLimited']
fields = []
for key in outgoing_keys:
if key in self._fields and self._fields[key].dirty:
fields.append(key)
return fields
def _push(self):
"""Push changed fields to the server"""
dirty = self._dirty_fields()
args = {}
for key in dirty:
args[key] = self._fields[key].value
self._fields[key] = self._fields[key]._replace(dirty=False)
if len(args) > 0:
self._client.change_torrent(self.id, **args)
def _update_fields(self, other):
"""
Update the torrent data from a Transmission JSON-RPC arguments dictionary
"""
fields = None
if isinstance(other, dict):
for key, value in other.iteritems():
self._fields[key.replace('-', '_')] = Field(value, False)
elif isinstance(other, Torrent):
for key in other._fields.keys():
self._fields[key] = Field(other._fields[key].value, False)
else:
raise ValueError('Cannot update with supplied data')
self._incoming_pending = False
def _status_old(self, code):
mapping = {
@@ -82,26 +125,12 @@ class Torrent(object):
return mapping[code]
def _status(self):
code = self.fields['status']
code = self._fields['status'].value
if self._rpc_version() >= 14:
return self._status_new(code)
else:
return self._status_old(code)
def update(self, other):
"""
Update the torrent data from a Transmission JSON-RPC arguments dictionary
"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Torrent):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for key, value in fields.iteritems():
self.fields[key.replace('-', '_')] = value
def files(self):
"""
Get list of files for this torrent.
@@ -122,11 +151,11 @@ class Torrent(object):
}
"""
result = {}
if 'files' in self.fields:
indices = xrange(len(self.fields['files']))
files = self.fields['files']
priorities = self.fields['priorities']
wanted = self.fields['wanted']
if 'files' in self._fields:
files = self._fields['files'].value
indices = xrange(len(files))
priorities = self._fields['priorities'].value
wanted = self._fields['wanted'].value
for item in zip(indices, files, priorities, wanted):
selected = True if item[3] else False
priority = PRIORITY[item[2]]
@@ -138,12 +167,6 @@ class Torrent(object):
'completed': item[1]['bytesCompleted']}
return result
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError:
raise AttributeError('No attribute %s' % name)
@property
def status(self):
"""
@@ -157,19 +180,21 @@ class Torrent(object):
def progress(self):
"""Get the download progress in percent."""
try:
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone'])
size = self._fields['sizeWhenDone'].value
left = self._fields['leftUntilDone'].value
return 100.0 * (size - left) / float(size)
except ZeroDivisionError:
return 0.0
@property
def ratio(self):
"""Get the upload/download ratio."""
return float(self.fields['uploadRatio'])
return float(self._fields['uploadRatio'].value)
@property
def eta(self):
"""Get the "eta" as datetime.timedelta."""
eta = self.fields['eta']
eta = self._fields['eta'].value
if eta >= 0:
return datetime.timedelta(seconds=eta)
else:
@@ -178,22 +203,22 @@ class Torrent(object):
@property
def date_active(self):
"""Get the attribute "activityDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['activityDate'])
return datetime.datetime.fromtimestamp(self._fields['activityDate'].value)
@property
def date_added(self):
"""Get the attribute "addedDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['addedDate'])
return datetime.datetime.fromtimestamp(self._fields['addedDate'].value)
@property
def date_started(self):
"""Get the attribute "startDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['startDate'])
return datetime.datetime.fromtimestamp(self._fields['startDate'].value)
@property
def date_done(self):
"""Get the attribute "doneDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['doneDate'])
return datetime.datetime.fromtimestamp(self._fields['doneDate'].value)
def format_eta(self):
"""
@@ -203,7 +228,7 @@ class Torrent(object):
* If eta is -2 the result is 'unknown'
* Otherwise eta is formatted as <days> <hours>:<minutes>:<seconds>.
"""
eta = self.fields['eta']
eta = self._fields['eta'].value
if eta == -1:
return 'not available'
elif eta == -2:
@@ -211,10 +236,233 @@ class Torrent(object):
else:
return format_timedelta(self.eta)
@property
def priority(self):
def _get_download_limit(self):
"""
Get the download limit.
Can be a number or None.
"""
if self._fields['downloadLimited'].value:
return self._fields['downloadLimit'].value
else:
return None
def _set_download_limit(self, limit):
"""
Get the download limit.
Can be a number, 'session' or None.
"""
if isinstance(limit, (int, long)):
self._fields['downloadLimited'] = Field(True, True)
self._fields['downloadLimit'] = Field(limit, True)
self._push()
elif limit == None:
self._fields['downloadLimited'] = Field(False, True)
self._push()
else:
raise ValueError("Not a valid limit")
download_limit = property(_get_download_limit, _set_download_limit, None, "Download limit in Kbps or None. This is a mutator.")
def _get_peer_limit(self):
"""
Get the peer limit.
"""
return self._fields['peer_limit'].value
def _set_peer_limit(self, limit):
"""
Set the peer limit.
"""
if isinstance(limit, (int, long)):
self._fields['peer_limit'] = Field(limit, True)
self._push()
else:
raise ValueError("Not a valid limit")
peer_limit = property(_get_peer_limit, _set_peer_limit, None, "Peer limit. This is a mutator.")
def _get_priority(self):
"""
Get the priority as string.
Can be one of 'low', 'normal', 'high'.
"""
return PRIORITY[self.fields['bandwidthPriority']]
return PRIORITY[self._fields['bandwidthPriority'].value]
def _set_priority(self, priority):
"""
Set the priority as string.
Can be one of 'low', 'normal', 'high'.
"""
if isinstance(priority, (str, unicode)):
self._fields['bandwidthPriority'] = Field(PRIORITY[priority], True)
self._push()
priority = property(_get_priority, _set_priority, None
, "Bandwidth priority as string. Can be one of 'low', 'normal', 'high'. This is a mutator.")
def _get_seed_idle_limit(self):
"""
Get the seed idle limit in minutes.
"""
return self._fields['seedIdleLimit'].value
def _set_seed_idle_limit(self, limit):
"""
Set the seed idle limit in minutes.
"""
if isinstance(limit, (int, long)):
self._fields['seedIdleLimit'] = Field(limit, True)
self._push()
else:
raise ValueError("Not a valid limit")
seed_idle_limit = property(_get_seed_idle_limit, _set_seed_idle_limit, None
, "Torrent seed idle limit in minutes. Also see seed_idle_mode. This is a mutator.")
def _get_seed_idle_mode(self):
"""
Get the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
"""
return IDLE_LIMIT[self._fields['seedIdleMode'].value]
def _set_seed_idle_mode(self, mode):
"""
Set the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
"""
if isinstance(mode, str):
self._fields['seedIdleMode'] = Field(IDLE_LIMIT[mode], True)
self._push()
else:
raise ValueError("Not a valid limit")
seed_idle_mode = property(_get_seed_idle_mode, _set_seed_idle_mode, None,
"""
Seed idle mode as string. Can be one of 'global', 'single' or 'unlimited'.
* global, use session seed idle limit.
* single, use torrent seed idle limit. See seed_idle_limit.
* unlimited, no seed idle limit.
This is a mutator.
"""
)
def _get_seed_ratio_limit(self):
"""
Get the seed ratio limit as float.
"""
return float(self._fields['seedRatioLimit'].value)
def _set_seed_ratio_limit(self, limit):
"""
Set the seed ratio limit as float.
"""
if isinstance(limit, (int, long, float)) and limit >= 0.0:
self._fields['seedRatioLimit'] = Field(float(limit), True)
self._push()
else:
raise ValueError("Not a valid limit")
seed_ratio_limit = property(_get_seed_ratio_limit, _set_seed_ratio_limit, None
, "Torrent seed ratio limit as float. Also see seed_ratio_mode. This is a mutator.")
def _get_seed_ratio_mode(self):
"""
Get the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
"""
return RATIO_LIMIT[self._fields['seedRatioMode'].value]
def _set_seed_ratio_mode(self, mode):
"""
Set the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
"""
if isinstance(mode, str):
self._fields['seedRatioMode'] = Field(RATIO_LIMIT[mode], True)
self._push()
else:
raise ValueError("Not a valid limit")
seed_ratio_mode = property(_get_seed_ratio_mode, _set_seed_ratio_mode, None,
"""
Seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
* global, use session seed ratio limit.
* single, use torrent seed ratio limit. See seed_ratio_limit.
* unlimited, no seed ratio limit.
This is a mutator.
"""
)
def _get_upload_limit(self):
"""
Get the upload limit.
Can be a number or None.
"""
if self._fields['uploadLimited'].value:
return self._fields['uploadLimit'].value
else:
return None
def _set_upload_limit(self, limit):
"""
Set the upload limit.
Can be a number, 'session' or None.
"""
if isinstance(limit, (int, long)):
self._fields['uploadLimited'] = Field(True, True)
self._fields['uploadLimit'] = Field(limit, True)
self._push()
elif limit == None:
self._fields['uploadLimited'] = Field(False, True)
self._push()
else:
raise ValueError("Not a valid limit")
upload_limit = property(_get_upload_limit, _set_upload_limit, None, "Upload limit in Kbps or None. This is a mutator.")
def _get_queue_position(self):
if self._rpc_version() >= 14:
return self._fields['queuePosition'].value
else:
return 0
def _set_queue_position(self, position):
if self._rpc_version() >= 14:
if isinstance(position, (int, long)):
self._fields['queuePosition'] = Field(position, True)
self._push()
else:
raise ValueError("Not a valid position")
else:
pass
queue_position = property(_get_queue_position, _set_queue_position, None, "Queue position")
def update(self, timeout=None):
"""Update the torrent information."""
self._push()
torrent = self._client.get_torrent(self.id, timeout=timeout)
self._update_fields(torrent)
def start(self, bypass_queue=False, timeout=None):
"""
Start the torrent.
"""
self._incoming_pending = True
self._client.start_torrent(self.id, bypass_queue=bypass_queue, timeout=timeout)
def stop(self, timeout=None):
"""Stop the torrent."""
self._incoming_pending = True
self._client.stop_torrent(self.id, timeout=timeout)
def move_data(self, location, timeout=None):
"""Move torrent data to location."""
self._incoming_pending = True
self._client.move_torrent_data(self.id, location, timeout=timeout)
def locate_data(self, location, timeout=None):
"""Locate torrent data at location."""
self._incoming_pending = True
self._client.locate_torrent_data(self.id, location, timeout=timeout)
+3
View File
@@ -3,6 +3,7 @@
# Licensed under the MIT license.
import socket, datetime, logging
from collections import namedtuple
import transmissionrpc.constants as constants
from transmissionrpc.constants import LOGGER
@@ -186,3 +187,5 @@ def add_stdout_logger(level='debug'):
trpc_logger.setLevel(loglevel)
loghandler.setLevel(loglevel)
trpc_logger.addHandler(loghandler)
Field = namedtuple('Field', ['value', 'dirty'])