Merge branch 'refs/heads/develop' into desktop

Conflicts:
	CouchPotato.py
This commit is contained in:
Ruud committed 2012-02-19 13:14:56 +01:00
commit a4a4a6a185
47 files changed
+3425 -360

No files matched your search

-23
View File
@@ -8,7 +8,6 @@ from couchpotato.environment import Env
from flask import request from flask import request
from uuid import uuid4 from uuid import uuid4
import os import os
import thread
import time import time
import traceback import traceback
import webbrowser import webbrowser
@@ -32,7 +31,6 @@ class Core(Plugin):
addEvent('app.crappy_shutdown', self.crappyShutdown) addEvent('app.crappy_shutdown', self.crappyShutdown)
addEvent('app.crappy_restart', self.crappyRestart) addEvent('app.crappy_restart', self.crappyRestart)
addEvent('app.load', self.launchBrowser, priority = 1) addEvent('app.load', self.launchBrowser, priority = 1)
addEvent('app.load', self.monitorParent)
addEvent('app.base_url', self.createBaseUrl) addEvent('app.base_url', self.createBaseUrl)
addEvent('app.api_url', self.createApiUrl) addEvent('app.api_url', self.createApiUrl)
@@ -41,27 +39,6 @@ class Core(Plugin):
self.removeRestartFile() 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): def md5Password(self, value):
return md5(value) if value else '' return md5(value) if value else ''
+9 -1
View File
@@ -64,7 +64,7 @@ class Updater(Plugin):
return return
log.info('Checking for new version on github for %s' % self.repo_name) 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() self.repo.fetch()
current_branch = self.repo.getCurrentBranch().name current_branch = self.repo.getCurrentBranch().name
@@ -135,5 +135,13 @@ class Updater(Plugin):
except: except:
log.error('Couldn\'t remove %s: %s' % (full_path, traceback.format_exc())) 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): def isEnabled(self):
return super(Updater, self).isEnabled() and Env.get('uses_git') 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.event import addEvent
from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from couchpotato.core.helpers.encoding import toSafeString
import os import os
log = CPLog(__name__) log = CPLog(__name__)
@@ -42,6 +42,6 @@ class Blackhole(Downloader):
pass pass
except: 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
return False return False
@@ -29,6 +29,7 @@ config = [{
}, },
{ {
'name': 'password', 'name': 'password',
'type': 'password',
}, },
{ {
'name': 'paused', 'name': 'paused',
@@ -43,6 +44,7 @@ config = [{
{ {
'name': 'ratio', 'name': 'ratio',
'default': 10, 'default': 10,
'type': 'int',
'advanced': True, 'advanced': True,
'description': 'Stop transfer when reaching ratio', 'description': 'Stop transfer when reaching ratio',
}, },
@@ -1,3 +1,4 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -17,7 +18,6 @@ class Transmission(Downloader):
log.info('Sending "%s" to Transmission.' % data.get('name')) log.info('Sending "%s" to Transmission.' % data.get('name'))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = self.conf('host').split(':')
if not isInt(host[1]): if not isInt(host[1]):
@@ -26,24 +26,21 @@ class Transmission(Downloader):
# Set parameters for Transmission # Set parameters for Transmission
params = { params = {
'paused': self.conf('paused', 0), 'paused': self.conf('paused', default = 0),
'download_dir': self.conf('directory', None) 'download_dir': self.conf('directory', default = None)
}
change_params = {
'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': 1 if self.conf('ratio') else 0
} }
try: try:
tc = transmissionrpc.Client(host[0], port = host[1], user = self.conf('username'), password = self.conf('password')) 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 # Change settings of added torrents
for item in tr_id: try:
try: torrent.seed_ratio_limit = self.conf('ratio')
tc.change(item, timeout = None, **change_params) torrent.seed_ratio_mode = 'single' if self.conf('ratio') else 'global'
except transmissionrpc.TransmissionError, e: except transmissionrpc.TransmissionError, e:
log.error('Failed to change settings for transfer in transmission: %s' % e) log.error('Failed to change settings for transfer in transmission: %s' % e)
return True return True
+2
View File
@@ -34,6 +34,8 @@ def mergeDicts(a, b):
else: else:
if isDict(current_src[key]) and isDict(current_dst[key]): if isDict(current_src[key]) and isDict(current_dst[key]):
stack.append((current_dst[key], current_src[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: else:
current_dst[key] = current_src[key] current_dst[key] = current_src[key]
return dst return dst
+13 -11
View File
@@ -32,9 +32,9 @@ class Loader(object):
self.paths[provider + '_provider'] = (25, 'couchpotato.core.providers.' + provider, path) self.paths[provider + '_provider'] = (25, 'couchpotato.core.providers.' + provider, path)
for type, tuple in self.paths.iteritems(): for plugin_type, plugin_tuple in self.paths.iteritems():
priority, module, dir = tuple priority, module, dir_name = plugin_tuple
self.addFromDir(type, priority, module, dir) self.addFromDir(plugin_type, priority, module, dir_name)
def run(self): def run(self):
did_save = 0 did_save = 0
@@ -51,19 +51,21 @@ class Loader(object):
did_save += self.loadSettings(m, module_name, save = False) did_save += self.loadSettings(m, module_name, save = False)
self.loadPlugins(m, plugin.get('name')) 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())) log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc()))
if did_save: if did_save:
fireEvent('settings.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) 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) 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): def loadSettings(self, module, name, save = True):
try: try:
@@ -75,7 +77,7 @@ class Loader(object):
options[option['name']] = option options[option['name']] = option
fireEvent('settings.register', section_name = section['name'], options = options, save = save) fireEvent('settings.register', section_name = section['name'], options = options, save = save)
return True return True
except Exception, e: except:
log.debug("Failed loading settings for '%s': %s" % (name, traceback.format_exc())) log.debug("Failed loading settings for '%s': %s" % (name, traceback.format_exc()))
return False return False
@@ -92,7 +94,7 @@ class Loader(object):
log.error("Failed loading plugin '%s': %s" % (module.__file__, traceback.format_exc())) log.error("Failed loading plugin '%s': %s" % (module.__file__, traceback.format_exc()))
return False return False
def addModule(self, priority, type, module, name): def addModule(self, priority, plugin_type, module, name):
if not self.modules.get(priority): if not self.modules.get(priority):
self.modules[priority] = {} self.modules[priority] = {}
@@ -100,7 +102,7 @@ class Loader(object):
self.modules[priority][module] = { self.modules[priority][module] = {
'priority': priority, 'priority': priority,
'module': module, 'module': module,
'type': type, 'type': plugin_type,
'name': name, 'name': name,
} }
+6 -2
View File
@@ -22,6 +22,9 @@ class CPLog(object):
def error(self, msg): def error(self, msg):
self.logger.error(self.addContext(msg)) self.logger.error(self.addContext(msg))
def warning(self, msg):
self.logger.warning(self.addContext(msg))
def critical(self, msg): def critical(self, msg):
self.logger.critical(self.addContext(msg), exc_info = 1) self.logger.critical(self.addContext(msg), exc_info = 1)
@@ -35,7 +38,7 @@ class CPLog(object):
pass pass
from couchpotato.environment import Env from couchpotato.environment import Env
if not Env.setting('development'): if not Env.get('dev'):
for replace in self.replace_private: for replace in self.replace_private:
msg = re.sub('(%s=)[^\&]+' % replace, '%s=xxx' % replace, msg) msg = re.sub('(%s=)[^\&]+' % replace, '%s=xxx' % replace, msg)
@@ -43,7 +46,8 @@ class CPLog(object):
# Replace api key # Replace api key
try: try:
api_key = Env.setting('api_key') api_key = Env.setting('api_key')
msg = msg.replace(api_key, 'API_KEY') if api_key:
msg = msg.replace(api_key, 'API_KEY')
except: except:
pass pass
@@ -23,7 +23,7 @@ var NotificationBase = new Class({
self.request = Api.request('core_notifier.listener', { self.request = Api.request('core_notifier.listener', {
'initialDelay': 100, 'initialDelay': 100,
'delay': 3000, 'delay': 3000,
'onComplete': self.processData.bind(self) 'onSuccess': self.processData.bind(self)
}) })
self.request.startTimer() 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 couchpotato.core.notifications.base import Notification
from gntp import notifier from gntp import notifier
import logging import logging
import thread
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -12,31 +10,35 @@ log = CPLog(__name__)
class Growl(Notification): class Growl(Notification):
registered = False
def __init__(self): def __init__(self):
super(Growl, self).__init__() super(Growl, self).__init__()
logging.getLogger('gntp').setLevel(logging.WARNING) logging.getLogger('gntp').setLevel(logging.WARNING)
if self.isEnabled():
self.register()
def register(self):
if self.registered: return
try: try:
def startGrowl(): self.growl = notifier.GrowlNotifier(
time.sleep(2) applicationName = 'CouchPotato',
try: notifications = ["Updates"],
self.growl = notifier.GrowlNotifier( defaultNotifications = ["Updates"],
applicationName = 'CouchPotato', applicationIcon = '%s/static/images/couch.png' % fireEvent('app.api_url', single = True),
notifications = ["Updates"], )
defaultNotifications = ["Updates"], self.growl.register()
applicationIcon = '%s/static/images/couch.png' % fireEvent('app.api_url', single = True), self.registered = True
)
self.growl.register()
except:
log.error('Failed register of growl: %s' % traceback.format_exc())
thread.start_new_thread(startGrowl, ())
except: except:
pass log.error('Failed register of growl: %s' % traceback.format_exc())
def notify(self, type = '', message = '', data = {}): def notify(self, type = '', message = '', data = {}):
if self.isDisabled(): return if self.isDisabled(): return
self.register()
try: try:
self.growl.notify( self.growl.notify(
noteType = "Updates", noteType = "Updates",
+1 -1
View File
@@ -10,7 +10,7 @@ class Automation(Plugin):
def __init__(self): def __init__(self):
if not Env.setting('development'): if not Env.get('dev'):
addEvent('app.load', self.addMovies) addEvent('app.load', self.addMovies)
def addMovies(self): def addMovies(self):
+1 -1
View File
@@ -174,7 +174,7 @@ class Plugin(object):
def getCache(self, cache_key, url = None, timeout = 300): def getCache(self, cache_key, url = None, timeout = 300):
cache = Env.get('cache').get(cache_key) cache = Env.get('cache').get(cache_key)
if cache: 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 return cache
if url: if url:
+16 -14
View File
@@ -29,7 +29,7 @@ class FileManager(Plugin):
from flask.helpers import send_from_directory from flask.helpers import send_from_directory
return send_from_directory(cache_dir, filename) 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 if not dest: # to Cache
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url))) dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
@@ -38,14 +38,15 @@ class FileManager(Plugin):
return dest return dest
try: try:
filedata = self.urlopen(url) filedata = self.urlopen(url, **urlopen_kwargs)
except: except:
return False return False
self.createFile(dest, filedata, binary = True) self.createFile(dest, filedata, binary = True)
return dest 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() db = get_session()
f = db.query(File).filter(File.path == toUnicode(path)).first() f = db.query(File).filter(File.path == toUnicode(path)).first()
@@ -56,7 +57,7 @@ class FileManager(Plugin):
f.path = toUnicode(path) f.path = toUnicode(path)
f.part = part f.part = part
f.available = available f.available = available
f.type_id = self.getType(type).id f.type_id = type_id
db.commit() db.commit()
@@ -64,23 +65,24 @@ class FileManager(Plugin):
return file_dict return file_dict
def getType(self, type): def getType(self, type_tuple):
db = get_session() 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: if not ft:
ft = FileType( ft = FileType(
type = toUnicode(type), type = toUnicode(type_type),
identifier = identifier, identifier = type_identifier,
name = toUnicode(identifier[0].capitalize() + identifier[1:]) name = toUnicode(type_identifier[0].capitalize() + type_identifier[1:])
) )
db.add(ft) db.add(ft)
db.commit() db.commit()
return ft type_dict = ft.to_dict()
db.remove()
return type_dict
def getTypes(self): def getTypes(self):
@@ -89,7 +91,7 @@ class FileManager(Plugin):
results = db.query(FileType).all() results = db.query(FileType).all()
types = [] types = []
for type in results: for type_object in results:
types.append(type.to_dict()) types.append(type_object.to_dict())
return types return types
+1 -1
View File
@@ -102,7 +102,7 @@ class LibraryPlugin(Plugin):
continue continue
file_path = fireEvent('file.download', url = image, single = True) 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: try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one() file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj) library.files.append(file_obj)
@@ -66,6 +66,7 @@ Page.Log = new Class({
text = text.replace(/\u001b\[31m/gi, '</span><span class="error">') text = text.replace(/\u001b\[31m/gi, '</span><span class="error">')
text = text.replace(/\u001b\[36m/gi, '</span><span class="debug">') 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\n/gi, '</span><span class="time">')
text = text.replace(/\u001b\[0m/gi, '</span><span>') text = text.replace(/\u001b\[0m/gi, '</span><span>')
+1 -1
View File
@@ -23,7 +23,7 @@ class Manage(Plugin):
addEvent('manage.update', self.updateLibrary) addEvent('manage.update', self.updateLibrary)
addApiView('manage.update', self.updateLibraryView) addApiView('manage.update', self.updateLibraryView)
if not Env.setting('development'): if not Env.get('dev'):
addEvent('app.load', self.updateLibrary) addEvent('app.load', self.updateLibrary)
def updateLibraryView(self): def updateLibraryView(self):
@@ -183,7 +183,7 @@ Block.Search.Item = new Class({
'click': self.showOptions.bind(self) 'click': self.showOptions.bind(self)
} }
}).adopt( }).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] 'src': info.images.poster[0]
}) : null, }) : null,
new Element('div.info').adopt( new Element('div.info').adopt(
@@ -288,7 +288,7 @@ Block.Search.Item = new Class({
self.options.adopt( self.options.adopt(
new Element('div').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] 'src': self.info.images.poster[0]
}) : null, }) : null,
self.info.in_wanted ? new Element('span.in_wanted', { self.info.in_wanted ? new Element('span.in_wanted', {
+1 -1
View File
@@ -79,7 +79,7 @@ class Release(Plugin):
properties = {} properties = {}
# Check database and update/insert if necessary # 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): 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.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.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.logger import CPLog
from couchpotato.core.plugins.base import Plugin 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 from couchpotato.environment import Env
import os.path import os
import re import re
import shutil import shutil
import traceback import traceback
@@ -123,6 +123,9 @@ class Renamer(Plugin):
# Move nfo depending on settings # Move nfo depending on settings
if file_type is 'nfo' and not self.conf('rename_nfo'): if file_type is 'nfo' and not self.conf('rename_nfo'):
log.debug('Skipping, renaming of %s disabled' % file_type) log.debug('Skipping, renaming of %s disabled' % file_type)
if self.conf('clean_up'):
for current_file in group['files'][file_type]:
remove_files.append(current_file)
continue continue
# Subtitle extra # Subtitle extra
@@ -190,19 +193,30 @@ class Renamer(Plugin):
# Check for extra subtitle files # Check for extra subtitle files
if file_type is 'subtitle': if file_type is 'subtitle':
rename_extras = self.getRenameExtras(
def test(s): extra_type = 'subtitle_extra',
return current_file[:-len(replacements['ext'])] in s replacements = replacements,
folder_name = folder_name,
for subtitle_extra in set(filter(test, group['files']['subtitle_extra'])): file_name = file_name,
replacements['ext'] = getExt(subtitle_extra) destination = destination,
group = group,
final_folder_name = self.doReplace(folder_name, replacements) current_file = current_file
final_file_name = self.doReplace(file_name, replacements) )
rename_files[subtitle_extra] = os.path.join(destination, final_folder_name, final_file_name) rename_files = mergeDicts(rename_files, rename_extras)
# Filename without cd etc # Filename without cd etc
if file_type is 'movie': 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) group['destination_dir'] = os.path.join(destination, final_folder_name)
if multiple: if multiple:
@@ -290,11 +304,15 @@ class Renamer(Plugin):
# Remove files # Remove files
for src in remove_files: for src in remove_files:
if isinstance(src, File):
src = src.path
log.info('(fake) Removing "%s"' % src) log.info('(fake) Removing "%s"' % src)
# Remove matching releases # Remove matching releases
for release in remove_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 # Search for trailers etc
fireEventAsync('renamer.after', group) fireEventAsync('renamer.after', group)
@@ -309,6 +327,22 @@ class Renamer(Plugin):
self.renaming_started = False 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): def moveFile(self, old, dest):
try: try:
shutil.move(old, dest) 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 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'] ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = { extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'], 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf'],
'movie_extra': ['mds'],
'dvd': ['vts_*', 'vob'], 'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'], 'nfo': ['nfo', 'txt', 'tag'],
'subtitle': ['sub', 'srt', 'ssa', 'ass'], 'subtitle': ['sub', 'srt', 'ssa', 'ass'],
@@ -42,6 +43,7 @@ class Scanner(Plugin):
'trailer': ('video', 'trailer'), 'trailer': ('video', 'trailer'),
'nfo': ('nfo', 'nfo'), 'nfo': ('nfo', 'nfo'),
'movie': ('video', 'movie'), 'movie': ('video', 'movie'),
'movie': ('movie', 'movie_extra'),
'backdrop': ('image', 'backdrop'), 'backdrop': ('image', 'backdrop'),
'leftover': ('leftover', 'leftover'), 'leftover': ('leftover', 'leftover'),
} }
@@ -267,6 +269,7 @@ class Scanner(Plugin):
# Group extra (and easy) files first # Group extra (and easy) files first
# images = self.getImages(group['unsorted_files']) # images = self.getImages(group['unsorted_files'])
group['files'] = { group['files'] = {
'movie_extra': self.getMovieExtras(group['unsorted_files']),
'subtitle': self.getSubtitles(group['unsorted_files']), 'subtitle': self.getSubtitles(group['unsorted_files']),
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']), 'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']), 'nfo': self.getNfo(group['unsorted_files']),
@@ -281,6 +284,10 @@ class Scanner(Plugin):
else: else:
group['files']['movie'] = self.getMediaFiles(group['unsorted_files']) 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) log.debug('Getting metadata for %s' % identifier)
group['meta_data'] = self.getMetaData(group) group['meta_data'] = self.getMetaData(group)
@@ -470,6 +477,9 @@ class Scanner(Plugin):
return set(filter(test, files)) 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 getDVDFiles(self, files):
def test(s): def test(s):
return self.isDVDFile(s) return self.isDVDFile(s)
+8
View File
@@ -23,4 +23,12 @@ class Score(Plugin):
score += sizeScore(nzb['size']) 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 return score
@@ -28,7 +28,7 @@ config = [{
{ {
'name': 'ignored_words', 'name': 'ignored_words',
'label': '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) single_category = kwargs.get('single_category', False)
retention = Env.setting('retention', section = 'nzb') 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'])) log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name']))
return False 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): def __init__(self):
addEvent('provider.belongs_to', self.belongsTo) 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): def belongsTo(self, url, host = None):
try: try:
hostname = urlparse(url).hostname 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.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import json import json
import os.path import os
import shutil import shutil
import traceback import traceback
@@ -30,20 +30,24 @@ class IMDBAPI(MovieProvider):
cache_key = 'imdbapi.cache.%s' % q 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')})) cached = self.getCache(cache_key, self.urls['search'] % urlencode({'t': name_year.get('name'), 'y': name_year.get('year')}))
result = self.parseMovie(cached) if cached:
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') result = self.parseMovie(cached)
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
return [result]
return [result] return []
def getInfo(self, identifier = None): def getInfo(self, identifier = None):
cache_key = 'imdbapi.cache.%s' % identifier cache_key = 'imdbapi.cache.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % identifier) cached = self.getCache(cache_key, self.urls['info'] % identifier)
result = self.parseMovie(cached) if cached:
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') result = self.parseMovie(cached)
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
return result
return result return {}
def parseMovie(self, movie): def parseMovie(self, movie):
-17
View File
@@ -6,22 +6,5 @@ import time
class NZBProvider(YarrProvider): class NZBProvider(YarrProvider):
type = 'nzb' 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): def calculateAge(self, unix):
return int(time.time() - unix) / 24 / 60 / 60 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.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
@@ -20,6 +20,7 @@ class NZBMatrix(NZBProvider, RSS):
} }
cat_ids = [ cat_ids = [
([50], ['bd50']),
([42, 53], ['720p', '1080p']), ([42, 53], ['720p', '1080p']),
([2], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([2], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
([54], ['brrip']), ([54], ['brrip']),
@@ -3,4 +3,19 @@ from .main import KickAssTorrents
def start(): def start():
return KickAssTorrents() 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.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
import StringIO
import gzip
import re
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -7,8 +14,124 @@ log = CPLog(__name__)
class KickAssTorrents(TorrentProvider): class KickAssTorrents(TorrentProvider):
urls = { urls = {
'download': 'http://torrents.thepiratebay.org/%s/%s.torrent', 'test': 'http://www.kat.ph/',
'detail': 'http://www.kat.ph/%s-t%s.html', 'detail': 'http://www.kat.ph/%s-t%s.html',
'search': 'http://www.kat.ph/%s-i%s/', '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 from couchpotato.core.providers.base import Provider
log = CPLog(__name__)
class TrailerProvider(Provider): class TrailerProvider(Provider):
type = 'trailer' type = 'trailer'
def __init__(self):
addEvent('trailer.search', self.search)
@@ -1,4 +1,5 @@
from BeautifulSoup import SoupStrainer, BeautifulSoup from BeautifulSoup import SoupStrainer, BeautifulSoup
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.trailer.base import TrailerProvider from couchpotato.core.providers.trailer.base import TrailerProvider
from string import letters, digits from string import letters, digits
@@ -16,47 +17,40 @@ class HDTrailers(TrailerProvider):
} }
providers = ['apple.ico', 'yahoo.ico', 'moviefone.ico', 'myspace.ico', 'favicon.ico'] 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) url = self.urls['api'] % self.movieUrlName(movie_name)
try: data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url)
data = self.urlopen(url)
except: result_data = {}
return {}
p480 = []
p720 = []
p1080 = []
did_alternative = False did_alternative = False
for provider in self.providers: for provider in self.providers:
results = self.findByProvider(data, provider) results = self.findByProvider(data, provider)
# Find alternative # Find alternative
if results.get('404') and not did_alternative: if results.get('404') and not did_alternative:
results = self.findViaAlternative(movie_name) results = self.findViaAlternative(group)
did_alternative = True did_alternative = True
p480.extend(results.get('480p')) result_data = mergeDicts(result_data, results)
p720.extend(results.get('720p'))
p1080.extend(results.get('1080p'))
return {'480p':p480, '720p':p720, '1080p':p1080} return result_data
def findViaAlternative(self, movie): def findViaAlternative(self, group):
results = {'480p':[], '720p':[], '1080p':[]} results = {'480p':[], '720p':[], '1080p':[]}
url = "%s?%s" % (self.url['backup'], urlencode({'s':movie})) movie_name = group['library']['titles'][0]['title']
try:
data = self.urlopen(url) url = "%s?%s" % (self.url['backup'], urlencode({'s':movie_name}))
except: data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
return results
try: try:
tables = SoupStrainer('div') tables = SoupStrainer('div')
html = BeautifulSoup(data, parseOnlyThese = tables) 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: for h2 in result_table:
if 'trailer' in h2.lower(): if 'trailer' in h2.lower():
@@ -88,6 +82,8 @@ class HDTrailers(TrailerProvider):
break break
if 'trailer' in trtext and not 'clip' in trtext and provider in trtext: if 'trailer' in trtext and not 'clip' in trtext and provider in trtext:
nr = 0 nr = 0
if 'trailer' not in tr.find('span', 'standardTrailerName').text.lower():
continue
resolutions = tr.findAll('td', attrs = {'class':'bottomTableResolution'}) resolutions = tr.findAll('td', attrs = {'class':'bottomTableResolution'})
for res in resolutions: for res in resolutions:
results[str(res.a.contents[0])].insert(0, res.a['href']) results[str(res.a.contents[0])].insert(0, res.a['href'])
+1
View File
@@ -8,6 +8,7 @@ class Env(object):
_encoding = '' _encoding = ''
_uses_git = False _uses_git = False
_debug = False _debug = False
_dev = False
_settings = Settings() _settings = Settings()
_loader = Loader() _loader = Loader()
_cache = None _cache = None
+15 -30
View File
@@ -11,7 +11,7 @@ import logging
import os.path import os.path
import sys import sys
import time import time
import traceback import warnings
def getOptions(base_path, args): def getOptions(base_path, args):
@@ -46,25 +46,7 @@ def cleanup():
time.sleep(1) time.sleep(1)
def runCouchPotato(options, base_path, args, desktop = None): def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, Env = None, 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)
try: try:
locale.setlocale(locale.LC_ALL, "") 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') debug = options.debug or Env.setting('debug', default = False, type = 'bool')
Env.set('debug', debug) 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) atexit.register(cleanup)
# Use reloader
reloader = debug is True and development and not Env.get('desktop') and not options.daemon
# Disable server access log # Disable server access log
logging.getLogger('werkzeug').setLevel(logging.WARNING) logging.getLogger('werkzeug').setLevel(logging.WARNING)
# Only run once when debugging # Only run once when debugging
fire_load = False 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
logger = logging.getLogger() logger = logging.getLogger()
@@ -128,6 +116,10 @@ def runCouchPotato(options, base_path, args, desktop = None):
log = CPLog(__name__) log = CPLog(__name__)
log.debug('Started with options %s' % options) 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 # Load configs & plugins
loader = Env.get('loader') loader = Env.get('loader')
@@ -168,7 +160,6 @@ def runCouchPotato(options, base_path, args, desktop = None):
from couchpotato import app from couchpotato import app
api_key = Env.setting('api_key') api_key = Env.setting('api_key')
url_base = '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '' 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 # Basic config
app.secret_key = api_key app.secret_key = api_key
@@ -194,10 +185,4 @@ def runCouchPotato(options, base_path, args, desktop = None):
if fire_load: fireEventAsync('app.load') if fire_load: fireEventAsync('app.load')
# Go go go! # Go go go!
try: app.run(**config)
app.run(**config)
except (KeyboardInterrupt, SystemExit):
raise
except:
log.error('Failed starting: %s' % traceback.format_exc())
raise
+15 -12
View File
@@ -14,13 +14,17 @@
############### EDIT ME ################## ############### EDIT ME ##################
# path to app # 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 # path to python bin
DAEMON=/usr/bin/python DAEMON=/usr/bin/python
# startup args # Path to store PID file
DAEMON_OPTS=" CouchPotato.py" PID_FILE=/var/run/couchpotato/server.pid
PID_PATH=$(dirname $PID_FILE)
# script name # script name
NAME=couchpotato NAME=couchpotato
@@ -28,10 +32,8 @@ NAME=couchpotato
# app name # app name
DESC=CouchPotato DESC=CouchPotato
# user # startup args
RUN_AS=root DAEMON_OPTS=" CouchPotato.py --daemon --pid_file=${PID_FILE}"
PID_FILE=/var/run/couchpotato.pid
############### END EDIT ME ################## ############### END EDIT ME ##################
@@ -42,18 +44,19 @@ set -e
case "$1" in case "$1" in
start) start)
echo "Starting $DESC" 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) stop)
echo "Stopping $DESC" echo "Stopping $DESC"
start-stop-daemon --stop --pidfile $PID_FILE start-stop-daemon --stop --pidfile $PID_FILE --retry 15
;; ;;
restart|force-reload) restart|force-reload)
echo "Restarting $DESC" echo "Restarting $DESC"
start-stop-daemon --stop --pidfile $PID_FILE start-stop-daemon --stop --pidfile $PID_FILE --retry 15
sleep 15 start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --make-pidfile --exec $DAEMON -- $DAEMON_OPTS
;; ;;
*) *)
N=/etc/init.d/$NAME 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) file(self.pidfile, 'w+').write("%s\n" % pid)
def delpid(self): def delpid(self):
os.remove(self.pidfile) if os.path.exists(self.pidfile):
os.remove(self.pidfile)
def start(self): def start(self):
""" """
@@ -112,8 +113,7 @@ class Daemon():
except OSError, err: except OSError, err:
err = str(err) err = str(err)
if err.find("No such process") > 0: if err.find("No such process") > 0:
if os.path.exists(self.pidfile): self.delpid()
os.remove(self.pidfile)
else: else:
print str(err) print str(err)
sys.exit(1) sys.exit(1)
+1 -1
View File
@@ -11,6 +11,6 @@ from transmissionrpc.client import Client
from transmissionrpc.utils import add_stdout_logger from transmissionrpc.utils import add_stdout_logger
__author__ = u'Erik Svensson <erik.public@gmail.com>' __author__ = u'Erik Svensson <erik.public@gmail.com>'
__version__ = u'0.8' __version__ = u'0.9'
__copyright__ = u'Copyright (c) 2008-2011 Erik Svensson' __copyright__ = u'Copyright (c) 2008-2011 Erik Svensson'
__license__ = u'MIT' __license__ = u'MIT'
+228 -88
View File
@@ -2,7 +2,7 @@
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
import re, time import re, time, operator, warnings
import urllib2, urlparse, base64 import urllib2, urlparse, base64
try: try:
@@ -36,7 +36,7 @@ def debug_httperror(error):
'data': data, 'data': data,
} }
}, },
indent=2 indent = 2
) )
) )
@@ -59,7 +59,7 @@ class Client(object):
Client is the class handling the Transmission JSON-RPC client protocol. 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)): if isinstance(timeout, (int, long, float)):
self._query_timeout = float(timeout) self._query_timeout = float(timeout)
else: else:
@@ -91,7 +91,7 @@ class Client(object):
elif user or password: elif user or password:
LOGGER.warning('Either user or password missing, not using authentication.') LOGGER.warning('Either user or password missing, not using authentication.')
self._sequence = 0 self._sequence = 0
self.session = Session() self.session = None
self.session_id = 0 self.session_id = 0
self.server_version = None self.server_version = None
self.protocol_version = None self.protocol_version = None
@@ -117,9 +117,9 @@ class Client(object):
""" """
self._query_timeout = DEFAULT_TIMEOUT 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. Query Transmission through HTTP.
""" """
@@ -129,7 +129,7 @@ class Client(object):
if timeout is None: if timeout is None:
timeout = self._query_timeout timeout = self._query_timeout
while True: 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: try:
result = self.http_handler.request(self.url, query, headers, timeout) result = self.http_handler.request(self.url, query, headers, timeout)
break break
@@ -150,7 +150,7 @@ class Client(object):
request_count += 1 request_count += 1
return result 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 Send json-rpc request to Transmission using http POST
""" """
@@ -182,7 +182,7 @@ class Client(object):
LOGGER.error('HTTP data: \"%s\"' % (http_data)) LOGGER.error('HTTP data: \"%s\"' % (http_data))
raise raise
LOGGER.debug(json.dumps(data, indent=2)) LOGGER.debug(json.dumps(data, indent = 2))
if 'result' in data: if 'result' in data:
if data['result'] != 'success': if data['result'] != 'success':
raise TransmissionError('Query failed with result \"%s\".' % (data['result'])) raise TransmissionError('Query failed with result \"%s\".' % (data['result']))
@@ -264,7 +264,10 @@ class Client(object):
""" """
Update session data. 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): def _update_server_version(self):
if self.server_version is None: 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. Add a warning to the log if the Transmission RPC version is lower then the provided version.
""" """
if self.rpc_version < 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: Additional arguments are:
===================== ===== =========== ============================================================= ===================== ===== =========== =============================================================
@@ -327,6 +331,41 @@ class Client(object):
``priority_normal`` 1 - A list of file id's that should have normal priority. ``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 = {} args = {}
if data: if data:
@@ -335,31 +374,16 @@ class Client(object):
raise ValueError('No torrent data or torrent uri.') raise ValueError('No torrent data or torrent uri.')
for key, value in kwargs.iteritems(): for key, value in kwargs.iteritems():
argument = make_rpc_name(key) argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add', (arg, val) = argument_value_convert('torrent-add', argument, value, self.rpc_version)
argument, value, self.rpc_version)
args[arg] = val 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): 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:
===================== ===== =========== ============================================================= .. WARNING::
Argument RPC Replaced by Description Deprecated, please use add_torrent.
===================== ===== =========== =============================================================
``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.
===================== ===== =========== =============================================================
""" """
if uri is None: if uri is None:
raise ValueError('add_uri requires a URI.') raise ValueError('add_uri requires a URI.')
@@ -370,47 +394,145 @@ class Client(object):
if parsed_uri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']: if parsed_uri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']:
torrent_file = urllib2.urlopen(uri) torrent_file = urllib2.urlopen(uri)
torrent_data = base64.b64encode(torrent_file.read()) torrent_data = base64.b64encode(torrent_file.read())
warnings.warn('add_uri has been deprecated, please use add_torrent instead.', DeprecationWarning)
if torrent_data: if torrent_data:
return self.add(torrent_data, **kwargs) return self.add(torrent_data, **kwargs)
else: 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 remove torrent(s) with provided id(s). Local data is removed if
delete_data is True, otherwise not. delete_data is True, otherwise not.
""" """
self._rpc_version_warning(3) self._rpc_version_warning(3)
self._request('torrent-remove', 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): def remove(self, ids, delete_data = False, timeout = None):
"""start torrent(s) with provided id(s)""" """
.. 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' method = 'torrent-start'
if bypass_queue and self.rpc_version >= 14: if bypass_queue and self.rpc_version >= 14:
method = 'torrent-start-now' 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)""" """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)""" """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)""" """Reannounce torrent(s) with provided id(s)"""
self._rpc_version_warning(5) 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): def reannounce(self, ids, timeout = None):
"""Get detailed information for torrent(s) with provided id(s).""" """
.. 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: if not arguments:
arguments = self.torrent_get_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, Get list of files for provided torrent id(s). If ids is empty,
information for all torrents are fetched. This function returns a dictionary 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'] 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 = {} result = {}
for tid, torrent in request_result.iteritems(): for tid, torrent in request_result.iteritems():
result[tid] = torrent.files() result[tid] = torrent.files()
return result 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 Set file properties. Takes a dictionary with similar contents as the result
of `get_files`. of `get_files`.
@@ -498,14 +620,7 @@ class Client(object):
args['priority_low'] = low args['priority_low'] = low
self.change([tid], **args) self.change([tid], **args)
def list(self, timeout=None): def change_torrent(self, ids, timeout = None, **kwargs):
"""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):
""" """
Change torrent parameters for the torrent(s) with the supplied id's. The Change torrent parameters for the torrent(s) with the supplied id's. The
parameters are: parameters are:
@@ -546,54 +661,80 @@ class Client(object):
args = {} args = {}
for key, value in kwargs.iteritems(): for key, value in kwargs.iteritems():
argument = make_rpc_name(key) argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-set' (arg, val) = argument_value_convert('torrent-set' , argument, value, self.rpc_version)
, argument, value, self.rpc_version)
args[arg] = val args[arg] = val
if len(args) > 0: if len(args) > 0:
self._request('torrent-set', args, ids, True, timeout=timeout) self._request('torrent-set', args, ids, True, timeout = timeout)
else: else:
ValueError("No arguments to set") 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.""" """Move torrent data to the new location."""
self._rpc_version_warning(6) self._rpc_version_warning(6)
args = {'location': location, 'move': True} 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): def move(self, ids, location, timeout = None):
"""Locate torrent data at the location.""" """
.. 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) self._rpc_version_warning(6)
args = {'location': location, 'move': False} 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.""" """Move transfer to the top of the queue."""
self._rpc_version_warning(14) 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.""" """Move transfer to the bottom of the queue."""
self._rpc_version_warning(14) self._rpc_version_warning(14)
self._request('queue-move-bottom', ids=ids, require_ids=True, timeout=timeout) self._request('queue-move-bottom', ids = ids, require_ids = True, timeout = timeout)
def queue_up(self, ids, timeout=None): def queue_up(self, ids, timeout = None):
"""Move transfer up in the queue.""" """Move transfer up in the queue."""
self._rpc_version_warning(14) 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.""" """Move transfer down in the queue."""
self._rpc_version_warning(14) 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""" """Get session parameters"""
self._request('session-get', timeout=timeout) self._request('session-get', timeout = timeout)
self._update_server_version() self._update_server_version()
return self.session return self.session
def set_session(self, timeout=None, **kwargs): def set_session(self, timeout = None, **kwargs):
""" """
Set session parameters. The parameters are: Set session parameters. The parameters are:
@@ -655,32 +796,31 @@ class Client(object):
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']: if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
raise ValueError('Invalid encryption value') raise ValueError('Invalid encryption value')
argument = make_rpc_name(key) argument = make_rpc_name(key)
(arg, val) = argument_value_convert('session-set' (arg, val) = argument_value_convert('session-set' , argument, value, self.rpc_version)
, argument, value, self.rpc_version)
args[arg] = val args[arg] = val
if len(args) > 0: 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.""" """Update block list. Returns the size of the block list."""
self._rpc_version_warning(5) self._rpc_version_warning(5)
result = self._request('blocklist-update', timeout=timeout) result = self._request('blocklist-update', timeout = timeout)
if 'blocklist-size' in result: if 'blocklist-size' in result:
return result['blocklist-size'] return result['blocklist-size']
return None 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 Tests to see if your incoming peer port is accessible from the
outside world. outside world.
""" """
self._rpc_version_warning(5) 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: if 'port-is-open' in result:
return result['port-is-open'] return result['port-is-open']
return None return None
def session_stats(self, timeout=None): def session_stats(self, timeout = None):
"""Get session statistics""" """Get session statistics"""
self._request('session-stats', timeout=timeout) self._request('session-stats', timeout = timeout)
return self.session return self.session
+2 -2
View File
@@ -15,9 +15,9 @@ class TransmissionError(Exception):
def __str__(self): def __str__(self):
if self.original: if self.original:
original_name = type(self.original).__name__ 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: else:
return self.message return self._message
class HTTPHandlerError(Exception): class HTTPHandlerError(Exception):
""" """
+84 -20
View File
@@ -2,6 +2,8 @@
# Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com> # Copyright (c) 2008-2011 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license. # Licensed under the MIT license.
from transmissionrpc.utils import Field
class Session(object): class Session(object):
""" """
Session is a class holding the session data for a Transmission daemon. Session is a class holding the session data for a Transmission daemon.
@@ -12,33 +14,95 @@ class Session(object):
``download-dir`` -> ``download_dir``. ``download-dir`` -> ``download_dir``.
""" """
def __init__(self, fields=None): def __init__(self, client=None, fields=None):
self.fields = {} self._client = client
self._fields = {}
if fields is not None: if fields is not None:
self.update(fields) self._update_fields(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
def __getattr__(self, name): def __getattr__(self, name):
try: try:
return self.fields[name] return self._fields[name].value
except KeyError: except KeyError:
raise AttributeError('No attribute %s' % name) raise AttributeError('No attribute %s' % name)
def __str__(self): def __str__(self):
text = '' text = ''
for key in sorted(self.fields.keys()): for key in sorted(self._fields.keys()):
text += "% 32s: %s\n" % (key[-32:], self.fields[key]) text += "% 32s: %s\n" % (key[-32:], self._fields[key].value)
return text 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 import sys, datetime
from transmissionrpc.constants import PRIORITY from transmissionrpc.constants import PRIORITY, RATIO_LIMIT, IDLE_LIMIT
from transmissionrpc.utils import format_timedelta from transmissionrpc.utils import Field, format_timedelta
class Torrent(object): class Torrent(object):
""" """
@@ -17,17 +17,19 @@ class Torrent(object):
def __init__(self, client, fields): def __init__(self, client, fields):
if 'id' not in fields: if 'id' not in fields:
raise ValueError('Torrent requires an id') raise ValueError('Torrent requires an id')
self.fields = {} self._fields = {}
self.update(fields) self._update_fields(fields)
self.client = client self._incoming_pending= False
self._outgoing_pending= False
self._client = client
def _getNameString(self, codec=None): def _getNameString(self, codec=None):
if codec is None: if codec is None:
codec = sys.getdefaultencoding() codec = sys.getdefaultencoding()
name = None name = None
# try to find name # try to find name
if 'name' in self.fields: if 'name' in self._fields:
name = self.fields['name'] name = self._fields['name'].value
# if name is unicode, try to decode # if name is unicode, try to decode
if isinstance(name, unicode): if isinstance(name, unicode):
try: try:
@@ -37,7 +39,7 @@ class Torrent(object):
return name return name
def __repr__(self): def __repr__(self):
tid = self.fields['id'] tid = self._fields['id'].value
name = self._getNameString() name = self._getNameString()
if isinstance(name, str): if isinstance(name, str):
return '<Torrent %d \"%s\">' % (tid, name) return '<Torrent %d \"%s\">' % (tid, name)
@@ -52,12 +54,53 @@ class Torrent(object):
return 'Torrent' return 'Torrent'
def __copy__(self): 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): def _rpc_version(self):
if self.client: if self._client:
return self.client.rpc_version return self._client.rpc_version
return 2 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): def _status_old(self, code):
mapping = { mapping = {
@@ -82,26 +125,12 @@ class Torrent(object):
return mapping[code] return mapping[code]
def _status(self): def _status(self):
code = self.fields['status'] code = self._fields['status'].value
if self._rpc_version() >= 14: if self._rpc_version() >= 14:
return self._status_new(code) return self._status_new(code)
else: else:
return self._status_old(code) 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): def files(self):
""" """
Get list of files for this torrent. Get list of files for this torrent.
@@ -122,11 +151,11 @@ class Torrent(object):
} }
""" """
result = {} result = {}
if 'files' in self.fields: if 'files' in self._fields:
indices = xrange(len(self.fields['files'])) files = self._fields['files'].value
files = self.fields['files'] indices = xrange(len(files))
priorities = self.fields['priorities'] priorities = self._fields['priorities'].value
wanted = self.fields['wanted'] wanted = self._fields['wanted'].value
for item in zip(indices, files, priorities, wanted): for item in zip(indices, files, priorities, wanted):
selected = True if item[3] else False selected = True if item[3] else False
priority = PRIORITY[item[2]] priority = PRIORITY[item[2]]
@@ -138,12 +167,6 @@ class Torrent(object):
'completed': item[1]['bytesCompleted']} 'completed': item[1]['bytesCompleted']}
return result return result
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError:
raise AttributeError('No attribute %s' % name)
@property @property
def status(self): def status(self):
""" """
@@ -157,19 +180,21 @@ class Torrent(object):
def progress(self): def progress(self):
"""Get the download progress in percent.""" """Get the download progress in percent."""
try: 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: except ZeroDivisionError:
return 0.0 return 0.0
@property @property
def ratio(self): def ratio(self):
"""Get the upload/download ratio.""" """Get the upload/download ratio."""
return float(self.fields['uploadRatio']) return float(self._fields['uploadRatio'].value)
@property @property
def eta(self): def eta(self):
"""Get the "eta" as datetime.timedelta.""" """Get the "eta" as datetime.timedelta."""
eta = self.fields['eta'] eta = self._fields['eta'].value
if eta >= 0: if eta >= 0:
return datetime.timedelta(seconds=eta) return datetime.timedelta(seconds=eta)
else: else:
@@ -178,22 +203,22 @@ class Torrent(object):
@property @property
def date_active(self): def date_active(self):
"""Get the attribute "activityDate" as datetime.datetime.""" """Get the attribute "activityDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['activityDate']) return datetime.datetime.fromtimestamp(self._fields['activityDate'].value)
@property @property
def date_added(self): def date_added(self):
"""Get the attribute "addedDate" as datetime.datetime.""" """Get the attribute "addedDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['addedDate']) return datetime.datetime.fromtimestamp(self._fields['addedDate'].value)
@property @property
def date_started(self): def date_started(self):
"""Get the attribute "startDate" as datetime.datetime.""" """Get the attribute "startDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['startDate']) return datetime.datetime.fromtimestamp(self._fields['startDate'].value)
@property @property
def date_done(self): def date_done(self):
"""Get the attribute "doneDate" as datetime.datetime.""" """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): def format_eta(self):
""" """
@@ -203,7 +228,7 @@ class Torrent(object):
* If eta is -2 the result is 'unknown' * If eta is -2 the result is 'unknown'
* Otherwise eta is formatted as <days> <hours>:<minutes>:<seconds>. * Otherwise eta is formatted as <days> <hours>:<minutes>:<seconds>.
""" """
eta = self.fields['eta'] eta = self._fields['eta'].value
if eta == -1: if eta == -1:
return 'not available' return 'not available'
elif eta == -2: elif eta == -2:
@@ -211,10 +236,233 @@ class Torrent(object):
else: else:
return format_timedelta(self.eta) return format_timedelta(self.eta)
@property def _get_download_limit(self):
def priority(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. Get the priority as string.
Can be one of 'low', 'normal', 'high'. 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. # Licensed under the MIT license.
import socket, datetime, logging import socket, datetime, logging
from collections import namedtuple
import transmissionrpc.constants as constants import transmissionrpc.constants as constants
from transmissionrpc.constants import LOGGER from transmissionrpc.constants import LOGGER
@@ -186,3 +187,5 @@ def add_stdout_logger(level='debug'):
trpc_logger.setLevel(loglevel) trpc_logger.setLevel(loglevel)
loghandler.setLevel(loglevel) loghandler.setLevel(loglevel)
trpc_logger.addHandler(loghandler) trpc_logger.addHandler(loghandler)
Field = namedtuple('Field', ['value', 'dirty'])