mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-01-04 13:41:00 -05:00
Compare commits
110 Commits
2.2.0Alpha
...
2.2.0Alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01dfb7538d | ||
|
|
3f0d4675b6 | ||
|
|
f23c5caf80 | ||
|
|
bd22430b26 | ||
|
|
1189a7fdbc | ||
|
|
f3aa4f84fc | ||
|
|
ea26ce4700 | ||
|
|
a1e649b7e2 | ||
|
|
3b9f2b2cf0 | ||
|
|
7333d19e1c | ||
|
|
232d537d23 | ||
|
|
c6e17e7bcb | ||
|
|
54c6fd55dd | ||
|
|
0625aa1ca8 | ||
|
|
83643f3298 | ||
|
|
ff3c46fe1f | ||
|
|
0930f0dcee | ||
|
|
3221257310 | ||
|
|
8048a73156 | ||
|
|
ea552cd402 | ||
|
|
dcb925f621 | ||
|
|
cce91e1985 | ||
|
|
e17d417c2e | ||
|
|
a69f5bd2df | ||
|
|
97e53eb4d3 | ||
|
|
a6da2b7bee | ||
|
|
4a21e7c217 | ||
|
|
9bd3c7be44 | ||
|
|
434f5c4b2d | ||
|
|
d3cc4f9f07 | ||
|
|
a16aa17c17 | ||
|
|
68445d0409 | ||
|
|
32b68a45cc | ||
|
|
345f8359cc | ||
|
|
81f9886584 | ||
|
|
adbc618808 | ||
|
|
41eafc6b4b | ||
|
|
9f18d8e8c1 | ||
|
|
8c2c853166 | ||
|
|
97914906a0 | ||
|
|
f1ce4ed19b | ||
|
|
99185d8151 | ||
|
|
385b6b7ade | ||
|
|
81ea513f8c | ||
|
|
336b1ddba3 | ||
|
|
7274973322 | ||
|
|
af132965de | ||
|
|
5586742886 | ||
|
|
5868b51490 | ||
|
|
7f17a38b9b | ||
|
|
415e843ebb | ||
|
|
7ffc1192bb | ||
|
|
945e769a03 | ||
|
|
86c7fb86cc | ||
|
|
ff20f3f620 | ||
|
|
e8bef94706 | ||
|
|
d05fe2d680 | ||
|
|
4f8cc3f697 | ||
|
|
6fa619fa37 | ||
|
|
a43f5369ea | ||
|
|
2040173dc2 | ||
|
|
a15b7ec7ac | ||
|
|
6adcf2ce10 | ||
|
|
e756b9b5c1 | ||
|
|
b3de745849 | ||
|
|
77f3dc18b5 | ||
|
|
6b2f15f82e | ||
|
|
570e58611d | ||
|
|
6b69010aec | ||
|
|
e3e2fb7057 | ||
|
|
ece04909e7 | ||
|
|
963920eb88 | ||
|
|
cf5fa542b6 | ||
|
|
1be7e99754 | ||
|
|
14e3334682 | ||
|
|
b1e033dd55 | ||
|
|
111feb1b57 | ||
|
|
886b23d034 | ||
|
|
f2590792b3 | ||
|
|
02a497ed74 | ||
|
|
48df0eed84 | ||
|
|
0f58cbb671 | ||
|
|
9d71670f59 | ||
|
|
7f838ebb38 | ||
|
|
ef1cb05bc8 | ||
|
|
c14b3ed82a | ||
|
|
792e337936 | ||
|
|
6cd2e66052 | ||
|
|
728022b86d | ||
|
|
7718446313 | ||
|
|
66dea54053 | ||
|
|
f19b60bd41 | ||
|
|
09f1c92856 | ||
|
|
589715901d | ||
|
|
3f1a5ff5e0 | ||
|
|
49cd956d4c | ||
|
|
f9acde862f | ||
|
|
503e1dd899 | ||
|
|
c8e12b948d | ||
|
|
18949d68c0 | ||
|
|
0c51b6c016 | ||
|
|
63a5c22c1f | ||
|
|
f76e2a7b56 | ||
|
|
bab151d6f5 | ||
|
|
d43fec088b | ||
|
|
a8ca1cbcd7 | ||
|
|
ada3494483 | ||
|
|
43c238b7f1 | ||
|
|
128d10c51e | ||
|
|
1a1e01f9f6 |
6
PKG-INFO
6
PKG-INFO
@@ -1,8 +1,8 @@
|
||||
Metadata-Version: 1.0
|
||||
Name: SABnzbd
|
||||
Version: 2.2.0Alpha1
|
||||
Summary: SABnzbd-2.2.0Alpha1
|
||||
Home-page: http://sabnzbd.org
|
||||
Version: 2.2.0Alpha3
|
||||
Summary: SABnzbd-2.2.0Alpha3
|
||||
Home-page: https://sabnzbd.org
|
||||
Author: The SABnzbd Team
|
||||
Author-email: team@sabnzbd.org
|
||||
License: GNU General Public License 2 (GPL2 or later)
|
||||
|
||||
@@ -5,7 +5,7 @@ SABnzbd is an Open Source Binary Newsreader written in Python.
|
||||
|
||||
It's totally free, incredibly easy to use, and works practically everywhere.
|
||||
SABnzbd makes Usenet as simple and streamlined as possible by automating everything we can. All you have to do is add an `.nzb`. SABnzbd takes over from there, where it will be automatically downloaded, verified, repaired, extracted and filed away with zero human interaction.
|
||||
If you want to know more you can head over to our website: http://sabnzbd.org.
|
||||
If you want to know more you can head over to our website: https://sabnzbd.org.
|
||||
|
||||
## Resolving Dependencies
|
||||
|
||||
|
||||
43
README.mkd
43
README.mkd
@@ -1,24 +1,51 @@
|
||||
Release Notes - SABnzbd 2.2.0 Alpha 1
|
||||
Release Notes - SABnzbd 2.2.0 Alpha 3
|
||||
=========================================================
|
||||
|
||||
NOTE: Due to changes in this release, the queue will be converted when 2.2.0
|
||||
is started for the first time. Job order, settings and data will be preserved,
|
||||
but URL's that did finished fetching before the upgrade will be lost!
|
||||
is started for the first time. Job order, settings and data will be
|
||||
preserved, but all jobs will be unpaused and URL's that did not finish
|
||||
fetching before the upgrade will be lost!
|
||||
|
||||
## Changes in 2.2.0
|
||||
## Changes since 2.1.0
|
||||
- Direct Unpack: Jobs will start unpacking during the download, reduces
|
||||
post-processing time but requires capable hard drive. Only works for jobs that
|
||||
do not need repair. Will be enabled if your incomplete folder-speed > 60MB/s
|
||||
- Reduced memory usage, especially with larger queues
|
||||
- Slight improvement in download performance by removing internal locks
|
||||
- Removed 5 second delay between fetching URLs
|
||||
- Notifications can now be limited to certain Categories
|
||||
- Each item in the Queue and Filelist now has Move to Top/Bottom buttons
|
||||
- Smoother animations in Firefox (disabled previously due to FF high-CPU usage)
|
||||
- If enabled, replace dots in filenames also when there are spaces already
|
||||
- Jobs outside server retention are processed faster
|
||||
- Show missing articles in MB instead of number of articles
|
||||
- Obfuscated filenames are renamed during downloading, if possible
|
||||
- If enable_par_cleanup is disabled all par2 files be downloaded
|
||||
- If enabled, replace dots in filenames also when there are spaces already
|
||||
- Update GNTP bindings to 1.0.3
|
||||
- max_art_opt and replace_illegal moved from Switches to Specials
|
||||
- Removed Specials enable_meta, par2_multicore and allow_streaming
|
||||
- Windows: Full unicode support when calling repair and unpack
|
||||
- Windows: Move enable_multipar to Specials
|
||||
- Windows: Better indication of verification process before and after repair
|
||||
- Windows: MultiPar verification of a job is skipped after blocks are fetched
|
||||
- Windows & macOS: removed par2cmdline in favor of par2tbb/Multipar
|
||||
|
||||
## Bugfixes in 2.2.0
|
||||
## Bugfixes since 2.1.0
|
||||
- Shutdown/suspend did not work on some Linux systems
|
||||
- Deleting a job could result in write errors
|
||||
- Display warning if custom par2 parameters are wrong
|
||||
- macOS: Catch 'Protocol wrong type for socket' errors
|
||||
- RSS URLs with commas were broken
|
||||
- Fixed some "Saving failed" errors
|
||||
- Fixed crashing URLGrabber
|
||||
- Jobs with renamed files are now correctly handled when using Retry
|
||||
- Disk-space readings could be updated incorrectly
|
||||
- Correct redirect after enabling HTTPS in the Config
|
||||
- Fix race-condition in Post-processing
|
||||
- History would not always show latest changes
|
||||
- Convert HTML in error messages
|
||||
- Fixed unicode error during Sorting
|
||||
- Not all texts were shown in the selected Language
|
||||
- Windows: Fix error in MultiPar-code when first par2-file was damaged
|
||||
- macOS: Catch 'Protocol wrong type for socket' errors
|
||||
|
||||
## Translations
|
||||
- Added Hebrew translation by ION IL, many other languages updated.
|
||||
|
||||
@@ -428,9 +428,6 @@ def print_modules():
|
||||
else:
|
||||
logging.error(T('par2 binary... NOT found!'))
|
||||
|
||||
if sabnzbd.newsunpack.PAR2C_COMMAND:
|
||||
logging.info("par2cmdline binary... found (%s)", sabnzbd.newsunpack.PAR2C_COMMAND)
|
||||
|
||||
if sabnzbd.newsunpack.MULTIPAR_COMMAND:
|
||||
logging.info("MultiPar binary... found (%s)", sabnzbd.newsunpack.MULTIPAR_COMMAND)
|
||||
|
||||
|
||||
509
gntp/__init__.py
509
gntp/__init__.py
@@ -1,509 +0,0 @@
|
||||
import re
|
||||
import hashlib
|
||||
import time
|
||||
import StringIO
|
||||
|
||||
__version__ = '0.8'
|
||||
|
||||
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
|
||||
GNTP_INFO_LINE = re.compile(
|
||||
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
|
||||
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
|
||||
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
GNTP_INFO_LINE_SHORT = re.compile(
|
||||
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
GNTP_HEADER = re.compile('([\w-]+):(.+)')
|
||||
|
||||
GNTP_EOL = '\r\n'
|
||||
|
||||
|
||||
class BaseError(Exception):
|
||||
def gntp_error(self):
|
||||
error = GNTPError(self.errorcode, self.errordesc)
|
||||
return error.encode()
|
||||
|
||||
|
||||
class ParseError(BaseError):
|
||||
errorcode = 500
|
||||
errordesc = 'Error parsing the message'
|
||||
|
||||
|
||||
class AuthError(BaseError):
|
||||
errorcode = 400
|
||||
errordesc = 'Error with authorization'
|
||||
|
||||
|
||||
class UnsupportedError(BaseError):
|
||||
errorcode = 500
|
||||
errordesc = 'Currently unsupported by gntp.py'
|
||||
|
||||
|
||||
class _GNTPBuffer(StringIO.StringIO):
|
||||
"""GNTP Buffer class"""
|
||||
def writefmt(self, message="", *args):
|
||||
"""Shortcut function for writing GNTP Headers"""
|
||||
self.write((message % args).encode('utf8', 'replace'))
|
||||
self.write(GNTP_EOL)
|
||||
|
||||
|
||||
class _GNTPBase(object):
|
||||
"""Base initilization
|
||||
|
||||
:param string messagetype: GNTP Message type
|
||||
:param string version: GNTP Protocol version
|
||||
:param string encription: Encryption protocol
|
||||
"""
|
||||
def __init__(self, messagetype=None, version='1.0', encryption=None):
|
||||
self.info = {
|
||||
'version': version,
|
||||
'messagetype': messagetype,
|
||||
'encryptionAlgorithmID': encryption
|
||||
}
|
||||
self.headers = {}
|
||||
self.resources = {}
|
||||
|
||||
def __str__(self):
|
||||
return self.encode()
|
||||
|
||||
def _parse_info(self, data):
|
||||
"""Parse the first line of a GNTP message to get security and other info values
|
||||
|
||||
:param string data: GNTP Message
|
||||
:return dict: Parsed GNTP Info line
|
||||
"""
|
||||
|
||||
match = GNTP_INFO_LINE.match(data)
|
||||
|
||||
if not match:
|
||||
raise ParseError('ERROR_PARSING_INFO_LINE')
|
||||
|
||||
info = match.groupdict()
|
||||
if info['encryptionAlgorithmID'] == 'NONE':
|
||||
info['encryptionAlgorithmID'] = None
|
||||
|
||||
return info
|
||||
|
||||
def set_password(self, password, encryptAlgo='MD5'):
|
||||
"""Set a password for a GNTP Message
|
||||
|
||||
:param string password: Null to clear password
|
||||
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
|
||||
"""
|
||||
hash = {
|
||||
'MD5': hashlib.md5,
|
||||
'SHA1': hashlib.sha1,
|
||||
'SHA256': hashlib.sha256,
|
||||
'SHA512': hashlib.sha512,
|
||||
}
|
||||
|
||||
self.password = password
|
||||
self.encryptAlgo = encryptAlgo.upper()
|
||||
if not password:
|
||||
self.info['encryptionAlgorithmID'] = None
|
||||
self.info['keyHashAlgorithm'] = None
|
||||
return
|
||||
if not self.encryptAlgo in hash.keys():
|
||||
raise UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
|
||||
|
||||
hashfunction = hash.get(self.encryptAlgo)
|
||||
|
||||
password = password.encode('utf8')
|
||||
seed = time.ctime()
|
||||
salt = hashfunction(seed).hexdigest()
|
||||
saltHash = hashfunction(seed).digest()
|
||||
keyBasis = password + saltHash
|
||||
key = hashfunction(keyBasis).digest()
|
||||
keyHash = hashfunction(key).hexdigest()
|
||||
|
||||
self.info['keyHashAlgorithmID'] = self.encryptAlgo
|
||||
self.info['keyHash'] = keyHash.upper()
|
||||
self.info['salt'] = salt.upper()
|
||||
|
||||
def _decode_hex(self, value):
|
||||
"""Helper function to decode hex string to `proper` hex string
|
||||
|
||||
:param string value: Human readable hex string
|
||||
:return string: Hex string
|
||||
"""
|
||||
result = ''
|
||||
for i in range(0, len(value), 2):
|
||||
tmp = int(value[i:i + 2], 16)
|
||||
result += chr(tmp)
|
||||
return result
|
||||
|
||||
def _decode_binary(self, rawIdentifier, identifier):
|
||||
rawIdentifier += '\r\n\r\n'
|
||||
dataLength = int(identifier['Length'])
|
||||
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
|
||||
pointerEnd = pointerStart + dataLength
|
||||
data = self.raw[pointerStart:pointerEnd]
|
||||
if not len(data) == dataLength:
|
||||
raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
|
||||
return data
|
||||
|
||||
def _validate_password(self, password):
|
||||
"""Validate GNTP Message against stored password"""
|
||||
self.password = password
|
||||
if password == None:
|
||||
raise AuthError('Missing password')
|
||||
keyHash = self.info.get('keyHash', None)
|
||||
if keyHash is None and self.password is None:
|
||||
return True
|
||||
if keyHash is None:
|
||||
raise AuthError('Invalid keyHash')
|
||||
if self.password is None:
|
||||
raise AuthError('Missing password')
|
||||
|
||||
password = self.password.encode('utf8')
|
||||
saltHash = self._decode_hex(self.info['salt'])
|
||||
|
||||
keyBasis = password + saltHash
|
||||
key = hashlib.md5(keyBasis).digest()
|
||||
keyHash = hashlib.md5(key).hexdigest()
|
||||
|
||||
if not keyHash.upper() == self.info['keyHash'].upper():
|
||||
raise AuthError('Invalid Hash')
|
||||
return True
|
||||
|
||||
def validate(self):
|
||||
"""Verify required headers"""
|
||||
for header in self._requiredHeaders:
|
||||
if not self.headers.get(header, False):
|
||||
raise ParseError('Missing Notification Header: ' + header)
|
||||
|
||||
def _format_info(self):
|
||||
"""Generate info line for GNTP Message
|
||||
|
||||
:return string:
|
||||
"""
|
||||
info = u'GNTP/%s %s' % (
|
||||
self.info.get('version'),
|
||||
self.info.get('messagetype'),
|
||||
)
|
||||
if self.info.get('encryptionAlgorithmID', None):
|
||||
info += ' %s:%s' % (
|
||||
self.info.get('encryptionAlgorithmID'),
|
||||
self.info.get('ivValue'),
|
||||
)
|
||||
else:
|
||||
info += ' NONE'
|
||||
|
||||
if self.info.get('keyHashAlgorithmID', None):
|
||||
info += ' %s:%s.%s' % (
|
||||
self.info.get('keyHashAlgorithmID'),
|
||||
self.info.get('keyHash'),
|
||||
self.info.get('salt')
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
def _parse_dict(self, data):
|
||||
"""Helper function to parse blocks of GNTP headers into a dictionary
|
||||
|
||||
:param string data:
|
||||
:return dict:
|
||||
"""
|
||||
dict = {}
|
||||
for line in data.split('\r\n'):
|
||||
match = GNTP_HEADER.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
key = unicode(match.group(1).strip(), 'utf8', 'replace')
|
||||
val = unicode(match.group(2).strip(), 'utf8', 'replace')
|
||||
dict[key] = val
|
||||
return dict
|
||||
|
||||
def add_header(self, key, value):
|
||||
if isinstance(value, unicode):
|
||||
self.headers[key] = value
|
||||
else:
|
||||
self.headers[key] = unicode('%s' % value, 'utf8', 'replace')
|
||||
|
||||
def add_resource(self, data):
|
||||
"""Add binary resource
|
||||
|
||||
:param string data: Binary Data
|
||||
"""
|
||||
identifier = hashlib.md5(data).hexdigest()
|
||||
self.resources[identifier] = data
|
||||
return 'x-growl-resource://%s' % identifier
|
||||
|
||||
def decode(self, data, password=None):
|
||||
"""Decode GNTP Message
|
||||
|
||||
:param string data:
|
||||
"""
|
||||
self.password = password
|
||||
self.raw = data
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(data)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
def encode(self):
|
||||
"""Encode a generic GNTP Message
|
||||
|
||||
:return string: GNTP Message ready to be sent
|
||||
"""
|
||||
|
||||
buffer = _GNTPBuffer()
|
||||
|
||||
buffer.writefmt(self._format_info())
|
||||
|
||||
#Headers
|
||||
for k, v in self.headers.iteritems():
|
||||
buffer.writefmt('%s: %s', k, v)
|
||||
buffer.writefmt()
|
||||
|
||||
#Resources
|
||||
for resource, data in self.resources.iteritems():
|
||||
buffer.writefmt('Identifier: %s', resource)
|
||||
buffer.writefmt('Length: %d', len(data))
|
||||
buffer.writefmt()
|
||||
buffer.write(data)
|
||||
buffer.writefmt()
|
||||
buffer.writefmt()
|
||||
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
class GNTPRegister(_GNTPBase):
|
||||
"""Represents a GNTP Registration Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Application-Name',
|
||||
'Notifications-Count'
|
||||
]
|
||||
_requiredNotificationHeaders = ['Notification-Name']
|
||||
|
||||
def __init__(self, data=None, password=None):
|
||||
_GNTPBase.__init__(self, 'REGISTER')
|
||||
self.notifications = []
|
||||
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
self.add_header('Application-Name', 'pygntp')
|
||||
self.add_header('Notifications-Count', 0)
|
||||
|
||||
def validate(self):
|
||||
'''Validate required headers and validate notification headers'''
|
||||
for header in self._requiredHeaders:
|
||||
if not self.headers.get(header, False):
|
||||
raise ParseError('Missing Registration Header: ' + header)
|
||||
for notice in self.notifications:
|
||||
for header in self._requiredNotificationHeaders:
|
||||
if not notice.get(header, False):
|
||||
raise ParseError('Missing Notification Header: ' + header)
|
||||
|
||||
def decode(self, data, password):
|
||||
"""Decode existing GNTP Registration message
|
||||
|
||||
:param string data: Message to decode
|
||||
"""
|
||||
self.raw = data
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(data)
|
||||
self._validate_password(password)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if i == 0:
|
||||
continue # Skip Header
|
||||
if part.strip() == '':
|
||||
continue
|
||||
notice = self._parse_dict(part)
|
||||
if notice.get('Notification-Name', False):
|
||||
self.notifications.append(notice)
|
||||
elif notice.get('Identifier', False):
|
||||
notice['Data'] = self._decode_binary(part, notice)
|
||||
#open('register.png','wblol').write(notice['Data'])
|
||||
self.resources[notice.get('Identifier')] = notice
|
||||
|
||||
def add_notification(self, name, enabled=True):
|
||||
"""Add new Notification to Registration message
|
||||
|
||||
:param string name: Notification Name
|
||||
:param boolean enabled: Enable this notification by default
|
||||
"""
|
||||
notice = {}
|
||||
notice['Notification-Name'] = u'%s' % name
|
||||
notice['Notification-Enabled'] = u'%s' % enabled
|
||||
|
||||
self.notifications.append(notice)
|
||||
self.add_header('Notifications-Count', len(self.notifications))
|
||||
|
||||
def encode(self):
|
||||
"""Encode a GNTP Registration Message
|
||||
|
||||
:return string: Encoded GNTP Registration message
|
||||
"""
|
||||
|
||||
buffer = _GNTPBuffer()
|
||||
|
||||
buffer.writefmt(self._format_info())
|
||||
|
||||
#Headers
|
||||
for k, v in self.headers.iteritems():
|
||||
buffer.writefmt('%s: %s', k, v)
|
||||
buffer.writefmt()
|
||||
|
||||
#Notifications
|
||||
if len(self.notifications) > 0:
|
||||
for notice in self.notifications:
|
||||
for k, v in notice.iteritems():
|
||||
buffer.writefmt('%s: %s', k, v)
|
||||
buffer.writefmt()
|
||||
|
||||
#Resources
|
||||
for resource, data in self.resources.iteritems():
|
||||
buffer.writefmt('Identifier: %s', resource)
|
||||
buffer.writefmt('Length: %d', len(data))
|
||||
buffer.writefmt()
|
||||
buffer.write(data)
|
||||
buffer.writefmt()
|
||||
buffer.writefmt()
|
||||
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
class GNTPNotice(_GNTPBase):
|
||||
"""Represents a GNTP Notification Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string app: (Optional) Set Application-Name
|
||||
:param string name: (Optional) Set Notification-Name
|
||||
:param string title: (Optional) Set Notification Title
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Application-Name',
|
||||
'Notification-Name',
|
||||
'Notification-Title'
|
||||
]
|
||||
|
||||
def __init__(self, data=None, app=None, name=None, title=None, password=None):
|
||||
_GNTPBase.__init__(self, 'NOTIFY')
|
||||
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
if app:
|
||||
self.add_header('Application-Name', app)
|
||||
if name:
|
||||
self.add_header('Notification-Name', name)
|
||||
if title:
|
||||
self.add_header('Notification-Title', title)
|
||||
|
||||
def decode(self, data, password):
|
||||
"""Decode existing GNTP Notification message
|
||||
|
||||
:param string data: Message to decode.
|
||||
"""
|
||||
self.raw = data
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(data)
|
||||
self._validate_password(password)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if i == 0:
|
||||
continue # Skip Header
|
||||
if part.strip() == '':
|
||||
continue
|
||||
notice = self._parse_dict(part)
|
||||
if notice.get('Identifier', False):
|
||||
notice['Data'] = self._decode_binary(part, notice)
|
||||
#open('notice.png','wblol').write(notice['Data'])
|
||||
self.resources[notice.get('Identifier')] = notice
|
||||
|
||||
|
||||
class GNTPSubscribe(_GNTPBase):
|
||||
"""Represents a GNTP Subscribe Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Subscriber-ID',
|
||||
'Subscriber-Name',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, password=None):
|
||||
_GNTPBase.__init__(self, 'SUBSCRIBE')
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
|
||||
|
||||
class GNTPOK(_GNTPBase):
|
||||
"""Represents a GNTP OK Response
|
||||
|
||||
:param string data: (Optional) See _GNTPResponse.decode()
|
||||
:param string action: (Optional) Set type of action the OK Response is for
|
||||
"""
|
||||
_requiredHeaders = ['Response-Action']
|
||||
|
||||
def __init__(self, data=None, action=None):
|
||||
_GNTPBase.__init__(self, '-OK')
|
||||
if data:
|
||||
self.decode(data)
|
||||
if action:
|
||||
self.add_header('Response-Action', action)
|
||||
|
||||
|
||||
class GNTPError(_GNTPBase):
|
||||
"""Represents a GNTP Error response
|
||||
|
||||
:param string data: (Optional) See _GNTPResponse.decode()
|
||||
:param string errorcode: (Optional) Error code
|
||||
:param string errordesc: (Optional) Error Description
|
||||
"""
|
||||
_requiredHeaders = ['Error-Code', 'Error-Description']
|
||||
|
||||
def __init__(self, data=None, errorcode=None, errordesc=None):
|
||||
_GNTPBase.__init__(self, '-ERROR')
|
||||
if data:
|
||||
self.decode(data)
|
||||
if errorcode:
|
||||
self.add_header('Error-Code', errorcode)
|
||||
self.add_header('Error-Description', errordesc)
|
||||
|
||||
def error(self):
|
||||
return (self.headers.get('Error-Code', None),
|
||||
self.headers.get('Error-Description', None))
|
||||
|
||||
|
||||
def parse_gntp(data, password=None):
|
||||
"""Attempt to parse a message as a GNTP message
|
||||
|
||||
:param string data: Message to be parsed
|
||||
:param string password: Optional password to be used to verify the message
|
||||
"""
|
||||
match = GNTP_INFO_LINE_SHORT.match(data)
|
||||
if not match:
|
||||
raise ParseError('INVALID_GNTP_INFO')
|
||||
info = match.groupdict()
|
||||
if info['messagetype'] == 'REGISTER':
|
||||
return GNTPRegister(data, password=password)
|
||||
elif info['messagetype'] == 'NOTIFY':
|
||||
return GNTPNotice(data, password=password)
|
||||
elif info['messagetype'] == 'SUBSCRIBE':
|
||||
return GNTPSubscribe(data, password=password)
|
||||
elif info['messagetype'] == '-OK':
|
||||
return GNTPOK(data)
|
||||
elif info['messagetype'] == '-ERROR':
|
||||
return GNTPError(data)
|
||||
raise ParseError('INVALID_GNTP_MESSAGE')
|
||||
|
||||
141
gntp/cli.py
Normal file
141
gntp/cli.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from optparse import OptionParser, OptionGroup
|
||||
|
||||
from gntp.notifier import GrowlNotifier
|
||||
from gntp.shim import RawConfigParser
|
||||
from gntp.version import __version__
|
||||
|
||||
DEFAULT_CONFIG = os.path.expanduser('~/.gntp')
|
||||
|
||||
config = RawConfigParser({
|
||||
'hostname': 'localhost',
|
||||
'password': None,
|
||||
'port': 23053,
|
||||
})
|
||||
config.read([DEFAULT_CONFIG])
|
||||
if not config.has_section('gntp'):
|
||||
config.add_section('gntp')
|
||||
|
||||
|
||||
class ClientParser(OptionParser):
|
||||
def __init__(self):
|
||||
OptionParser.__init__(self, version="%%prog %s" % __version__)
|
||||
|
||||
group = OptionGroup(self, "Network Options")
|
||||
group.add_option("-H", "--host",
|
||||
dest="host", default=config.get('gntp', 'hostname'),
|
||||
help="Specify a hostname to which to send a remote notification. [%default]")
|
||||
group.add_option("--port",
|
||||
dest="port", default=config.getint('gntp', 'port'), type="int",
|
||||
help="port to listen on [%default]")
|
||||
group.add_option("-P", "--password",
|
||||
dest='password', default=config.get('gntp', 'password'),
|
||||
help="Network password")
|
||||
self.add_option_group(group)
|
||||
|
||||
group = OptionGroup(self, "Notification Options")
|
||||
group.add_option("-n", "--name",
|
||||
dest="app", default='Python GNTP Test Client',
|
||||
help="Set the name of the application [%default]")
|
||||
group.add_option("-s", "--sticky",
|
||||
dest='sticky', default=False, action="store_true",
|
||||
help="Make the notification sticky [%default]")
|
||||
group.add_option("--image",
|
||||
dest="icon", default=None,
|
||||
help="Icon for notification (URL or /path/to/file)")
|
||||
group.add_option("-m", "--message",
|
||||
dest="message", default=None,
|
||||
help="Sets the message instead of using stdin")
|
||||
group.add_option("-p", "--priority",
|
||||
dest="priority", default=0, type="int",
|
||||
help="-2 to 2 [%default]")
|
||||
group.add_option("-d", "--identifier",
|
||||
dest="identifier",
|
||||
help="Identifier for coalescing")
|
||||
group.add_option("-t", "--title",
|
||||
dest="title", default=None,
|
||||
help="Set the title of the notification [%default]")
|
||||
group.add_option("-N", "--notification",
|
||||
dest="name", default='Notification',
|
||||
help="Set the notification name [%default]")
|
||||
group.add_option("--callback",
|
||||
dest="callback",
|
||||
help="URL callback")
|
||||
self.add_option_group(group)
|
||||
|
||||
# Extra Options
|
||||
self.add_option('-v', '--verbose',
|
||||
dest='verbose', default=0, action='count',
|
||||
help="Verbosity levels")
|
||||
|
||||
def parse_args(self, args=None, values=None):
|
||||
values, args = OptionParser.parse_args(self, args, values)
|
||||
|
||||
if values.message is None:
|
||||
print('Enter a message followed by Ctrl-D')
|
||||
try:
|
||||
message = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
exit()
|
||||
else:
|
||||
message = values.message
|
||||
|
||||
if values.title is None:
|
||||
values.title = ' '.join(args)
|
||||
|
||||
# If we still have an empty title, use the
|
||||
# first bit of the message as the title
|
||||
if values.title == '':
|
||||
values.title = message[:20]
|
||||
|
||||
values.verbose = logging.WARNING - values.verbose * 10
|
||||
|
||||
return values, message
|
||||
|
||||
|
||||
def main():
|
||||
(options, message) = ClientParser().parse_args()
|
||||
logging.basicConfig(level=options.verbose)
|
||||
if not os.path.exists(DEFAULT_CONFIG):
|
||||
logging.info('No config read found at %s', DEFAULT_CONFIG)
|
||||
|
||||
growl = GrowlNotifier(
|
||||
applicationName=options.app,
|
||||
notifications=[options.name],
|
||||
defaultNotifications=[options.name],
|
||||
hostname=options.host,
|
||||
password=options.password,
|
||||
port=options.port,
|
||||
)
|
||||
result = growl.register()
|
||||
if result is not True:
|
||||
exit(result)
|
||||
|
||||
# This would likely be better placed within the growl notifier
|
||||
# class but until I make _checkIcon smarter this is "easier"
|
||||
if options.icon and growl._checkIcon(options.icon) is False:
|
||||
logging.info('Loading image %s', options.icon)
|
||||
f = open(options.icon, 'rb')
|
||||
options.icon = f.read()
|
||||
f.close()
|
||||
|
||||
result = growl.notify(
|
||||
noteType=options.name,
|
||||
title=options.title,
|
||||
description=message,
|
||||
icon=options.icon,
|
||||
sticky=options.sticky,
|
||||
priority=options.priority,
|
||||
callback=options.callback,
|
||||
identifier=options.identifier,
|
||||
)
|
||||
if result is not True:
|
||||
exit(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
77
gntp/config.py
Normal file
77
gntp/config.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
"""
|
||||
The gntp.config module is provided as an extended GrowlNotifier object that takes
|
||||
advantage of the ConfigParser module to allow us to setup some default values
|
||||
(such as hostname, password, and port) in a more global way to be shared among
|
||||
programs using gntp
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import gntp.notifier
|
||||
import gntp.shim
|
||||
|
||||
__all__ = [
|
||||
'mini',
|
||||
'GrowlNotifier'
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GrowlNotifier(gntp.notifier.GrowlNotifier):
|
||||
"""
|
||||
ConfigParser enhanced GrowlNotifier object
|
||||
|
||||
For right now, we are only interested in letting users overide certain
|
||||
values from ~/.gntp
|
||||
|
||||
::
|
||||
|
||||
[gntp]
|
||||
hostname = ?
|
||||
password = ?
|
||||
port = ?
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
config = gntp.shim.RawConfigParser({
|
||||
'hostname': kwargs.get('hostname', 'localhost'),
|
||||
'password': kwargs.get('password'),
|
||||
'port': kwargs.get('port', 23053),
|
||||
})
|
||||
|
||||
config.read([os.path.expanduser('~/.gntp')])
|
||||
|
||||
# If the file does not exist, then there will be no gntp section defined
|
||||
# and the config.get() lines below will get confused. Since we are not
|
||||
# saving the config, it should be safe to just add it here so the
|
||||
# code below doesn't complain
|
||||
if not config.has_section('gntp'):
|
||||
logger.info('Error reading ~/.gntp config file')
|
||||
config.add_section('gntp')
|
||||
|
||||
kwargs['password'] = config.get('gntp', 'password')
|
||||
kwargs['hostname'] = config.get('gntp', 'hostname')
|
||||
kwargs['port'] = config.getint('gntp', 'port')
|
||||
|
||||
super(GrowlNotifier, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def mini(description, **kwargs):
|
||||
"""Single notification function
|
||||
|
||||
Simple notification function in one line. Has only one required parameter
|
||||
and attempts to use reasonable defaults for everything else
|
||||
:param string description: Notification message
|
||||
"""
|
||||
kwargs['notifierFactory'] = GrowlNotifier
|
||||
gntp.notifier.mini(description, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# If we're running this module directly we're likely running it as a test
|
||||
# so extra debugging is useful
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
mini('Testing mini notification')
|
||||
518
gntp/core.py
Normal file
518
gntp/core.py
Normal file
@@ -0,0 +1,518 @@
|
||||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import time
|
||||
|
||||
import gntp.shim
|
||||
import gntp.errors as errors
|
||||
|
||||
__all__ = [
|
||||
'GNTPRegister',
|
||||
'GNTPNotice',
|
||||
'GNTPSubscribe',
|
||||
'GNTPOK',
|
||||
'GNTPError',
|
||||
'parse_gntp',
|
||||
]
|
||||
|
||||
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
|
||||
GNTP_INFO_LINE = re.compile(
|
||||
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
|
||||
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
|
||||
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
GNTP_INFO_LINE_SHORT = re.compile(
|
||||
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
GNTP_HEADER = re.compile('([\w-]+):(.+)')
|
||||
|
||||
GNTP_EOL = gntp.shim.b('\r\n')
|
||||
GNTP_SEP = gntp.shim.b(': ')
|
||||
|
||||
|
||||
class _GNTPBuffer(gntp.shim.StringIO):
|
||||
"""GNTP Buffer class"""
|
||||
def writeln(self, value=None):
|
||||
if value:
|
||||
self.write(gntp.shim.b(value))
|
||||
self.write(GNTP_EOL)
|
||||
|
||||
def writeheader(self, key, value):
|
||||
if not isinstance(value, str):
|
||||
value = str(value)
|
||||
self.write(gntp.shim.b(key))
|
||||
self.write(GNTP_SEP)
|
||||
self.write(gntp.shim.b(value))
|
||||
self.write(GNTP_EOL)
|
||||
|
||||
|
||||
class _GNTPBase(object):
|
||||
"""Base initilization
|
||||
|
||||
:param string messagetype: GNTP Message type
|
||||
:param string version: GNTP Protocol version
|
||||
:param string encription: Encryption protocol
|
||||
"""
|
||||
def __init__(self, messagetype=None, version='1.0', encryption=None):
|
||||
self.info = {
|
||||
'version': version,
|
||||
'messagetype': messagetype,
|
||||
'encryptionAlgorithmID': encryption
|
||||
}
|
||||
self.hash_algo = {
|
||||
'MD5': hashlib.md5,
|
||||
'SHA1': hashlib.sha1,
|
||||
'SHA256': hashlib.sha256,
|
||||
'SHA512': hashlib.sha512,
|
||||
}
|
||||
self.headers = {}
|
||||
self.resources = {}
|
||||
|
||||
# For Python2 we can just return the bytes as is without worry
|
||||
# but on Python3 we want to make sure we return the packet as
|
||||
# a unicode string so that things like logging won't get confused
|
||||
if gntp.shim.PY2:
|
||||
def __str__(self):
|
||||
return self.encode()
|
||||
else:
|
||||
def __str__(self):
|
||||
return gntp.shim.u(self.encode())
|
||||
|
||||
def _parse_info(self, data):
|
||||
"""Parse the first line of a GNTP message to get security and other info values
|
||||
|
||||
:param string data: GNTP Message
|
||||
:return dict: Parsed GNTP Info line
|
||||
"""
|
||||
|
||||
match = GNTP_INFO_LINE.match(data)
|
||||
|
||||
if not match:
|
||||
raise errors.ParseError('ERROR_PARSING_INFO_LINE')
|
||||
|
||||
info = match.groupdict()
|
||||
if info['encryptionAlgorithmID'] == 'NONE':
|
||||
info['encryptionAlgorithmID'] = None
|
||||
|
||||
return info
|
||||
|
||||
def set_password(self, password, encryptAlgo='MD5'):
|
||||
"""Set a password for a GNTP Message
|
||||
|
||||
:param string password: Null to clear password
|
||||
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
|
||||
"""
|
||||
if not password:
|
||||
self.info['encryptionAlgorithmID'] = None
|
||||
self.info['keyHashAlgorithm'] = None
|
||||
return
|
||||
|
||||
self.password = gntp.shim.b(password)
|
||||
self.encryptAlgo = encryptAlgo.upper()
|
||||
|
||||
if not self.encryptAlgo in self.hash_algo:
|
||||
raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
|
||||
|
||||
hashfunction = self.hash_algo.get(self.encryptAlgo)
|
||||
|
||||
password = password.encode('utf8')
|
||||
seed = time.ctime().encode('utf8')
|
||||
salt = hashfunction(seed).hexdigest()
|
||||
saltHash = hashfunction(seed).digest()
|
||||
keyBasis = password + saltHash
|
||||
key = hashfunction(keyBasis).digest()
|
||||
keyHash = hashfunction(key).hexdigest()
|
||||
|
||||
self.info['keyHashAlgorithmID'] = self.encryptAlgo
|
||||
self.info['keyHash'] = keyHash.upper()
|
||||
self.info['salt'] = salt.upper()
|
||||
|
||||
def _decode_hex(self, value):
|
||||
"""Helper function to decode hex string to `proper` hex string
|
||||
|
||||
:param string value: Human readable hex string
|
||||
:return string: Hex string
|
||||
"""
|
||||
result = ''
|
||||
for i in range(0, len(value), 2):
|
||||
tmp = int(value[i:i + 2], 16)
|
||||
result += chr(tmp)
|
||||
return result
|
||||
|
||||
def _decode_binary(self, rawIdentifier, identifier):
|
||||
rawIdentifier += '\r\n\r\n'
|
||||
dataLength = int(identifier['Length'])
|
||||
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
|
||||
pointerEnd = pointerStart + dataLength
|
||||
data = self.raw[pointerStart:pointerEnd]
|
||||
if not len(data) == dataLength:
|
||||
raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
|
||||
return data
|
||||
|
||||
def _validate_password(self, password):
|
||||
"""Validate GNTP Message against stored password"""
|
||||
self.password = password
|
||||
if password is None:
|
||||
raise errors.AuthError('Missing password')
|
||||
keyHash = self.info.get('keyHash', None)
|
||||
if keyHash is None and self.password is None:
|
||||
return True
|
||||
if keyHash is None:
|
||||
raise errors.AuthError('Invalid keyHash')
|
||||
if self.password is None:
|
||||
raise errors.AuthError('Missing password')
|
||||
|
||||
keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5')
|
||||
|
||||
password = self.password.encode('utf8')
|
||||
saltHash = self._decode_hex(self.info['salt'])
|
||||
|
||||
keyBasis = password + saltHash
|
||||
self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest()
|
||||
keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest()
|
||||
|
||||
if not keyHash.upper() == self.info['keyHash'].upper():
|
||||
raise errors.AuthError('Invalid Hash')
|
||||
return True
|
||||
|
||||
def validate(self):
|
||||
"""Verify required headers"""
|
||||
for header in self._requiredHeaders:
|
||||
if not self.headers.get(header, False):
|
||||
raise errors.ParseError('Missing Notification Header: ' + header)
|
||||
|
||||
def _format_info(self):
|
||||
"""Generate info line for GNTP Message
|
||||
|
||||
:return string:
|
||||
"""
|
||||
info = 'GNTP/%s %s' % (
|
||||
self.info.get('version'),
|
||||
self.info.get('messagetype'),
|
||||
)
|
||||
if self.info.get('encryptionAlgorithmID', None):
|
||||
info += ' %s:%s' % (
|
||||
self.info.get('encryptionAlgorithmID'),
|
||||
self.info.get('ivValue'),
|
||||
)
|
||||
else:
|
||||
info += ' NONE'
|
||||
|
||||
if self.info.get('keyHashAlgorithmID', None):
|
||||
info += ' %s:%s.%s' % (
|
||||
self.info.get('keyHashAlgorithmID'),
|
||||
self.info.get('keyHash'),
|
||||
self.info.get('salt')
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
def _parse_dict(self, data):
|
||||
"""Helper function to parse blocks of GNTP headers into a dictionary
|
||||
|
||||
:param string data:
|
||||
:return dict: Dictionary of parsed GNTP Headers
|
||||
"""
|
||||
d = {}
|
||||
for line in data.split('\r\n'):
|
||||
match = GNTP_HEADER.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
key = match.group(1).strip()
|
||||
val = match.group(2).strip()
|
||||
d[key] = val
|
||||
return d
|
||||
|
||||
def add_header(self, key, value):
|
||||
self.headers[key] = value
|
||||
|
||||
def add_resource(self, data):
|
||||
"""Add binary resource
|
||||
|
||||
:param string data: Binary Data
|
||||
"""
|
||||
data = gntp.shim.b(data)
|
||||
identifier = hashlib.md5(data).hexdigest()
|
||||
self.resources[identifier] = data
|
||||
return 'x-growl-resource://%s' % identifier
|
||||
|
||||
def decode(self, data, password=None):
|
||||
"""Decode GNTP Message
|
||||
|
||||
:param string data:
|
||||
"""
|
||||
self.password = password
|
||||
self.raw = gntp.shim.u(data)
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(self.raw)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
def encode(self):
|
||||
"""Encode a generic GNTP Message
|
||||
|
||||
:return string: GNTP Message ready to be sent. Returned as a byte string
|
||||
"""
|
||||
|
||||
buff = _GNTPBuffer()
|
||||
|
||||
buff.writeln(self._format_info())
|
||||
|
||||
#Headers
|
||||
for k, v in self.headers.items():
|
||||
buff.writeheader(k, v)
|
||||
buff.writeln()
|
||||
|
||||
#Resources
|
||||
for resource, data in self.resources.items():
|
||||
buff.writeheader('Identifier', resource)
|
||||
buff.writeheader('Length', len(data))
|
||||
buff.writeln()
|
||||
buff.write(data)
|
||||
buff.writeln()
|
||||
buff.writeln()
|
||||
|
||||
return buff.getvalue()
|
||||
|
||||
|
||||
class GNTPRegister(_GNTPBase):
|
||||
"""Represents a GNTP Registration Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Application-Name',
|
||||
'Notifications-Count'
|
||||
]
|
||||
_requiredNotificationHeaders = ['Notification-Name']
|
||||
|
||||
def __init__(self, data=None, password=None):
|
||||
_GNTPBase.__init__(self, 'REGISTER')
|
||||
self.notifications = []
|
||||
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
self.add_header('Application-Name', 'pygntp')
|
||||
self.add_header('Notifications-Count', 0)
|
||||
|
||||
def validate(self):
|
||||
'''Validate required headers and validate notification headers'''
|
||||
for header in self._requiredHeaders:
|
||||
if not self.headers.get(header, False):
|
||||
raise errors.ParseError('Missing Registration Header: ' + header)
|
||||
for notice in self.notifications:
|
||||
for header in self._requiredNotificationHeaders:
|
||||
if not notice.get(header, False):
|
||||
raise errors.ParseError('Missing Notification Header: ' + header)
|
||||
|
||||
def decode(self, data, password):
|
||||
"""Decode existing GNTP Registration message
|
||||
|
||||
:param string data: Message to decode
|
||||
"""
|
||||
self.raw = gntp.shim.u(data)
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(self.raw)
|
||||
self._validate_password(password)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if i == 0:
|
||||
continue # Skip Header
|
||||
if part.strip() == '':
|
||||
continue
|
||||
notice = self._parse_dict(part)
|
||||
if notice.get('Notification-Name', False):
|
||||
self.notifications.append(notice)
|
||||
elif notice.get('Identifier', False):
|
||||
notice['Data'] = self._decode_binary(part, notice)
|
||||
#open('register.png','wblol').write(notice['Data'])
|
||||
self.resources[notice.get('Identifier')] = notice
|
||||
|
||||
def add_notification(self, name, enabled=True):
|
||||
"""Add new Notification to Registration message
|
||||
|
||||
:param string name: Notification Name
|
||||
:param boolean enabled: Enable this notification by default
|
||||
"""
|
||||
notice = {}
|
||||
notice['Notification-Name'] = name
|
||||
notice['Notification-Enabled'] = enabled
|
||||
|
||||
self.notifications.append(notice)
|
||||
self.add_header('Notifications-Count', len(self.notifications))
|
||||
|
||||
def encode(self):
|
||||
"""Encode a GNTP Registration Message
|
||||
|
||||
:return string: Encoded GNTP Registration message. Returned as a byte string
|
||||
"""
|
||||
|
||||
buff = _GNTPBuffer()
|
||||
|
||||
buff.writeln(self._format_info())
|
||||
|
||||
#Headers
|
||||
for k, v in self.headers.items():
|
||||
buff.writeheader(k, v)
|
||||
buff.writeln()
|
||||
|
||||
#Notifications
|
||||
if len(self.notifications) > 0:
|
||||
for notice in self.notifications:
|
||||
for k, v in notice.items():
|
||||
buff.writeheader(k, v)
|
||||
buff.writeln()
|
||||
|
||||
#Resources
|
||||
for resource, data in self.resources.items():
|
||||
buff.writeheader('Identifier', resource)
|
||||
buff.writeheader('Length', len(data))
|
||||
buff.writeln()
|
||||
buff.write(data)
|
||||
buff.writeln()
|
||||
buff.writeln()
|
||||
|
||||
return buff.getvalue()
|
||||
|
||||
|
||||
class GNTPNotice(_GNTPBase):
|
||||
"""Represents a GNTP Notification Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string app: (Optional) Set Application-Name
|
||||
:param string name: (Optional) Set Notification-Name
|
||||
:param string title: (Optional) Set Notification Title
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Application-Name',
|
||||
'Notification-Name',
|
||||
'Notification-Title'
|
||||
]
|
||||
|
||||
def __init__(self, data=None, app=None, name=None, title=None, password=None):
|
||||
_GNTPBase.__init__(self, 'NOTIFY')
|
||||
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
if app:
|
||||
self.add_header('Application-Name', app)
|
||||
if name:
|
||||
self.add_header('Notification-Name', name)
|
||||
if title:
|
||||
self.add_header('Notification-Title', title)
|
||||
|
||||
def decode(self, data, password):
|
||||
"""Decode existing GNTP Notification message
|
||||
|
||||
:param string data: Message to decode.
|
||||
"""
|
||||
self.raw = gntp.shim.u(data)
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(self.raw)
|
||||
self._validate_password(password)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if i == 0:
|
||||
continue # Skip Header
|
||||
if part.strip() == '':
|
||||
continue
|
||||
notice = self._parse_dict(part)
|
||||
if notice.get('Identifier', False):
|
||||
notice['Data'] = self._decode_binary(part, notice)
|
||||
#open('notice.png','wblol').write(notice['Data'])
|
||||
self.resources[notice.get('Identifier')] = notice
|
||||
|
||||
|
||||
class GNTPSubscribe(_GNTPBase):
|
||||
"""Represents a GNTP Subscribe Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Subscriber-ID',
|
||||
'Subscriber-Name',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, password=None):
|
||||
_GNTPBase.__init__(self, 'SUBSCRIBE')
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
|
||||
|
||||
class GNTPOK(_GNTPBase):
|
||||
"""Represents a GNTP OK Response
|
||||
|
||||
:param string data: (Optional) See _GNTPResponse.decode()
|
||||
:param string action: (Optional) Set type of action the OK Response is for
|
||||
"""
|
||||
_requiredHeaders = ['Response-Action']
|
||||
|
||||
def __init__(self, data=None, action=None):
|
||||
_GNTPBase.__init__(self, '-OK')
|
||||
if data:
|
||||
self.decode(data)
|
||||
if action:
|
||||
self.add_header('Response-Action', action)
|
||||
|
||||
|
||||
class GNTPError(_GNTPBase):
|
||||
"""Represents a GNTP Error response
|
||||
|
||||
:param string data: (Optional) See _GNTPResponse.decode()
|
||||
:param string errorcode: (Optional) Error code
|
||||
:param string errordesc: (Optional) Error Description
|
||||
"""
|
||||
_requiredHeaders = ['Error-Code', 'Error-Description']
|
||||
|
||||
def __init__(self, data=None, errorcode=None, errordesc=None):
|
||||
_GNTPBase.__init__(self, '-ERROR')
|
||||
if data:
|
||||
self.decode(data)
|
||||
if errorcode:
|
||||
self.add_header('Error-Code', errorcode)
|
||||
self.add_header('Error-Description', errordesc)
|
||||
|
||||
def error(self):
|
||||
return (self.headers.get('Error-Code', None),
|
||||
self.headers.get('Error-Description', None))
|
||||
|
||||
|
||||
def parse_gntp(data, password=None):
|
||||
"""Attempt to parse a message as a GNTP message
|
||||
|
||||
:param string data: Message to be parsed
|
||||
:param string password: Optional password to be used to verify the message
|
||||
"""
|
||||
data = gntp.shim.u(data)
|
||||
match = GNTP_INFO_LINE_SHORT.match(data)
|
||||
if not match:
|
||||
raise errors.ParseError('INVALID_GNTP_INFO')
|
||||
info = match.groupdict()
|
||||
if info['messagetype'] == 'REGISTER':
|
||||
return GNTPRegister(data, password=password)
|
||||
elif info['messagetype'] == 'NOTIFY':
|
||||
return GNTPNotice(data, password=password)
|
||||
elif info['messagetype'] == 'SUBSCRIBE':
|
||||
return GNTPSubscribe(data, password=password)
|
||||
elif info['messagetype'] == '-OK':
|
||||
return GNTPOK(data)
|
||||
elif info['messagetype'] == '-ERROR':
|
||||
return GNTPError(data)
|
||||
raise errors.ParseError('INVALID_GNTP_MESSAGE')
|
||||
25
gntp/errors.py
Normal file
25
gntp/errors.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
class BaseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ParseError(BaseError):
|
||||
errorcode = 500
|
||||
errordesc = 'Error parsing the message'
|
||||
|
||||
|
||||
class AuthError(BaseError):
|
||||
errorcode = 400
|
||||
errordesc = 'Error with authorization'
|
||||
|
||||
|
||||
class UnsupportedError(BaseError):
|
||||
errorcode = 500
|
||||
errordesc = 'Currently unsupported by gntp.py'
|
||||
|
||||
|
||||
class NetworkError(BaseError):
|
||||
errorcode = 500
|
||||
errordesc = "Error connecting to growl server"
|
||||
161
gntp/notifier.py
161
gntp/notifier.py
@@ -1,3 +1,6 @@
|
||||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
"""
|
||||
The gntp.notifier module is provided as a simple way to send notifications
|
||||
using GNTP
|
||||
@@ -9,10 +12,15 @@ using GNTP
|
||||
`Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
|
||||
|
||||
"""
|
||||
import gntp
|
||||
import socket
|
||||
import logging
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from gntp.version import __version__
|
||||
import gntp.core
|
||||
import gntp.errors as errors
|
||||
import gntp.shim
|
||||
|
||||
__all__ = [
|
||||
'mini',
|
||||
@@ -22,45 +30,6 @@ __all__ = [
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def mini(description, applicationName='PythonMini', noteType="Message",
|
||||
title="Mini Message", applicationIcon=None, hostname='localhost',
|
||||
password=None, port=23053, sticky=False, priority=None,
|
||||
callback=None, notificationIcon=None, identifier=None):
|
||||
"""Single notification function
|
||||
|
||||
Simple notification function in one line. Has only one required parameter
|
||||
and attempts to use reasonable defaults for everything else
|
||||
:param string description: Notification message
|
||||
|
||||
.. warning::
|
||||
For now, only URL callbacks are supported. In the future, the
|
||||
callback argument will also support a function
|
||||
"""
|
||||
growl = GrowlNotifier(
|
||||
applicationName=applicationName,
|
||||
notifications=[noteType],
|
||||
defaultNotifications=[noteType],
|
||||
applicationIcon=applicationIcon,
|
||||
hostname=hostname,
|
||||
password=password,
|
||||
port=port,
|
||||
)
|
||||
result = growl.register()
|
||||
if result is not True:
|
||||
return result
|
||||
|
||||
return growl.notify(
|
||||
noteType=noteType,
|
||||
title=title,
|
||||
description=description,
|
||||
icon=notificationIcon,
|
||||
sticky=sticky,
|
||||
priority=priority,
|
||||
callback=callback,
|
||||
identifier=identifier,
|
||||
)
|
||||
|
||||
|
||||
class GrowlNotifier(object):
|
||||
"""Helper class to simplfy sending Growl messages
|
||||
|
||||
@@ -99,8 +68,9 @@ class GrowlNotifier(object):
|
||||
If it's a simple URL icon, then we return True. If it's a data icon
|
||||
then we return False
|
||||
'''
|
||||
logger.debug('Checking icon')
|
||||
return data.startswith('http')
|
||||
logger.info('Checking icon')
|
||||
|
||||
return gntp.shim.u(data)[:4] in ['http', 'file']
|
||||
|
||||
def register(self):
|
||||
"""Send GNTP Registration
|
||||
@@ -109,8 +79,8 @@ class GrowlNotifier(object):
|
||||
Before sending notifications to Growl, you need to have
|
||||
sent a registration message at least once
|
||||
"""
|
||||
logger.debug('Sending registration to %s:%s', self.hostname, self.port)
|
||||
register = gntp.GNTPRegister()
|
||||
logger.info('Sending registration to %s:%s', self.hostname, self.port)
|
||||
register = gntp.core.GNTPRegister()
|
||||
register.add_header('Application-Name', self.applicationName)
|
||||
for notification in self.notifications:
|
||||
enabled = notification in self.defaultNotifications
|
||||
@@ -119,8 +89,8 @@ class GrowlNotifier(object):
|
||||
if self._checkIcon(self.applicationIcon):
|
||||
register.add_header('Application-Icon', self.applicationIcon)
|
||||
else:
|
||||
id = register.add_resource(self.applicationIcon)
|
||||
register.add_header('Application-Icon', id)
|
||||
resource = register.add_resource(self.applicationIcon)
|
||||
register.add_header('Application-Icon', resource)
|
||||
if self.password:
|
||||
register.set_password(self.password, self.passwordHash)
|
||||
self.add_origin_info(register)
|
||||
@@ -128,7 +98,7 @@ class GrowlNotifier(object):
|
||||
return self._send('register', register)
|
||||
|
||||
def notify(self, noteType, title, description, icon=None, sticky=False,
|
||||
priority=None, callback=None, identifier=None):
|
||||
priority=None, callback=None, identifier=None, custom={}):
|
||||
"""Send a GNTP notifications
|
||||
|
||||
.. warning::
|
||||
@@ -141,14 +111,16 @@ class GrowlNotifier(object):
|
||||
:param boolean sticky: Sticky notification
|
||||
:param integer priority: Message priority level from -2 to 2
|
||||
:param string callback: URL callback
|
||||
:param dict custom: Custom attributes. Key names should be prefixed with X-
|
||||
according to the spec but this is not enforced by this class
|
||||
|
||||
.. warning::
|
||||
For now, only URL callbacks are supported. In the future, the
|
||||
callback argument will also support a function
|
||||
"""
|
||||
logger.debug('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
|
||||
logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
|
||||
assert noteType in self.notifications
|
||||
notice = gntp.GNTPNotice()
|
||||
notice = gntp.core.GNTPNotice()
|
||||
notice.add_header('Application-Name', self.applicationName)
|
||||
notice.add_header('Notification-Name', noteType)
|
||||
notice.add_header('Notification-Title', title)
|
||||
@@ -162,8 +134,8 @@ class GrowlNotifier(object):
|
||||
if self._checkIcon(icon):
|
||||
notice.add_header('Notification-Icon', icon)
|
||||
else:
|
||||
id = notice.add_resource(icon)
|
||||
notice.add_header('Notification-Icon', id)
|
||||
resource = notice.add_resource(icon)
|
||||
notice.add_header('Notification-Icon', resource)
|
||||
|
||||
if description:
|
||||
notice.add_header('Notification-Text', description)
|
||||
@@ -172,6 +144,9 @@ class GrowlNotifier(object):
|
||||
if identifier:
|
||||
notice.add_header('Notification-Coalescing-ID', identifier)
|
||||
|
||||
for key in custom:
|
||||
notice.add_header(key, custom[key])
|
||||
|
||||
self.add_origin_info(notice)
|
||||
self.notify_hook(notice)
|
||||
|
||||
@@ -179,7 +154,7 @@ class GrowlNotifier(object):
|
||||
|
||||
def subscribe(self, id, name, port):
|
||||
"""Send a Subscribe request to a remote machine"""
|
||||
sub = gntp.GNTPSubscribe()
|
||||
sub = gntp.core.GNTPSubscribe()
|
||||
sub.add_header('Subscriber-ID', id)
|
||||
sub.add_header('Subscriber-Name', name)
|
||||
sub.add_header('Subscriber-Port', port)
|
||||
@@ -195,7 +170,7 @@ class GrowlNotifier(object):
|
||||
"""Add optional Origin headers to message"""
|
||||
packet.add_header('Origin-Machine-Name', platform.node())
|
||||
packet.add_header('Origin-Software-Name', 'gntp.py')
|
||||
packet.add_header('Origin-Software-Version', gntp.__version__)
|
||||
packet.add_header('Origin-Software-Version', __version__)
|
||||
packet.add_header('Origin-Platform-Name', platform.system())
|
||||
packet.add_header('Origin-Platform-Version', platform.platform())
|
||||
|
||||
@@ -214,34 +189,78 @@ class GrowlNotifier(object):
|
||||
packet.validate()
|
||||
data = packet.encode()
|
||||
|
||||
#logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
|
||||
#Less verbose
|
||||
logger.debug('To : %s:%s <%s>', self.hostname, self.port, packet.__class__)
|
||||
logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(self.socketTimeout)
|
||||
s.connect((self.hostname, self.port))
|
||||
s.send(data)
|
||||
recv_data = s.recv(1024)
|
||||
while not recv_data.endswith("\r\n\r\n"):
|
||||
recv_data += s.recv(1024)
|
||||
response = gntp.parse_gntp(recv_data)
|
||||
try:
|
||||
s.connect((self.hostname, self.port))
|
||||
s.send(data)
|
||||
recv_data = s.recv(1024)
|
||||
while not recv_data.endswith(gntp.shim.b("\r\n\r\n")):
|
||||
recv_data += s.recv(1024)
|
||||
except socket.error:
|
||||
# Python2.5 and Python3 compatibile exception
|
||||
exc = sys.exc_info()[1]
|
||||
raise errors.NetworkError(exc)
|
||||
|
||||
response = gntp.core.parse_gntp(recv_data)
|
||||
s.close()
|
||||
|
||||
#logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
|
||||
#Less verbose
|
||||
logger.debug('From : %s:%s <%s>', self.hostname, self.port, response.__class__)
|
||||
logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
|
||||
|
||||
if type(response) == gntp.GNTPOK:
|
||||
return True
|
||||
if response.error()[0] == '404' and 'disabled' in response.error()[1]:
|
||||
# Ignore message saying that user has disabled this class
|
||||
if type(response) == gntp.core.GNTPOK:
|
||||
return True
|
||||
logger.error('Invalid response: %s', response.error())
|
||||
return response.error()
|
||||
|
||||
|
||||
def mini(description, applicationName='PythonMini', noteType="Message",
|
||||
title="Mini Message", applicationIcon=None, hostname='localhost',
|
||||
password=None, port=23053, sticky=False, priority=None,
|
||||
callback=None, notificationIcon=None, identifier=None,
|
||||
notifierFactory=GrowlNotifier):
|
||||
"""Single notification function
|
||||
|
||||
Simple notification function in one line. Has only one required parameter
|
||||
and attempts to use reasonable defaults for everything else
|
||||
:param string description: Notification message
|
||||
|
||||
.. warning::
|
||||
For now, only URL callbacks are supported. In the future, the
|
||||
callback argument will also support a function
|
||||
"""
|
||||
try:
|
||||
growl = notifierFactory(
|
||||
applicationName=applicationName,
|
||||
notifications=[noteType],
|
||||
defaultNotifications=[noteType],
|
||||
applicationIcon=applicationIcon,
|
||||
hostname=hostname,
|
||||
password=password,
|
||||
port=port,
|
||||
)
|
||||
result = growl.register()
|
||||
if result is not True:
|
||||
return result
|
||||
|
||||
return growl.notify(
|
||||
noteType=noteType,
|
||||
title=title,
|
||||
description=description,
|
||||
icon=notificationIcon,
|
||||
sticky=sticky,
|
||||
priority=priority,
|
||||
callback=callback,
|
||||
identifier=identifier,
|
||||
)
|
||||
except Exception:
|
||||
# We want the "mini" function to be simple and swallow Exceptions
|
||||
# in order to be less invasive
|
||||
logger.exception("Growl error")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# If we're running this module directly we're likely running it as a test
|
||||
# so extra debugging is useful
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
mini('Testing mini notification')
|
||||
|
||||
46
gntp/shim.py
Normal file
46
gntp/shim.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
"""
|
||||
Python2.5 and Python3.3 compatibility shim
|
||||
|
||||
Heavily inspirted by the "six" library.
|
||||
https://pypi.python.org/pypi/six
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3:
|
||||
def b(s):
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
return s.encode('utf8', 'replace')
|
||||
|
||||
def u(s):
|
||||
if isinstance(s, bytes):
|
||||
return s.decode('utf8', 'replace')
|
||||
return s
|
||||
|
||||
from io import BytesIO as StringIO
|
||||
from configparser import RawConfigParser
|
||||
else:
|
||||
def b(s):
|
||||
if isinstance(s, unicode):
|
||||
return s.encode('utf8', 'replace')
|
||||
return s
|
||||
|
||||
def u(s):
|
||||
if isinstance(s, unicode):
|
||||
return s
|
||||
if isinstance(s, int):
|
||||
s = str(s)
|
||||
return unicode(s, "utf8", "replace")
|
||||
|
||||
from StringIO import StringIO
|
||||
from ConfigParser import RawConfigParser
|
||||
|
||||
b.__doc__ = "Ensure we have a byte string"
|
||||
u.__doc__ = "Ensure we have a unicode string"
|
||||
4
gntp/version.py
Normal file
4
gntp/version.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
__version__ = '1.0.3'
|
||||
@@ -109,7 +109,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">$T('homePage') </th>
|
||||
<td><a href="http://sabnzbd.org/" target="_blank">http://sabnzbd.org/</a></td>
|
||||
<td><a href="https://sabnzbd.org/" target="_blank">https://sabnzbd.org/</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">$T('menu-wiki') </th>
|
||||
@@ -117,7 +117,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">$T('menu-forums') </th>
|
||||
<td><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td>
|
||||
<td><a href="https://forums.sabnzbd.org/" target="_blank">https://forums.sabnzbd.org/</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">$T('source') </th>
|
||||
@@ -131,6 +131,10 @@
|
||||
<th scope="row">$T('menu-issues') </th>
|
||||
<td><a href="https://sabnzbd.org/wiki/introduction/known-issues" target="_blank">https://sabnzbd.org/wiki/introduction/known-issues</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">$T('menu-donate') </th>
|
||||
<td><a href="https://sabnzbd.org/donate" target="_blank">https://sabnzbd.org/donate</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<label class="config" for="enable_https">$T('opt-enable_https')</label>
|
||||
<input type="checkbox" name="enable_https" id="enable_https" value="1" <!--#if int($enable_https) > 0 then 'checked="checked"' else ""#-->/>
|
||||
<input type="checkbox" name="enable_https" id="enable_https" value="1" <!--#if int($enable_https) > 0 then 'checked="checked" data-original="1"' else ""#-->/>
|
||||
<span class="desc">$T('explain-enable_https')</span>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
|
||||
@@ -13,6 +13,18 @@
|
||||
<!--#end for#-->
|
||||
<!--#end def#-->
|
||||
|
||||
<!--#def show_cat_box($section_label)#-->
|
||||
<div class="col2-cats" <!--#if int($getVar($section_label + '_enable')) > 0 then '' else 'style="display:none"'#-->>
|
||||
<hr>
|
||||
<b>$T('affectedCat')</b><br/>
|
||||
<select name="${section_label}_cats" multiple="multiple" class="multiple_cats">
|
||||
<!--#for $ct in $categories#-->
|
||||
<option value="$ct" <!--#if $ct in $getVar($section_label + '_cats') then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
|
||||
<!--#end for#-->
|
||||
</select>
|
||||
</div>
|
||||
<!--#end def#-->
|
||||
|
||||
<div class="colmask">
|
||||
<form action="saveEmail" method="post" name="fullform" class="fullform" autocomplete="off" novalidate>
|
||||
<input type="hidden" id="session" name="session" value="$session" />
|
||||
@@ -20,7 +32,15 @@
|
||||
<div class="section" id="email">
|
||||
<div class="col2">
|
||||
<h3>$T('cmenu-email') <a href="$helpuri$help_uri#toc0" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
|
||||
</div><!-- /col2 -->
|
||||
<div class="col2-cats" <!--#if int($email_endjob) > 0 then '' else 'style="display:none"'#-->>
|
||||
<b>$T('affectedCat')</b><br/>
|
||||
<select name="email_cats" multiple="multiple" class="multiple_cats">
|
||||
<!--#for $ct in $categories#-->
|
||||
<option value="$ct" <!--#if $ct in $email_cats then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
|
||||
<!--#end for#-->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col1">
|
||||
<fieldset>
|
||||
<div class="field-pair">
|
||||
@@ -79,8 +99,8 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
<!--#if $have_ncenter#-->
|
||||
<div class="section">
|
||||
<div class="col2">
|
||||
@@ -91,7 +111,7 @@
|
||||
<td><label for="ncenter_enable"> $T('opt-ncenter_enable')</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div><!-- /col2 -->
|
||||
</div>
|
||||
<div class="col1" <!--#if int($ncenter_enable) > 0 then '' else 'style="display:none"'#-->>
|
||||
<fieldset>
|
||||
$show_notify_checkboxes('ncenter')
|
||||
@@ -103,8 +123,8 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
<!--#end if#-->
|
||||
<!--#if $nt#-->
|
||||
<div class="section">
|
||||
@@ -116,7 +136,8 @@
|
||||
<td><label for="acenter_enable"> $T('opt-acenter_enable')</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div><!-- /col2 -->
|
||||
$show_cat_box('acenter')
|
||||
</div>
|
||||
<div class="col1" <!--#if int($acenter_enable) > 0 then '' else 'style="display:none"'#-->>
|
||||
<fieldset>
|
||||
$show_notify_checkboxes('acenter')
|
||||
@@ -128,8 +149,8 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
<!--#end if#-->
|
||||
<!--#if $have_ntfosd#-->
|
||||
<div class="section">
|
||||
@@ -141,7 +162,8 @@
|
||||
<td><label for="ntfosd_enable"> $T('opt-ntfosd_enable')</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div><!-- /col2 -->
|
||||
$show_cat_box('ntfosd')
|
||||
</div>
|
||||
<div class="col1" <!--#if int($ntfosd_enable) > 0 then '' else 'style="display:none"'#-->>
|
||||
<fieldset>
|
||||
$show_notify_checkboxes('ntfosd')
|
||||
@@ -153,8 +175,8 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
<!--#end if#-->
|
||||
<div class="section" id="growl">
|
||||
<div class="col2">
|
||||
@@ -165,7 +187,8 @@
|
||||
<td><label for="growl_enable"> $T('opt-growl_enable')</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div><!-- /col2 -->
|
||||
$show_cat_box('growl')
|
||||
</div>
|
||||
<div class="col1" <!--#if int($growl_enable) > 0 then '' else 'style="display:none"'#-->>
|
||||
<fieldset>
|
||||
<div class="field-pair">
|
||||
@@ -187,8 +210,8 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="prowl">
|
||||
<div class="col2">
|
||||
<h3>$T('section-Prowl')</h3>
|
||||
@@ -199,7 +222,8 @@
|
||||
</tr>
|
||||
</table>
|
||||
<em>$T('explain-prowl_enable')</em>
|
||||
</div><!-- /col2 -->
|
||||
$show_cat_box('prowl')
|
||||
</div>
|
||||
<div class="col1" <!--#if int($prowl_enable) > 0 then '' else 'style="display:none"'#-->>
|
||||
<fieldset>
|
||||
<div class="field-pair">
|
||||
@@ -231,8 +255,8 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="pushover">
|
||||
<div class="col2">
|
||||
@@ -244,7 +268,8 @@
|
||||
</tr>
|
||||
</table>
|
||||
<em>$T('explain-pushover_enable')</em>
|
||||
</div><!-- /col2 -->
|
||||
$show_cat_box('pushover')
|
||||
</div>
|
||||
<div class="col1" <!--#if int($pushover_enable) > 0 then '' else 'style="display:none"'#-->>
|
||||
<fieldset>
|
||||
<div class="field-pair">
|
||||
@@ -286,8 +311,8 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="pushbullet">
|
||||
<div class="col2">
|
||||
<h3>$T('section-Pushbullet')</h3>
|
||||
@@ -298,7 +323,8 @@
|
||||
</tr>
|
||||
</table>
|
||||
<em>$T('explain-pushbullet_enable')</em>
|
||||
</div><!-- /col2 -->
|
||||
$show_cat_box('pushbullet')
|
||||
</div>
|
||||
<div class="col1" <!--#if int($pushbullet_enable) > 0 then '' else 'style="display:none"'#-->>
|
||||
<fieldset>
|
||||
<div class="field-pair">
|
||||
@@ -322,19 +348,20 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="nscript">
|
||||
<div class="col2">
|
||||
<h3>$T('section-NScript')</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="nscript_enable" id="nscript_enable" value="1" <!--#if int($nscript_enable) > 0 then 'checked="checked"' else ""#--> /></td>
|
||||
<td><label for="nscript_enable"> $T('opt-nscript_enable')</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
<em>$T('explain-nscript_enable')</em>
|
||||
</div><!-- /col2 -->
|
||||
<h3>$T('section-NScript')</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="nscript_enable" id="nscript_enable" value="1" <!--#if int($nscript_enable) > 0 then 'checked="checked"' else ""#--> /></td>
|
||||
<td><label for="nscript_enable"> $T('opt-nscript_enable')</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
<em>$T('explain-nscript_enable')</em>
|
||||
$show_cat_box('nscript')
|
||||
</div>
|
||||
<div class="col1" <!--#if int($nscript_enable) > 0 then '' else 'style="display:none"'#-->>
|
||||
<fieldset>
|
||||
<div class="field-pair">
|
||||
@@ -360,8 +387,8 @@
|
||||
<div class="alert"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div><!-- /col1 -->
|
||||
</div><!-- /section -->
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /colmask -->
|
||||
|
||||
@@ -374,11 +401,20 @@
|
||||
\$('.col2 input[name$="enable"]').change(function() {
|
||||
if(this.checked) {
|
||||
\$(this).parents('.section').find('.col1').show()
|
||||
\$(this).parents('.col2').find('.col2-cats').show()
|
||||
} else {
|
||||
\$(this).parents('.section').find('.col1').hide()
|
||||
\$(this).parents('.col2').find('.col2-cats').hide()
|
||||
}
|
||||
\$('form').submit()
|
||||
})
|
||||
\$('#email_endjob').change(function() {
|
||||
if(\$(this).val() > 0) {
|
||||
\$(this).parents('.section').find('.col2-cats').show()
|
||||
} else {
|
||||
\$(this).parents('.section').find('.col2-cats').hide()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
Testing functions
|
||||
|
||||
@@ -130,6 +130,11 @@
|
||||
<input type="checkbox" name="auto_sort" id="auto_sort" value="1" <!--#if int($auto_sort) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-auto_sort')</span>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<label class="config" for="direct_unpack">$T('opt-direct_unpack')</label>
|
||||
<input type="checkbox" name="direct_unpack" id="direct_unpack" value="1" <!--#if int($direct_unpack) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-direct_unpack')</span>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
|
||||
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
|
||||
@@ -153,13 +158,6 @@
|
||||
<input type="checkbox" name="enable_all_par" id="enable_all_par" value="1" <!--#if int($enable_all_par) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-enable_all_par').replace('. ', '.<br/>')</span>
|
||||
</div>
|
||||
<!--#if $nt#-->
|
||||
<div class="field-pair">
|
||||
<label class="config" for="multipar">$T('opt-par2_multipar')</label>
|
||||
<input type="checkbox" name="multipar" id="multipar" value="1" <!--#if int($multipar) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-par2_multipar')</span>
|
||||
</div>
|
||||
<!--#end if#-->
|
||||
<div class="field-pair">
|
||||
<label class="config" for="par_option">$T('opt-par_option')</label>
|
||||
<input type="text" name="par_option" id="par_option" value="$par_option" />
|
||||
|
||||
@@ -137,7 +137,8 @@ input[type="checkbox"]+.desc {
|
||||
font-style: italic;
|
||||
padding: 0 1px;
|
||||
}
|
||||
.col2 p {
|
||||
.col2 p,
|
||||
.col2-cats {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 1em 0;
|
||||
|
||||
@@ -228,16 +228,13 @@ function do_restart() {
|
||||
// What template
|
||||
var arrPath = window.location.pathname.split('/');
|
||||
var urlPath = (arrPath[1] == "m" || arrPath[2] == "m") ? '/sabnzbd/m/' : '/sabnzbd/';
|
||||
var switchedHTTPS = !$('#enable_https').is(':checked') && window.location.protocol == 'https:'
|
||||
var switchedHTTPS = ($('#enable_https').is(':checked') == ($('#enable_https').data('original') === undefined))
|
||||
var portsUnchanged = ($('#port').val() == $('#port').data('original')) && ($('#https_port').val() == $('#https_port').data('original'))
|
||||
|
||||
// Are we on settings page?
|
||||
if(!$('body').hasClass('General')) {
|
||||
// Same as before, with fall-back in case location.origin is not supported (<IE9)
|
||||
var urlTotal = window.location.origin ? (window.location.origin + urlPath) : window.location;
|
||||
} else if (!switchedHTTPS && ($('#port').val() == $('#port').data('original')) && ($('#https_port').val() == $('#https_port').data('original'))) {
|
||||
// If the http/https port config didn't change, don't try and guess the URL/port to redirect to
|
||||
// This solves some incorrect behavior if running behind a reverse proxy
|
||||
var urlTotal = window.location.origin ? (window.location.origin + urlPath) : window.location;
|
||||
// Are we on settings page or did nothing change?
|
||||
if(!$('body').hasClass('General') || (!switchedHTTPS && !portsUnchanged)) {
|
||||
// Same as before
|
||||
var urlTotal = window.location.origin + urlPath
|
||||
} else {
|
||||
// Protocol and port depend on http(s) setting
|
||||
if($('#enable_https').is(':checked') && (window.location.protocol == 'https:' || !$('#https_port').val())) {
|
||||
@@ -401,7 +398,7 @@ $(document).ready(function () {
|
||||
$(checkDisabled).on('change', function() {
|
||||
$(this).parent().nextAll().toggleClass('disabled')
|
||||
})
|
||||
if($(checkDisabled).is(':checked')) {
|
||||
if(!$(checkDisabled).is(':checked')) {
|
||||
$(checkDisabled).parent().nextAll().addClass('disabled')
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<!-- ko if: historyStatus.has_rating -->
|
||||
<div class="dropdown history-ratings">
|
||||
<a href="#" class="name-ratings hover-button" data-toggle="dropdown" onclick="keepOpen(this)">
|
||||
<a href="#" class="name-icons hover-button" data-toggle="dropdown" onclick="keepOpen(this)">
|
||||
<span class="glyphicon glyphicon-facetime-video"></span> <span data-bind="text: historyStatus.rating_avg_video"></span>
|
||||
<span class="glyphicon glyphicon-volume-up"></span> <span data-bind="text: historyStatus.rating_avg_audio"></span>
|
||||
</a>
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu menu-options">
|
||||
<li><a href="#modal-help" data-toggle="modal"><span class="glyphicon glyphicon-question-sign"></span> $T('menu-help')</a></li>
|
||||
<li><a href="https://sabnzbd.org/donate" target="_blank"><span class="glyphicon glyphicon-heart"></span> $T('menu-donate')</a></li>
|
||||
<!--#if $have_logout or $have_quota or $have_rss_defined or $have_watched_dir or $pp_pause_event#--><li class="divider"></li><!--#end if#-->
|
||||
<!--#if $have_logout#--><li><a href="./login/?logout=1"><span class="glyphicon glyphicon-log-out"></span> $T('logout')</a></li><!--#end if#-->
|
||||
<!--#if $have_quota#--><li><a href="#" data-bind="click: doQueueAction" data-mode="reset_quota">$T('link-resetQuota')</a></li><!--#end if#-->
|
||||
|
||||
@@ -60,13 +60,13 @@
|
||||
<p><strong>If anything is not working as expected, or could be improved, let us know!</strong></p>
|
||||
<p><strong>If you encounter an error, please include the log file (click on <span class="glyphicon glyphicon-wrench"></span> ) when contacting us.</strong></p>
|
||||
<h4>General</h4>
|
||||
<span class="glyphicon glyphicon-home"></span> <a href="http://forums.sabnzbd.org/" target="_blank">SABnzbd Forum</a><br />
|
||||
<span class="glyphicon glyphicon-home"></span> <a href="https://forums.sabnzbd.org/" target="_blank">SABnzbd Forum</a><br />
|
||||
<span class="glyphicon glyphicon-plane"></span> <a href="https://github.com/sabnzbd/sabnzbd/" target="_blank">SABnzbd on Github</a><br />
|
||||
<span class="glyphicon glyphicon-globe"></span> <a href="https://translations.launchpad.net/sabnzbd" target="_blank">Translations of SABnzbd</a><br />
|
||||
<span class="glyphicon glyphicon-envelope"></span> <a href="mailto:bugs@sabnzbd.org?body=Version:%20$version%20Skin:%20Glitter">Email bugs@sabnzbd.org</a>
|
||||
|
||||
<h4>Interface (Glitter)</h4>
|
||||
<span class="glyphicon glyphicon-home"></span> <a href="http://forums.sabnzbd.org/viewtopic.php?f=5&t=18880" target="_blank">Glitter at SABnzbd Forum</a><br />
|
||||
<span class="glyphicon glyphicon-home"></span> <a href="https://forums.sabnzbd.org/viewtopic.php?f=5&t=18880" target="_blank">Glitter at SABnzbd Forum</a><br />
|
||||
<span class="glyphicon glyphicon-envelope"></span> <a href="mailto:safihre@sabnzbd.org?body=Version:%20$version">Email safihre@sabnzbd.org</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -525,10 +525,14 @@
|
||||
<div class="progress-bar progress-bar-info" data-bind="attr: { 'style': 'width: '+percentage()+'; background-color: ' + \$parent.filelist.currentItem.progressColor() + ';' }">
|
||||
<input type="checkbox" data-bind="attr: { 'name' : nzf_id }, disable: !canselect(), click : \$parent.filelist.checkSelectRange" title="$T('Glitter-multiSelect')" />
|
||||
<strong data-bind="text: percentage"></strong>
|
||||
<span>
|
||||
<div class="fileDetails">
|
||||
<span data-bind="truncatedTextCenter: filename"></span>
|
||||
<div class="fileControls">
|
||||
<a href="#" data-bind="click: \$parent.filelist.moveButton" class="hover-button buttonMoveToTop" title="$T('Glitter-top')"><span class="glyphicon glyphicon-chevron-up"></span></a>
|
||||
<a href="#" data-bind="click: \$parent.filelist.moveButton" class="hover-button buttonMoveToBottom" title="$T('Glitter-bottom')"><span class="glyphicon glyphicon-chevron-down"></span></a>
|
||||
</div>
|
||||
<small>(<span data-bind="text: file_age"></span> - <span data-bind="text: mb"></span> MB)</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -621,7 +625,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>$T('menu-forums'):</strong></td>
|
||||
<td><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td>
|
||||
<td><a href="https://forums.sabnzbd.org/" target="_blank">https://forums.sabnzbd.org/</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>GitHub:</strong></td>
|
||||
@@ -629,7 +633,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>$T('menu-irc'):</strong></td>
|
||||
<td><a href="http://www.sabnzbd.org/live-chat/" target="_blank">http://www.sabnzbd.org/live-chat/</a></td>
|
||||
<td><a href="https://sabnzbd.org/live-chat" target="_blank">https://sabnzbd.org/live-chat</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -95,17 +95,16 @@
|
||||
<span data-bind="text: password"></span>
|
||||
</small>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: (rating_avg_video() !== false) -->
|
||||
<div class="name-ratings hover-button">
|
||||
<span class="glyphicon glyphicon-facetime-video"></span> <span data-bind="text: rating_avg_video"></span>
|
||||
<span class="glyphicon glyphicon-volume-up"></span> <span data-bind="text: rating_avg_audio"></span>
|
||||
<div class="name-icons direct-unpack hover-button" data-bind="visible: direct_unpack">
|
||||
<span class="glyphicon glyphicon-compressed "></span> <span data-bind="text: direct_unpack">5</span>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<form data-bind="submit: editingNameSubmit">
|
||||
<input type="text" data-bind="value: nameForEdit, visible: editingName(), hasfocus: editingName" />
|
||||
</form>
|
||||
<div class="name-options" data-bind="visible: !editingName()">
|
||||
<a href="#" data-bind="click: \$parent.queue.moveButton" class="hover-button buttonMoveToTop" title="$T('Glitter-MoveToTop')"><span class="glyphicon glyphicon-chevron-up"></span></a>
|
||||
<a href="#" data-bind="click: \$parent.queue.moveButton" class="hover-button buttonMoveToBottom" title="$T('Glitter-MoveToBottom')"><span class="glyphicon glyphicon-chevron-down"></span></a>
|
||||
<a href="#" data-bind="click: editName, css: { disabled: isGrabbing() }" class="hover-button"><span class="glyphicon glyphicon-pencil"></span></a>
|
||||
<a href="#" data-bind="click: showFiles, css: { disabled: isGrabbing() }" class="hover-button" title="$T('nzoDetails') - $T('srv-password')"><span class="glyphicon glyphicon-folder-open"></span></a>
|
||||
<small data-bind="text: avg_age"></small>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
glitterTranslate.pauseFor = "$T('pauseFor')"
|
||||
glitterTranslate.minutes = "$T('mins')"
|
||||
glitterTranslate.shutdown = "$T('shutdownOK?')";
|
||||
glitterTranslate.restart = "$T('explain-Restart')".replace(/<br \/>/g, "\n");
|
||||
glitterTranslate.restart = "$T('explain-Restart') $T('explain-needNewLogin')".replace(/\<br(\s*\/|)\>/g, '\n');
|
||||
glitterTranslate.repair = "$T('explain-Repair')".replace(/<br \/>/g, "\n").replace(/"/g,'"');
|
||||
glitterTranslate.removeDown = "$T('Glitter-confirmClearDownloads')";
|
||||
glitterTranslate.removeDow1 = "$T('Glitter-confirmClear1Download')";
|
||||
|
||||
@@ -35,6 +35,39 @@ function Fileslisting(parent) {
|
||||
})
|
||||
}
|
||||
|
||||
// Move to top and bottom buttons
|
||||
self.moveButton = function (item,event) {
|
||||
var ITEMKEY = "ko_sortItem",
|
||||
INDEXKEY = "ko_sourceIndex",
|
||||
LISTKEY = "ko_sortList",
|
||||
PARENTKEY = "ko_parentList",
|
||||
DRAGKEY = "ko_dragItem",
|
||||
unwrap = ko.utils.unwrapObservable,
|
||||
dataGet = ko.utils.domData.get,
|
||||
dataSet = ko.utils.domData.set;
|
||||
var targetRow,sourceRow,tbody;
|
||||
sourceRow = $(event.currentTarget).parents("tr").filter(":first");
|
||||
tbody = sourceRow.parents("tbody").filter(":first");
|
||||
//debugger;
|
||||
dataSet(sourceRow[0], INDEXKEY, ko.utils.arrayIndexOf(sourceRow.parent().children(), sourceRow[0]));
|
||||
sourceRow = sourceRow.detach();
|
||||
if ($(event.currentTarget).is(".buttonMoveToTop")) {
|
||||
// we are moving to the top
|
||||
targetRow = tbody.children(".files-done").filter(":last");
|
||||
} else {
|
||||
//we are moving to the bottom
|
||||
targetRow = tbody.children(".files-sortable").filter(":last");
|
||||
}
|
||||
if(targetRow.length < 1 ){
|
||||
// we found an edge case and need to do something special
|
||||
targetRow = tbody.children(".files-sortable").filter(":first");
|
||||
sourceRow.insertBefore(targetRow[0]);
|
||||
} else {
|
||||
sourceRow.insertAfter($(targetRow[0]));
|
||||
}
|
||||
tbody.sortable('option', 'update').call(tbody[0],null, { item: sourceRow });
|
||||
};
|
||||
|
||||
// Trigger update
|
||||
self.triggerUpdate = function() {
|
||||
// Call API
|
||||
|
||||
@@ -538,7 +538,7 @@ function ViewModel() {
|
||||
// Go over all warnings and add
|
||||
$.each(response.warnings, function(index, warning) {
|
||||
// Split warning into parts
|
||||
var warningSplit = warning.split(/\n/);
|
||||
var warningSplit = convertHTMLtoText(warning).split(/\n/);
|
||||
|
||||
// Reformat CSS label and date
|
||||
var warningData = {
|
||||
|
||||
@@ -148,7 +148,6 @@ function QueueListModel(parent) {
|
||||
// See what the actual index is of the queue-object
|
||||
// This way we can see how we move up and down independent of pagination
|
||||
var itemReplaced = self.queueItems()[event.targetIndex+corTerm];
|
||||
|
||||
callAPI({
|
||||
mode: "switch",
|
||||
value: itemMoved.id,
|
||||
@@ -156,6 +155,25 @@ function QueueListModel(parent) {
|
||||
}).then(self.parent.refresh);
|
||||
};
|
||||
|
||||
// Move button clicked
|
||||
self.moveButton = function(event,ui) {
|
||||
var itemMoved = event;
|
||||
var targetIndex;
|
||||
if($(ui.currentTarget).is(".buttonMoveToTop")){
|
||||
//we want to move to the top
|
||||
targetIndex = 0;
|
||||
} else {
|
||||
// we want to move to the bottom
|
||||
targetIndex = self.totalItems() - 1;
|
||||
}
|
||||
callAPI({
|
||||
mode: "switch",
|
||||
value: itemMoved.id,
|
||||
value2: targetIndex
|
||||
}).then(self.parent.refresh);
|
||||
|
||||
}
|
||||
|
||||
// Save pagination state
|
||||
self.paginationLimit.subscribe(function(newValue) {
|
||||
// Save in config if global
|
||||
@@ -464,7 +482,8 @@ function QueueModel(parent, data) {
|
||||
self.totalMB = ko.observable(parseFloat(data.mb));
|
||||
self.remainingMB = ko.observable(parseFloat(data.mbleft));
|
||||
self.avg_age = ko.observable(data.avg_age)
|
||||
self.missing = ko.observable(data.missing)
|
||||
self.missing = ko.observable(parseFloat(data.mbmissing))
|
||||
self.direct_unpack = ko.observable(data.direct_unpack)
|
||||
self.category = ko.observable(data.cat);
|
||||
self.priority = ko.observable(parent.priorityName[data.priority]);
|
||||
self.script = ko.observable(data.script);
|
||||
@@ -476,8 +495,6 @@ function QueueModel(parent, data) {
|
||||
self.nameForEdit = ko.observable();
|
||||
self.editingName = ko.observable(false);
|
||||
self.hasDropdown = ko.observable(false);
|
||||
self.rating_avg_video = ko.observable(false)
|
||||
self.rating_avg_audio = ko.observable(false)
|
||||
|
||||
// Color of the progress bar
|
||||
self.progressColor = ko.computed(function() {
|
||||
@@ -485,8 +502,8 @@ function QueueModel(parent, data) {
|
||||
if(self.status() == 'Checking') {
|
||||
return '#58A9FA'
|
||||
}
|
||||
// Check for missing data, the value is arbitrary!
|
||||
if(self.missing() > 50) {
|
||||
// Check for missing data, the value is arbitrary! (3%)
|
||||
if(self.missing()/self.totalMB() > 0.03) {
|
||||
return '#F8A34E'
|
||||
}
|
||||
// Set to grey, only when not Force download
|
||||
@@ -510,9 +527,9 @@ function QueueModel(parent, data) {
|
||||
|
||||
// Texts
|
||||
self.missingText= ko.pureComputed(function() {
|
||||
// Check for missing data, the value is arbitrary!
|
||||
if(self.missing() > 50) {
|
||||
return self.missing() + ' ' + glitterTranslate.misingArt
|
||||
// Check for missing data, the value is arbitrary! (3%)
|
||||
if(self.missing()/self.totalMB() > 0.03) {
|
||||
return self.missing().toFixed(0) + ' MB ' + glitterTranslate.misingArt
|
||||
}
|
||||
return;
|
||||
})
|
||||
@@ -565,19 +582,14 @@ function QueueModel(parent, data) {
|
||||
self.totalMB(parseFloat(data.mb));
|
||||
self.remainingMB(parseFloat(data.mbleft));
|
||||
self.avg_age(data.avg_age)
|
||||
self.missing(data.missing)
|
||||
self.missing(parseFloat(data.mbmissing))
|
||||
self.direct_unpack(data.direct_unpack)
|
||||
self.category(data.cat);
|
||||
self.priority(parent.priorityName[data.priority]);
|
||||
self.script(data.script);
|
||||
self.unpackopts(parseInt(data.unpackopts)) // UnpackOpts fails if not parseInt'd!
|
||||
self.pausedStatus(data.status == 'Paused');
|
||||
self.timeLeft(data.timeleft);
|
||||
|
||||
// If exists, otherwise false
|
||||
if(data.rating_avg_video !== undefined) {
|
||||
self.rating_avg_video(data.rating_avg_video === 0 ? '-' : data.rating_avg_video);
|
||||
self.rating_avg_audio(data.rating_avg_audio === 0 ? '-' : data.rating_avg_audio);
|
||||
}
|
||||
};
|
||||
|
||||
// Pause individual download
|
||||
|
||||
@@ -50,7 +50,8 @@ legend,
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.hover-button {
|
||||
.hover-button,
|
||||
.fileControls a:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -81,7 +82,8 @@ legend,
|
||||
|
||||
.max-speed-input-clear,
|
||||
.max-speed-input-clear:hover,
|
||||
.nav-tabs>li>a:hover {
|
||||
.nav-tabs>li>a:hover,
|
||||
.fileControls a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@@ -175,7 +177,7 @@ tbody .caret {
|
||||
color: #D6D6D6;
|
||||
}
|
||||
|
||||
td.name .name-ratings span,
|
||||
td.name .name-icons span,
|
||||
.navbar-nav .open .dropdown-menu>li>a,
|
||||
.dropdown-header,
|
||||
#modal-help small,
|
||||
|
||||
@@ -52,8 +52,8 @@ h2 {
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
margin-left: -150px;
|
||||
margin-top: 4px;
|
||||
margin-left: -220px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.navbar-collapse.in .dropdown-menu a,
|
||||
@@ -617,6 +617,7 @@ td.name .row-wrap-text {
|
||||
}
|
||||
|
||||
.queue-table td.name .name-options small,
|
||||
.queue-table td.name .direct-unpack,
|
||||
.queue-item-password {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -626,7 +627,7 @@ td.name .row-wrap-text {
|
||||
}
|
||||
|
||||
.queue-table td.name:hover .row-wrap-text {
|
||||
max-width: calc(100% - 85px);
|
||||
max-width: calc(100% - 125px);
|
||||
/* Change for each size! */
|
||||
}
|
||||
|
||||
@@ -648,18 +649,18 @@ td.name .row-wrap-text {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
td.name .name-ratings {
|
||||
td.name .name-icons {
|
||||
display: inline;
|
||||
margin-left: 5px;
|
||||
color: black !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.queue-table td.name:hover .name-ratings {
|
||||
.queue-table td.name:hover .name-icons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td.name .name-ratings .glyphicon {
|
||||
td.name .name-icons .glyphicon {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@@ -769,6 +770,35 @@ tr.queue-item>td:first-child>a {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.item-files-table tr .fileControls{
|
||||
float:right;
|
||||
display:none;
|
||||
}
|
||||
|
||||
.item-files-table tr.files-sortable:hover .fileControls{
|
||||
float:right;
|
||||
display:block;
|
||||
margin-left:5px;
|
||||
}
|
||||
|
||||
.progress .progress-bar .fileDetails {
|
||||
display:inline;
|
||||
text-align: left;
|
||||
margin-left: 70px;
|
||||
line-height: 25px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
color: #404040;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.progress .progress-bar .fileDetails>span {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.progress strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -1035,7 +1065,7 @@ tr.queue-item>td:first-child>a {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.history-ratings .name-ratings {
|
||||
.history-ratings .name-icons {
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
@@ -1623,6 +1653,11 @@ input[name="nzbURL"] {
|
||||
|
||||
#modal-item-files .item-files-table .progress small {
|
||||
color: #727272 !important;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#modal-item-files .item-files-table tr.files-sortable:hover .progress small {
|
||||
display:none;
|
||||
}
|
||||
|
||||
#modal-item-files .item-files-table td {
|
||||
@@ -1810,7 +1845,7 @@ input[name="nzbURL"] {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
td.name .name-ratings {
|
||||
td.name .name-icons {
|
||||
margin-left: 0px;
|
||||
margin-right: -5px;
|
||||
display: block;
|
||||
@@ -1857,6 +1892,11 @@ input[name="nzbURL"] {
|
||||
.queue .sortable-placeholder td {
|
||||
padding: 9px 0px 8px !important;
|
||||
}
|
||||
|
||||
.queue-table .buttonMoveToBottom,
|
||||
.queue-table .buttonMoveToTop {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px) {
|
||||
|
||||
@@ -132,6 +132,11 @@ h2 {
|
||||
max-width: calc(100% - 45px);
|
||||
}
|
||||
|
||||
.queue-table .buttonMoveToBottom,
|
||||
.queue-table .buttonMoveToTop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tr.queue-item>td:first-child>a {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Plush for SABnzbd 0.6.x | Feb. 21 2010
|
||||
assembled by pairofdimes - see LICENSE-CC.txt
|
||||
http://forums.sabnzbd.org contributions welcome
|
||||
https://forums.sabnzbd.org contributions welcome
|
||||
|
||||
======================
|
||||
THANKS TO CONTRIBUTORS
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
<div id="help_modal">
|
||||
<table>
|
||||
<tr><td><strong>$T('menu-wiki'):</strong></td><td><a href="$helpuri$help_uri" target="_blank">$helpuri$help_uri</a></td></tr>
|
||||
<tr><td><strong>$T('menu-forums'):</strong></td><td><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td></tr>
|
||||
<tr><td><strong>$T('menu-irc'):</strong></td><td><a href="http://www.sabnzbd.org/live-chat/" target="_blank">http://www.sabnzbd.org/live-chat/</a></td></tr>
|
||||
<tr><td><strong>$T('menu-forums'):</strong></td><td><a href="https://forums.sabnzbd.org/" target="_blank">https://forums.sabnzbd.org/</a></td></tr>
|
||||
<tr><td><strong>$T('menu-irc'):</strong></td><td><a href="https://sabnzbd.org/live-chat.html" target="_blank">https://sabnzbd.org/live-chat.html</a></td></tr>
|
||||
</table>
|
||||
<div class="sabnzbd_logo main_sprite_container sprite_sabnzbdplus_logo"></div>
|
||||
<p><strong>SABnzbd $T('version'):</strong> $version</p>
|
||||
|
||||
@@ -1134,7 +1134,7 @@ function loadingJSON(){
|
||||
<li><a style="text-decoration:underline;cursor:pointer;" onclick="if(confirm('$T('shutdownOK?')')){shutdown()}">$T('link-shutdown')</a></li>
|
||||
<br/>
|
||||
<li><a href="$helpuri" target="_blank">$T('menu-wiki')</a></li>
|
||||
<li><a href="http://forums.sabnzbd.org" target="_blank">$T('menu-forums')</a></li>
|
||||
<li><a href="https://forums.sabnzbd.org" target="_blank">$T('menu-forums')</a></li>
|
||||
<li><a href="http://sabnzbd.org/live-chat/" target="_blank">$T('menu-irc')</a></li>
|
||||
</ul>
|
||||
<!--<input type="checkbox" name="enable_speedlimit" />-->
|
||||
|
||||
Binary file not shown.
@@ -1,11 +1,11 @@
|
||||
#
|
||||
# SABnzbd Translation Template file EMAIL
|
||||
# Copyright (C) 2011-2015 by the SABnzbd Team
|
||||
# Copyright 2011-2017 The SABnzbd-Team
|
||||
# team@sabnzbd.org
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-0.8.x\n"
|
||||
"Project-Id-Version: SABnzbd-2.2.0-develop\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: shypike@sabnzbd.org\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: shypike <Unknown>\n"
|
||||
"Language-Team: Danish <da@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Thomas Lucke (Lucky) <Unknown>\n"
|
||||
"Language-Team: German <de@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: shypike <Unknown>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Matti Ylönen <Unknown>\n"
|
||||
"Language-Team: Finnish <fi@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Fox Ace <Unknown>\n"
|
||||
"Language-Team: French <fr@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
121
po/email/he.po
Normal file
121
po/email/he.po
Normal file
@@ -0,0 +1,121 @@
|
||||
# Hebrew translation for sabnzbd
|
||||
# Copyright (c) 2017 Rosetta Contributors and Canonical Ltd 2017
|
||||
# This file is distributed under the same license as the sabnzbd package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-06-13 09:56+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Hebrew <he@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
"##\n"
|
||||
"## Default Email template for SABnzbd\n"
|
||||
"## This a Cheetah template\n"
|
||||
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#else#-->\n"
|
||||
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
|
||||
"\"(newzbin #\" + $msgid + \")\"#-->\n"
|
||||
"<!--#end if#-->\n"
|
||||
"Finished at $end_time\n"
|
||||
"Downloaded $size\n"
|
||||
"\n"
|
||||
"Results of the job:\n"
|
||||
"<!--#for $stage in $stages #-->\n"
|
||||
"Stage $stage <!--#slurp#-->\n"
|
||||
"<!--#for $result in $stages[$stage]#-->\n"
|
||||
" $result <!--#slurp#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#if $script!=\"\" #-->\n"
|
||||
"Output from user script \"$script\" (Exit code = $script_ret):\n"
|
||||
"$script_output\n"
|
||||
"<!--#end if#-->\n"
|
||||
"<!--#if $status #-->\n"
|
||||
"Enjoy!\n"
|
||||
"<!--#else#-->\n"
|
||||
"Sorry!\n"
|
||||
"<!--#end if#-->\n"
|
||||
msgstr ""
|
||||
|
||||
#: email/rss.tmpl:1
|
||||
msgid ""
|
||||
"##\n"
|
||||
"## RSS Email template for SABnzbd\n"
|
||||
"## This a Cheetah template\n"
|
||||
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has added $amount jobs to the queue\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"\n"
|
||||
"SABnzbd has added $amount job(s) to the queue.\n"
|
||||
"They are from RSS feed \"$feed\".\n"
|
||||
"<!--#for $job in $jobs#-->\n"
|
||||
" $job <!--#slurp#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"\n"
|
||||
"Bye\n"
|
||||
msgstr ""
|
||||
|
||||
#: email/badfetch.tmpl:1
|
||||
msgid ""
|
||||
"##\n"
|
||||
"## Bad URL Fetch Email template for SABnzbd\n"
|
||||
"## This a Cheetah template\n"
|
||||
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd failed to fetch an NZB\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"\n"
|
||||
"Hi,\n"
|
||||
"\n"
|
||||
"SABnzbd has failed to retrieve the NZB from $url.\n"
|
||||
"The error message was: $msg\n"
|
||||
"\n"
|
||||
"Bye\n"
|
||||
msgstr ""
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Norwegian Bokmal <nb@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: shypike <Unknown>\n"
|
||||
"Language-Team: Dutch <nl@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Tomasz 'Zen' Napierala <tomasz@napierala.org>\n"
|
||||
"Language-Team: Polish <pl@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: lrrosa <Unknown>\n"
|
||||
"Language-Team: Brazilian Portuguese <pt_BR@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: nicusor <Unknown>\n"
|
||||
"Language-Team: Romanian <ro@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Pavel Maryanov <Unknown>\n"
|
||||
"Language-Team: Russian <gnu@mx.ru>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Ozzii <Unknown>\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-06-24 19:51+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>\n"
|
||||
"Language-Team: Launchpad Serbian Translators\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-27 06:00+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
"Language: sr\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
@@ -66,20 +66,20 @@ msgid ""
|
||||
"<!--#end if#-->\n"
|
||||
msgstr ""
|
||||
"##\n"
|
||||
"## Шаблон основне е-поште за САБнзбд\n"
|
||||
"## Основни шаблон ел. поште за САБнзбд\n"
|
||||
"## Ово је Гепард шаблон\n"
|
||||
"## Документација: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Нови редови и размаци су важни!\n"
|
||||
"##\n"
|
||||
"## Ово су заглавља е-поруке\n"
|
||||
"Прима: $to\n"
|
||||
"Шаље: $from\n"
|
||||
"Датум: $date\n"
|
||||
"Тема: САБнзбд је <!--#if $status then \"завршио\" else \"није обавио\" #--> "
|
||||
"посао $name\n"
|
||||
"Х-приоритет: 5\n"
|
||||
"Х-МС-приоритет: 5\n"
|
||||
"## Ово су заглавља ел. поште\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: САБнзбд је <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"посао „$name“\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## После тога долази разрада, празни редови су потребни!\n"
|
||||
"\n"
|
||||
"Здраво,\n"
|
||||
@@ -101,7 +101,7 @@ msgstr ""
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#end for#-->\n"
|
||||
"<!--#if $script!=\"\" #-->\n"
|
||||
"Излаз корисничке скрипте „$script“ (Код излаза = $script_ret):\n"
|
||||
"Излаз корисничке скрипте „$script“ (Шифра излаза = $script_ret):\n"
|
||||
"$script_output\n"
|
||||
"<!--#end if#-->\n"
|
||||
"<!--#if $status #-->\n"
|
||||
@@ -139,19 +139,19 @@ msgid ""
|
||||
"Bye\n"
|
||||
msgstr ""
|
||||
"##\n"
|
||||
"## Шаблон РСС е-поруке за САБнзбд\n"
|
||||
"## РСС шаблон ел. поште за САБнзбд\n"
|
||||
"## Ово је Гепард шаблон\n"
|
||||
"## Документација: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Нови редови и размаци су важни!\n"
|
||||
"##\n"
|
||||
"## Ово су заглавља е-поруке\n"
|
||||
"Прима: $to\n"
|
||||
"Шаље: $from\n"
|
||||
"Датум: $date\n"
|
||||
"Тема: САБнзбд је додао $amount посла у ред\n"
|
||||
"Х-приоритет: 5\n"
|
||||
"Х-МС-приоритет: 5\n"
|
||||
"## Ово су заглавља ел. поште\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject САБнзбд је додао $amount посла у ред\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## После тога долази разрада, празни редови су потребни!\n"
|
||||
"\n"
|
||||
"Здраво,\n"
|
||||
@@ -190,24 +190,24 @@ msgid ""
|
||||
"Bye\n"
|
||||
msgstr ""
|
||||
"##\n"
|
||||
"## Bad URL Fetch Email template for SABnzbd\n"
|
||||
"## This a Cheetah template\n"
|
||||
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"## Шаблон ел. поште лошег набављања адресе за САБнзбд\n"
|
||||
"## Ово је Гепард шаблон\n"
|
||||
"## Документација: http://sabnzbd.wikidot.com/email-templates\n"
|
||||
"##\n"
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"## Нови редови и размаци су важни!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"За: $to\n"
|
||||
"Од: $from\n"
|
||||
"Датум: $date\n"
|
||||
"Сујјекат: SABnzbd није успео да преузме НЗБ\n"
|
||||
"## Ово су заглавља ел. поште\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: САБнзбд није успео да преузме НЗБ\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
"## После тога долази разрада, празни редови су потребни!\n"
|
||||
"\n"
|
||||
"Здраво,\n"
|
||||
"\n"
|
||||
"SABnzbd није успео да преузме НЗБ од $url.\n"
|
||||
"САБнзбд није успео да преузме НЗБ са „$url“.\n"
|
||||
"Порука грешке је: $msg\n"
|
||||
"\n"
|
||||
"Поздрав\n"
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Andreas Lindberg <andypandyswe@gmail.com>\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-06-24 19:50+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>\n"
|
||||
"Language-Team: Swedish <sv@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-04-26 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 17430)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-27 06:00+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
@@ -72,10 +72,10 @@ msgstr ""
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"Till: $to\n"
|
||||
"Från: $from\n"
|
||||
"Datum: $date\n"
|
||||
"Ämne: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
|
||||
"job $name\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
@@ -145,10 +145,10 @@ msgstr ""
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"Till: $to\n"
|
||||
"Från: $from\n"
|
||||
"Datum: $date\n"
|
||||
"Ämne: SABnzbd har lagt till $amount jobb i kön\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd har lagt till $amount jobb i kön\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
@@ -196,10 +196,10 @@ msgstr ""
|
||||
"## Newlines and whitespace are significant!\n"
|
||||
"##\n"
|
||||
"## These are the email headers\n"
|
||||
"Till: $to\n"
|
||||
"Från: $from\n"
|
||||
"Datum: $date\n"
|
||||
"Ämne: SABnzbd misslyckades med att hämta en NZB -fil\n"
|
||||
"To: $to\n"
|
||||
"From: $from\n"
|
||||
"Date: $date\n"
|
||||
"Subject: SABnzbd misslyckades med att hämta en NZB -fil\n"
|
||||
"X-priority: 5\n"
|
||||
"X-MS-priority: 5\n"
|
||||
"## After this comes the body, the empty line is required!\n"
|
||||
|
||||
@@ -7,15 +7,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2015-04-25 09:21+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2015-10-24 11:05+0000\n"
|
||||
"Last-Translator: shypike <Unknown>\n"
|
||||
"Language-Team: Chinese (Simplified) <zh_CN@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2015-10-25 05:43+0000\n"
|
||||
"X-Generator: Launchpad (build 17812)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:55+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: email/email.tmpl:1
|
||||
msgid ""
|
||||
|
||||
2398
po/main/SABnzbd.pot
2398
po/main/SABnzbd.pot
File diff suppressed because it is too large
Load Diff
2527
po/main/da.po
2527
po/main/da.po
File diff suppressed because it is too large
Load Diff
2529
po/main/de.po
2529
po/main/de.po
File diff suppressed because it is too large
Load Diff
@@ -15,27 +15,21 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: sabnzbd/skintext.py:67 [Config->Scheduler]
|
||||
msgid "Pause low prioirty jobs"
|
||||
msgstr "Pause low priority jobs"
|
||||
|
||||
#: sabnzbd/skintext.py:68 [Config->Scheduler]
|
||||
msgid "Pause normal prioirty jobs"
|
||||
msgstr "Pause normal priority jobs"
|
||||
|
||||
#: sabnzbd/skintext.py:69 [Config->Scheduler]
|
||||
msgid "Pause high prioirty jobs"
|
||||
msgstr "Pause high priority jobs"
|
||||
|
||||
#: sabnzbd/skintext.py:70 [Config->Scheduler]
|
||||
msgid "Resume low prioirty jobs"
|
||||
msgstr "Resume low priority jobs"
|
||||
|
||||
#: sabnzbd/skintext.py:71 [Config->Scheduler]
|
||||
msgid "Resume normal prioirty jobs"
|
||||
msgstr "Resume normal priority jobs"
|
||||
|
||||
#: sabnzbd/skintext.py:72 [Config->Scheduler]
|
||||
msgid "Resume high prioirty jobs"
|
||||
msgstr "Resume high priority jobs"
|
||||
|
||||
@@ -48,88 +42,66 @@ msgstr ""
|
||||
"click the button below.<br /><br /><strong><a "
|
||||
"href=\"..\">Refresh</a></strong><br />"
|
||||
|
||||
#: sabnzbd/skintext.py:251 [Retry all failed jobs in History]
|
||||
msgid "Retry all failed"
|
||||
msgstr "Retry All Failed"
|
||||
|
||||
#: sabnzbd/skintext.py:54 [#: Config->Scheduler]
|
||||
msgid "disable server"
|
||||
msgstr "Disable server:"
|
||||
|
||||
#: sabnzbd/skintext.py:55 [#: Config->Scheduler]
|
||||
msgid "enable server"
|
||||
msgstr "Enable server:"
|
||||
|
||||
#: sabnzbd/emailer.py:117
|
||||
msgid "The server didn't reply properly to the helo greeting"
|
||||
msgstr "The server didn't reply properly to the hello greeting"
|
||||
|
||||
#: sabnzbd/interface.py:226
|
||||
msgid "API Key missing, please enter the api key from Config->General into your 3rd party program:"
|
||||
msgstr "API key missing, please enter the API key from Config->General into your 3rd party program:"
|
||||
|
||||
#: sabnzbd/interface.py:233
|
||||
msgid "API Key incorrect, Use the api key from Config->General in your 3rd party program:"
|
||||
msgstr "API key incorrect, Use the API key from Config->General in your 3rd party program:"
|
||||
|
||||
#: sabnzbd/skintext.py:330
|
||||
msgid "HTTPS Chain Certifcates"
|
||||
msgstr "HTTPS Chain Certificates"
|
||||
|
||||
#: sabnzbd/skintext.py:465
|
||||
msgid "Replace Spaces in Foldername"
|
||||
msgstr "Replace spaces in folder name"
|
||||
|
||||
#: sabnzbd/skintext.py:467
|
||||
msgid "Replace dots in Foldername"
|
||||
msgstr "Replace dots in folder name"
|
||||
|
||||
#: sabnzbd/skintext.py:693
|
||||
msgid "Original Foldername"
|
||||
msgstr "Original folder name"
|
||||
|
||||
#: sabnzbd/skintext.py:808
|
||||
msgid "How long or untill when do you want to pause? (in English!)"
|
||||
msgstr "How long or until when do you want to pause? (in English!)"
|
||||
|
||||
#: sabnzbd/skintext.py:917
|
||||
msgid "Timeleft"
|
||||
msgstr "Time left"
|
||||
|
||||
#: sabnzbd/skintext.py:791 # sabnzbd/skintext.py:871
|
||||
msgid "Optionally specify a filename"
|
||||
msgstr "Optionally specify a name"
|
||||
|
||||
#: sabnzbd/skintext.py:819 # sabnzbd/skintext.py:880
|
||||
msgid "Confirm Queue Deletions"
|
||||
msgstr "Confirm queue deletions"
|
||||
|
||||
#: sabnzbd/skintext.py:820 # sabnzbd/skintext.py:881
|
||||
msgid "Confirm History Deletions"
|
||||
msgstr "Confirm history deletions"
|
||||
|
||||
#: sabnzbd/skintext.py:288 [Do not translate Pystone]
|
||||
msgid "System Performance (Pystone)"
|
||||
msgstr "System performance (Pystone)"
|
||||
|
||||
#: sabnzbd/skintext.py:317 # sabnzbd/skintext.py:779
|
||||
msgid "Web Interface"
|
||||
msgstr "Web interface"
|
||||
|
||||
#: sabnzbd/notifier.py
|
||||
msgid "Script returned exit code %s and output \"%s\""
|
||||
msgstr "Notification script returned exit code %s and output \"%s\""
|
||||
|
||||
#: sabnzbd/skintext.py:333
|
||||
msgid "If empty, the standard port will only listen to HTTPS."
|
||||
msgstr "If empty, the SABnzbd Port set above will listen to HTTPS."
|
||||
|
||||
#: sabnzbd/skintext.py:466
|
||||
msgid "Posts will be paused untill they are at least this age. Setting job priority to Force will skip the delay."
|
||||
msgstr "Posts will be paused until they are at least this age. Setting job priority to Force will skip the delay."
|
||||
|
||||
#: sabnzbd/skintext.py:841
|
||||
msgid "How long or until when do you want to pause? (in English!)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Support the project, donate!"
|
||||
|
||||
|
||||
2529
po/main/es.po
2529
po/main/es.po
File diff suppressed because it is too large
Load Diff
2533
po/main/fi.po
2533
po/main/fi.po
File diff suppressed because it is too large
Load Diff
2537
po/main/fr.po
2537
po/main/fr.po
File diff suppressed because it is too large
Load Diff
2996
po/main/he.po
2996
po/main/he.po
File diff suppressed because it is too large
Load Diff
2521
po/main/nb.po
2521
po/main/nb.po
File diff suppressed because it is too large
Load Diff
2553
po/main/nl.po
2553
po/main/nl.po
File diff suppressed because it is too large
Load Diff
2523
po/main/pl.po
2523
po/main/pl.po
File diff suppressed because it is too large
Load Diff
2525
po/main/pt_BR.po
2525
po/main/pt_BR.po
File diff suppressed because it is too large
Load Diff
2526
po/main/ro.po
2526
po/main/ro.po
File diff suppressed because it is too large
Load Diff
2522
po/main/ru.po
2522
po/main/ru.po
File diff suppressed because it is too large
Load Diff
2519
po/main/sr.po
2519
po/main/sr.po
File diff suppressed because it is too large
Load Diff
2521
po/main/sv.po
2521
po/main/sv.po
File diff suppressed because it is too large
Load Diff
2521
po/main/zh_CN.po
2521
po/main/zh_CN.po
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SABnzbd-develop\n"
|
||||
"Project-Id-Version: SABnzbd-2.2.0-develop\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: shypike@sabnzbd.org\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -13,71 +13,71 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=ASCII\n"
|
||||
"Content-Transfer-Encoding: 7bit\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "The installation directory has changed (now in \"Program Files\"). \\nIf you run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This system requires the Microsoft runtime library VC90 to be installed first. Do you want to do that now?"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "You cannot overwrite an existing installation. \\n\\nClick `OK` to remove the previous version or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -7,33 +7,33 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-04-10 11:28+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>\n"
|
||||
"Language-Team: Danish <da@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-04-14 05:48+0000\n"
|
||||
"X-Generator: Launchpad (build 18352)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Vis udgivelsesbemærkninger"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Støt projektet, donér!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Luk venligst \"SABnzbd.exe\" først"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
@@ -41,31 +41,31 @@ msgstr ""
|
||||
"Installationsmappen er ændret (nu i \"Program Files \"). \\nHvis du kører "
|
||||
"SABnzbd som en tjeneste, skal du opdatere tjenesteindstillingerne."
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Dette vil afinstallere SABnzbd fra dit system"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Kør ved opstart"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Skrivebordsikon"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "NZB filtilknytning"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Slet program"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Slet indstillinger"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -73,19 +73,19 @@ msgstr ""
|
||||
"Dette system kræver, at Microsoft runtime biblioteket VC90 skal installeres "
|
||||
"først. Ønsker du at gøre det nu?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Downloader Microsoft runtime installationsfil..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Download fejl, prøv igen?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Kan ikke installere uden runtime bibliotek, prøv igen?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -94,7 +94,7 @@ msgstr ""
|
||||
"fjerne den tidligere version eller `Annuller` for at annullere denne "
|
||||
"opgradering."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Dine indstillinger og data vil blive bevaret."
|
||||
|
||||
|
||||
@@ -7,33 +7,33 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-05-22 08:00+0000\n"
|
||||
"Last-Translator: larshuth <Unknown>\n"
|
||||
"Language-Team: German <de@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-05-23 06:10+0000\n"
|
||||
"X-Generator: Launchpad (build 18387)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Versionshinweise anzeigen"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr "Starte SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Bitte unterstützen Sie das Projekt durch eine Spende!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Schliessen Sie bitte zuerst \"SABnzbd.exe\"."
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
@@ -42,31 +42,31 @@ msgstr ""
|
||||
"nWenn du SABnzbd als Service ausführst, musst du die Serviceeinstellungen "
|
||||
"anpassen."
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Dies entfernt SABnzbd von Ihrem System"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Beim Systemstart ausführen"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Desktop-Symbol"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "Mit NZB-Dateien verknüpfen"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Programm löschen"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Einstellungen löschen"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -74,22 +74,22 @@ msgstr ""
|
||||
"Dieses System erfordert die Installation der Laufzeitbibliothek VC90 von "
|
||||
"Microsoft. Möchten Sie die Installation jetzt durchführen?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr ""
|
||||
"Installationsprogramm für Microsoft-Laufzeitbibliothek wird "
|
||||
"heruntergeladen..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Download-Fehler. Erneut versuchen?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr ""
|
||||
"Installation ohne Laufzeitbibliothek nicht möglich. Erneut versuchen?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -98,7 +98,7 @@ msgstr ""
|
||||
"Sie 'OK', um die vorherige Version zu entfernen oder 'Abbrechen' um die "
|
||||
"Aktualisierung abzubrechen."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Ihre Einstellungen und Daten bleiben erhalten."
|
||||
|
||||
|
||||
@@ -7,63 +7,63 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Victor Herrero <victorhera@gmail.com>\n"
|
||||
"Language-Team: Spanish <es@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-19 06:37+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Mostrar notas de la versión"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "¡Apoye el proyecto, haga una donación!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Por favor cierre primero \"SABnzbd.exe\""
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Esto desinstalará SABnzbd de su sistema"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Ejecutar al inicio"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Icono del escritorio"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "Asociación de archivos NZB"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Eliminar programa"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Eliminar Ajustes"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -71,20 +71,20 @@ msgstr ""
|
||||
"Este sistema requiere la ejecución de la biblioteca Microsoft runtime VC90 "
|
||||
"que debe ser instalada. ¿Quieres hacerlo ahora?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Descargando el instalador de Microsoft runtime..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Error en la descarga, ¿probamos de nuevo?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr ""
|
||||
"No se puede instalar sin la biblioteca runtime, ¿Lo volvemos a intentar?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -92,7 +92,7 @@ msgstr ""
|
||||
"No es posible sobrescribir una instalación existente. \\n\\nPresione `OK' "
|
||||
"para quitar la versión anterior o 'Cancelar' para cancelar la actualización."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Tus ajustes y datos se mantendrán intactos."
|
||||
|
||||
|
||||
@@ -7,33 +7,33 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-04-02 07:38+0000\n"
|
||||
"Last-Translator: Paavo Rissanen <paavo.rissanen@outlook.com>\n"
|
||||
"Language-Team: Finnish <fi@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-04-05 07:19+0000\n"
|
||||
"X-Generator: Launchpad (build 18335)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Näytä julkaisutiedot"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr "Käynnistä SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Tue projektia, lahjoita!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Ole hyvä ja sulje \"SABnzbd.exe\" ensin"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
@@ -41,31 +41,31 @@ msgstr ""
|
||||
"Asennuskansio on muuttunut (nykyisin \"Program Files\"). \\nJos suoritat "
|
||||
"SABnzbd:ta palveluna, sinun täytyy päivittää palvelun asetukset."
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Tämä poistaa SABnzbd:n tietokoneestasi"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Suorita käynnistyksen yhteydessä"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Työpöydän kuvake"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "NZB tiedostosidos"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Poista sovellus"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Poista asetukset"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -73,19 +73,19 @@ msgstr ""
|
||||
"Tämä järjestelmä vaatii, että Microsoft runtime kirjasto VC90 täytyy asentaa "
|
||||
"ensin. Haluatko asentaa sen nyt?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Ladataan Microsoft runtime asennusta..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Latausvirhe, yritä uudelleen?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Ei voida asentaa ilman runtime kirjastoa, yritä uudelleen?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -93,7 +93,7 @@ msgstr ""
|
||||
"Et voi asentaa tätä vanhan asennuksen päälle. \\n\\nPaina `OK` poistaaksesi "
|
||||
"edellisen version tai paina `Peruuta` peruuttaaksesi tämän päivityksen."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Asetuksiasi ja tietojasi ei poisteta."
|
||||
|
||||
|
||||
@@ -7,33 +7,33 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-03-21 08:58+0000\n"
|
||||
"Last-Translator: Fred <88com88@gmail.com>\n"
|
||||
"Language-Team: French <fr@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-22 06:58+0000\n"
|
||||
"X-Generator: Launchpad (build 18334)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Afficher les notes de version"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr "Démarrer SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Soutenez le projet, faites un don !"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Merci de fermer \"SABnzbd.exe\" avant l'installation"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
@@ -42,31 +42,31 @@ msgstr ""
|
||||
"nSi vous exécutez SABnzbd en tant que service, vous devez mettre à jour les "
|
||||
"paramètres du service."
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Ceci désinstallera SABnzbd de votre système"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Lancer au démarrage"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Icône sur le Bureau"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "Association des fichiers NZB"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Supprimer le programme"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Supprimer les paramètres"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -74,19 +74,19 @@ msgstr ""
|
||||
"Ce système nécessite que la bibliothèque d'exécution Microsoft vc90 soit "
|
||||
"installée en premier. Voulez-vous le faire maintenant?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Téléchargement de Microsoft runtime installer..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Erreur de téléchargement, réessayer ?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Impossible d'installer sans moteur d'exécution, réessayer?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -95,7 +95,7 @@ msgstr ""
|
||||
"pour supprimer la version précédente ou `Annuler` pour annuler cette mise à "
|
||||
"niveau."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Vos paramètres et données seront conservés."
|
||||
|
||||
|
||||
@@ -7,33 +7,33 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-05-06 09:07+0000\n"
|
||||
"Last-Translator: ION IL <Unknown>\n"
|
||||
"Language-Team: Hebrew <he@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-05-07 05:39+0000\n"
|
||||
"X-Generator: Launchpad (build 18366)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "הראה הערות שחרור"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr "התחל את SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "תמוך במיזם, תרום!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "אנא סגור את \"SABnzbd.exe\" תחילה"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
@@ -41,31 +41,31 @@ msgstr ""
|
||||
"ספרית ההתקנה השתנתה (עכשיו ב-\"Program Files\"). \\nאם תריץ את SABnzbd בתור "
|
||||
"שירות, אתה צריך לעדכן את הגדרות השירות."
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "זה יסיר את SABnzbd ממערכתך"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "הפעל באתחול"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "צלמית שולחן עבודה"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "שיוך קבצי NZB"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "מחק תכנית"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "מחק הגדרות"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -73,19 +73,19 @@ msgstr ""
|
||||
"מערכת זו דורשת את ספרית זמן-אמת VC90 של Microsoft שתהיה מותקנת תחילה. האם "
|
||||
"ברצונך להתקין אותה כעת?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "מוריד מתקין זמן-אמת של Microsoft..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "שגיאת הורדה, לנסות שוב?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "לא ניתן להתקין ללא ספרית זמן-אמת, לנסות שוב?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -93,6 +93,6 @@ msgstr ""
|
||||
"אינך יכול לדרוס התקנה קיימת.\\n\\nלחץ על `אישור` כדי להסיר את הגרסה הקודמת "
|
||||
"או על `ביטול` כדי לבטל שדרוג זה."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "ההגדרות והנתונים שלך יישמרו."
|
||||
|
||||
@@ -7,63 +7,63 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: Norwegian Bokmal <nb@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-19 06:37+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Vis versjonsmerknader"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Støtt prosjektet, donèr!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Vennligst lukk \"SABnzbd.exe\" først"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Dette vil avinstallere SABnzbd fra ditt system"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Kjør ved oppstart"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Skrivebordsikon"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "NZB-filassosiering"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Fjern program"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Slett innstillinger"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -71,19 +71,19 @@ msgstr ""
|
||||
"Dette sytemet krever at Microsoft runtime library VC90 er installert først. "
|
||||
"Ønsker du å gjøre dette nå?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Laster ned Microsoft runtime installer..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Nedlasting feilet, prøve på nytt?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Kan ikke installere uten runtime library, prøve på nytt?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -92,7 +92,7 @@ msgstr ""
|
||||
"fjerne tidligere installasjon, eller 'Avbryt' for å avbryte denne "
|
||||
"oppgraderingen."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Dine innstillinger og data vil bli tatt vare på."
|
||||
|
||||
|
||||
@@ -7,33 +7,33 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-03-19 09:47+0000\n"
|
||||
"Last-Translator: Safihre <safihre@sabnzbd.org>\n"
|
||||
"Language-Team: Dutch <nl@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-20 06:21+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Toon opmerkingen bij deze uitgave"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr "Start SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Steun het project, doneer!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Sluit \"SABnzbd.exe\" eerst af"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
@@ -42,31 +42,31 @@ msgstr ""
|
||||
"SABnzbd als een service draait, zul je de service instellingen moeten "
|
||||
"aanpassen."
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Dit verwijdert SABnzbd van je systeem"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Starten met Windows"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Bureaubladpictogram"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "NZB-bestanden openen met SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Programma verwijderen"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Verwijder alle instellingen"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -74,19 +74,19 @@ msgstr ""
|
||||
"Op dit systeem moeten eerst de Microsoft runtime bibliotheek VC90 "
|
||||
"geïnstalleerd worden. Wilt u dat nu doen?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Downloaden van de Microsoft bibliotheek"
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Download mislukt, opnieuw proberen?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Installeren heeft geen zin zonder de bibliotheek, opnieuw proberen?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -94,7 +94,7 @@ msgstr ""
|
||||
"U kunt geen bestaande installatie overschrijven.\\n\\nKlik op `OK` om de "
|
||||
"vorige versie te verwijderen of op `Annuleren` om te stoppen."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Je instellingen en bestanden blijven behouden."
|
||||
|
||||
|
||||
@@ -7,63 +7,63 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Tomasz 'Zen' Napierala <tomasz@napierala.org>\n"
|
||||
"Language-Team: Polish <pl@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-19 06:37+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Pokaż informacje o wydaniu"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Wspomóż projekt!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Najpierw zamknij SABnzbd.exe"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "To odinstaluje SABnzbd z systemu"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Uruchom wraz z systemem"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Ikona pulpitu"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "powiązanie pliku NZB"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Usuń program"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Skasuj obecne ustawienia"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -71,19 +71,19 @@ msgstr ""
|
||||
"Ten system wymaga najpierw zainstalowania bibliotek Microsoft VC90. Czy "
|
||||
"chcesz wykonać teraz instalację?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Pobieranie instalatora bibliotek Microsoft..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Problem z pobieraniem, spróbować ponownie?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Nie można wykonać instalacji bez bibliotek, spróbować ponownie?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -91,7 +91,7 @@ msgstr ""
|
||||
"Nie można nadpisać istniejącej instalacji. \\n\\n Naciśnij `OK`, aby usunąć "
|
||||
"poprzednia wersję lub `Anuluj` aby anulować aktualizację."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Twoje ustawienia i dane zostaną zachowane."
|
||||
|
||||
|
||||
@@ -7,63 +7,63 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: lrrosa <Unknown>\n"
|
||||
"Language-Team: Brazilian Portuguese <pt_BR@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-19 06:37+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Mostrar Notas de Lançamento"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Apoie o projeto. Faça uma doação!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Por favor, feche \"SABnzbd.exe\" primeiro"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Isso irá desinstalar SABnzbd de seu sistema"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Executar na inicialização"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Ícone na Área de Trabalho"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "Associação com Arquivos NZB"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Excluir o Programa"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Apagar Configurações"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -71,19 +71,19 @@ msgstr ""
|
||||
"Este sistema precisa que a biblioteca runtime Microsoft VC90 seja instalada "
|
||||
"antes. Você quer fazer isso agora?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Baixando o instalador runtime da Microsoft ..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Houve um erro de download. Quer tentar novamente?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Não é possível instalar sem a biblioteca runtime. Quer repetir?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -91,7 +91,7 @@ msgstr ""
|
||||
"Você não pode substituir uma instalação existente. \\n\\nClique `OK` para "
|
||||
"remover a versão anterior ou `Cancelar` para cancelar esta atualização."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Suas configurações e os dados serão preservados."
|
||||
|
||||
|
||||
@@ -7,63 +7,63 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: nicusor <Unknown>\n"
|
||||
"Language-Team: Romanian <ro@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-19 06:37+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Arată Notele de Publicare"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Susţine proiectul, Donează!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Închideţi mai întâi \"SABnzbd.exe\""
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Acest lucru va dezinstala SABnzbd din sistem"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Executare la pornire"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Icoană Desktop"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "Asociere cu Fişierele NZB"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Şterge Program"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Ştergeţi Setări"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -71,19 +71,19 @@ msgstr ""
|
||||
"Acest sistem necesită librăria Microsoft VC90 instalată. Dortiți să faceți "
|
||||
"asta acum ?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Descărcare rutină instalare Microsoft..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Eroare descărcare, încerc din nou?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Nu pot instala fără rutină librărie, încerc din nou?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -91,7 +91,7 @@ msgstr ""
|
||||
"Nu puteți suprascrie instalarea existentă. \\n\\nClick `OK` pentru a elimina "
|
||||
"versiunea anterioară sau `Anulare` pentru a anula actualizarea."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Setările şi informaţiile vor fi salvate."
|
||||
|
||||
|
||||
@@ -7,63 +7,63 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Pavel Maryanov <Unknown>\n"
|
||||
"Language-Team: Russian <gnu@mx.ru>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-19 06:37+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Показать заметки о выпуске"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Поддержите проект. Сделайте пожертвование!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Завершите сначала работу процесса SABnzbd.exe"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Приложение SABnzbd будет удалено из вашей системы"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Запускать вместе с системой"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Значок на рабочем столе"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "Ассоциировать с файлами NZB"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Удалить программу"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Удалить параметры"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -71,21 +71,21 @@ msgstr ""
|
||||
"Для этой системы сначала необходимо установить библиотеку времени выполнения "
|
||||
"Microsoft VC90. Сделать это сейчас?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Загрузка программы установки Microsoft..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Ошибка загрузки. Повторить попытку?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr ""
|
||||
"Не удаётся выполнить установку без библиотеки времени выполнения. Повторить "
|
||||
"попытку?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -94,6 +94,6 @@ msgstr ""
|
||||
"удалить предыдущую версию, нажмите кнопку «ОК». Чтобы отменить обновление, "
|
||||
"нажмите кнопку «Отмена»."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Ваши параметры и данные будут сохранены."
|
||||
|
||||
@@ -7,64 +7,64 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Ozzii <Unknown>\n"
|
||||
"Language-Team: Launchpad Serbian Translators\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-19 06:37+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
"Language: sr\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Прикажи белешке о издању"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Подржите пројекат, дајте добровољан прилог!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Прво затворите „SABnzbd.exe“"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Ово ће уклонити САБнзбд са вашег система"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Покрени са системом"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Иконица радне површи"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "Придруживање НЗБ датотеке"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Обриши програм"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Обриши подешавања"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -72,19 +72,19 @@ msgstr ""
|
||||
"Овај систем захтева да буде прво инсталирана Мајкрософтова извршна "
|
||||
"библиотека „VC90“. Да ли желите то да урадите?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Преузимам Мајкрософтов извршни програм за инсталацију..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Грешка у преузимању, да поновим?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Не могу да инсталирам без извршне библиотеке, да поновим?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -92,7 +92,7 @@ msgstr ""
|
||||
"Не можете да препишете постојећу инсталацију. \\n\\nПритисните „У реду“ да "
|
||||
"уклоните претходно издање или „Откажи“ да поништите ову надоградњу."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Ваша подешавања и подаци биће сачувани."
|
||||
|
||||
|
||||
@@ -7,63 +7,63 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2013-05-05 14:50+0000\n"
|
||||
"Last-Translator: Andreas Lindberg <andypandyswe@gmail.com>\n"
|
||||
"Language-Team: Swedish <sv@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-03-19 06:37+0000\n"
|
||||
"X-Generator: Launchpad (build 18332)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "Visa releasenoteringar"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "Donera och stöd detta projekt!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "Var vänlig stäng \"SABnzbd.exe\" först"
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr ""
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "Detta kommer att avinstallera SABnzbd från systemet"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "Kör vid uppstart"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "Skrivbordsikon"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "NZB Filassosication"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "Radera programmet"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "Radera inställningar"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
@@ -71,19 +71,19 @@ msgstr ""
|
||||
"Detta system kräver att Microsofts runtimebibliotek VC90 är installerat. "
|
||||
"Vill du göra detta nu?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "Laddar ned Microsofts runtimeinstaller..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "Misslyckat nedladdningsförsök, försök igen?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "Kan inte installera utan runtimebibliotek, försök igen?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
@@ -92,7 +92,7 @@ msgstr ""
|
||||
"avinstallera tidigare version eller 'Avbryt' för att avbryta denna "
|
||||
"uppgradering."
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "Dina inställningar och ditt data kommer att bevaras."
|
||||
|
||||
|
||||
@@ -7,87 +7,87 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: sabnzbd\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
|
||||
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
|
||||
"PO-Revision-Date: 2017-05-28 17:17+0000\n"
|
||||
"Last-Translator: ninjai <ninjai.us@gmail.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <zh_CN@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2017-05-29 06:02+0000\n"
|
||||
"X-Generator: Launchpad (build 18391)\n"
|
||||
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi:473
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "显示版本说明"
|
||||
|
||||
#: NSIS_Installer.nsi:475
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr "启动 SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi:477
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "支持该项目,捐助!"
|
||||
|
||||
#: NSIS_Installer.nsi:479
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "请先关闭 \"SABnzbd.exe\""
|
||||
|
||||
#: NSIS_Installer.nsi:481
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
|
||||
"run SABnzbd as a service, you need to update the service settings."
|
||||
msgstr "安装目录已更改(现在位于 \"Program Files\" 目录)。\\n如果以服务模式运行,你需要更新相应服务的设置。"
|
||||
|
||||
#: NSIS_Installer.nsi:483
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "这将从您的系统中卸载 SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi:485
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "启动时运行"
|
||||
|
||||
#: NSIS_Installer.nsi:487
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "桌面图标"
|
||||
|
||||
#: NSIS_Installer.nsi:489
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "NZB 文件关联"
|
||||
|
||||
#: NSIS_Installer.nsi:491
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "删除程序"
|
||||
|
||||
#: NSIS_Installer.nsi:493
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "删除设置"
|
||||
|
||||
#: NSIS_Installer.nsi:495
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
msgstr "该系统需要先安装 Microsoft 运行时库 VC90。是否希望立即安装?"
|
||||
|
||||
#: NSIS_Installer.nsi:497
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "正在下载 Microsoft 运行时安装程序..."
|
||||
|
||||
#: NSIS_Installer.nsi:499
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "下载出错,重试?"
|
||||
|
||||
#: NSIS_Installer.nsi:501
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "没有运行时库无法安装,重试?"
|
||||
|
||||
#: NSIS_Installer.nsi:503
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
msgstr "不可以覆盖安装。\\n\\n点击“确定”可移除旧版,或点击“取消”取消升级。"
|
||||
|
||||
#: NSIS_Installer.nsi:505
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "您的设置及数据将会保留。"
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ import sabnzbd.cfg as cfg
|
||||
import sabnzbd.database
|
||||
import sabnzbd.lang as lang
|
||||
import sabnzbd.api
|
||||
import sabnzbd.directunpacker as directunpacker
|
||||
from sabnzbd.decorators import synchronized, notify_downloader
|
||||
from sabnzbd.constants import NORMAL_PRIORITY, VALID_ARCHIVES, GIGI, \
|
||||
REPAIR_REQUEST, QUEUE_FILE_NAME, QUEUE_VERSION, QUEUE_FILE_TMPL
|
||||
@@ -157,7 +158,7 @@ WEBUI_READY = False
|
||||
LAST_WARNING = None
|
||||
LAST_ERROR = None
|
||||
EXTERNAL_IPV6 = False
|
||||
LAST_HISTORY_UPDATE = time.time()
|
||||
LAST_HISTORY_UPDATE = 1
|
||||
|
||||
# Performance measure for dashboard
|
||||
PYSTONE_SCORE = 0
|
||||
@@ -383,6 +384,8 @@ def halt():
|
||||
|
||||
sabnzbd.zconfig.remove_server()
|
||||
|
||||
sabnzbd.directunpacker.abort_all()
|
||||
|
||||
rss.stop()
|
||||
|
||||
logging.debug('Stopping URLGrabber')
|
||||
@@ -842,7 +845,7 @@ def keep_awake():
|
||||
def CheckFreeSpace():
|
||||
""" Check if enough disk space is free, if not pause downloader and send email """
|
||||
if cfg.download_free() and not sabnzbd.downloader.Downloader.do.paused:
|
||||
if misc.diskspace(cfg.download_dir.get_path(), force=True)[1] < cfg.download_free.get_float() / GIGI:
|
||||
if misc.diskspace(force=True)['download_dir'][1] < cfg.download_free.get_float() / GIGI:
|
||||
logging.warning(T('Too little diskspace forcing PAUSE'))
|
||||
# Pause downloader, but don't save, since the disk is almost full!
|
||||
Downloader.do.pause(save=False)
|
||||
@@ -885,9 +888,9 @@ def save_data(data, _id, path, do_pickle=True, silent=False):
|
||||
with open(path, 'wb') as data_file:
|
||||
if do_pickle:
|
||||
if cfg.use_pickle():
|
||||
cPickle.dump(data, data_file)
|
||||
else:
|
||||
pickle.dump(data, data_file)
|
||||
else:
|
||||
cPickle.dump(data, data_file)
|
||||
else:
|
||||
data_file.write(data)
|
||||
break
|
||||
@@ -1180,3 +1183,11 @@ def test_ipv6():
|
||||
except:
|
||||
logging.debug('Test IPv6: Problem during IPv6 connect. Disabling IPv6. Reason: %s', sys.exc_info()[0])
|
||||
return False
|
||||
|
||||
|
||||
def history_updated():
|
||||
""" To make sure we always have a fresh history """
|
||||
sabnzbd.LAST_HISTORY_UPDATE += 1
|
||||
# Never go over the limit
|
||||
if sabnzbd.LAST_HISTORY_UPDATE+1 >= sys.maxint:
|
||||
sabnzbd.LAST_HISTORY_UPDATE = 1
|
||||
|
||||
@@ -484,13 +484,14 @@ def _api_history(name, output, kwargs):
|
||||
value = kwargs.get('value', '')
|
||||
start = int_conv(kwargs.get('start'))
|
||||
limit = int_conv(kwargs.get('limit'))
|
||||
last_history_update = int_conv(kwargs.get('last_history_update', 0))
|
||||
search = kwargs.get('search')
|
||||
failed_only = kwargs.get('failed_only')
|
||||
categories = kwargs.get('category')
|
||||
last_history_update = kwargs.get('last_history_update', 0)
|
||||
|
||||
|
||||
# Do we need to send anything?
|
||||
if int(last_history_update) == int(sabnzbd.LAST_HISTORY_UPDATE):
|
||||
if last_history_update == sabnzbd.LAST_HISTORY_UPDATE:
|
||||
return report(output, keyword='history', data=False)
|
||||
|
||||
if categories and not isinstance(categories, list):
|
||||
@@ -510,15 +511,13 @@ def _api_history(name, output, kwargs):
|
||||
history_db.remove_failed(search)
|
||||
if special in ('all', 'completed'):
|
||||
history_db.remove_completed(search)
|
||||
# Update the last check time
|
||||
sabnzbd.LAST_HISTORY_UPDATE = time.time()
|
||||
sabnzbd.history_updated()
|
||||
return report(output)
|
||||
elif value:
|
||||
jobs = value.split(',')
|
||||
for job in jobs:
|
||||
del_hist_job(job, del_files)
|
||||
# Update the last check time
|
||||
sabnzbd.LAST_HISTORY_UPDATE = time.time()
|
||||
sabnzbd.history_updated()
|
||||
return report(output)
|
||||
else:
|
||||
return report(output, _MSG_NO_VALUE)
|
||||
@@ -532,7 +531,7 @@ def _api_history(name, output, kwargs):
|
||||
search=search, failed_only=failed_only,
|
||||
categories=categories,
|
||||
output=output)
|
||||
history['last_history_update'] = int(sabnzbd.LAST_HISTORY_UPDATE)
|
||||
history['last_history_update'] = sabnzbd.LAST_HISTORY_UPDATE
|
||||
history['version'] = sabnzbd.__version__
|
||||
return report(output, keyword='history', data=remove_callable(history))
|
||||
else:
|
||||
@@ -1347,6 +1346,8 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
slot['sizeleft'] = format_bytes(bytesleft)
|
||||
slot['percentage'] = "%s" % (int(((mb - mbleft) / mb) * 100)) if mb != mbleft else '0'
|
||||
slot['missing'] = pnfo.missing
|
||||
slot['mbmissing'] = "%.2f" % (pnfo.bytes_missing / MEBI)
|
||||
slot['direct_unpack'] = pnfo.direct_unpack
|
||||
if not output:
|
||||
slot['mb_fmt'] = locale.format('%d', int(mb), True)
|
||||
slot['mbdone_fmt'] = locale.format('%d', int(mb - mbleft), True)
|
||||
@@ -1517,7 +1518,6 @@ def options_list(output):
|
||||
return report(output, keyword='options', data={
|
||||
'yenc': sabnzbd.decoder.HAVE_YENC,
|
||||
'par2': sabnzbd.newsunpack.PAR2_COMMAND,
|
||||
'par2c': sabnzbd.newsunpack.PAR2C_COMMAND,
|
||||
'multipar': sabnzbd.newsunpack.MULTIPAR_COMMAND,
|
||||
'rar': sabnzbd.newsunpack.RAR_COMMAND,
|
||||
'zip': sabnzbd.newsunpack.ZIP_COMMAND,
|
||||
@@ -1622,8 +1622,7 @@ def build_header(webdir='', output=None):
|
||||
if speed_limit_abs <= 0:
|
||||
speed_limit_abs = ''
|
||||
|
||||
disk_total1, disk_free1 = diskspace(cfg.download_dir.get_path())
|
||||
disk_total2, disk_free2 = diskspace(cfg.complete_dir.get_path())
|
||||
diskspace_info = diskspace()
|
||||
|
||||
header = {}
|
||||
|
||||
@@ -1660,12 +1659,12 @@ def build_header(webdir='', output=None):
|
||||
header['pause_int'] = scheduler.pause_int()
|
||||
header['paused_all'] = sabnzbd.PAUSED_ALL
|
||||
|
||||
header['diskspace1'] = "%.2f" % disk_free1
|
||||
header['diskspace2'] = "%.2f" % disk_free2
|
||||
header['diskspace1_norm'] = to_units(disk_free1 * GIGI)
|
||||
header['diskspace2_norm'] = to_units(disk_free2 * GIGI)
|
||||
header['diskspacetotal1'] = "%.2f" % disk_total1
|
||||
header['diskspacetotal2'] = "%.2f" % disk_total2
|
||||
header['diskspace1'] = "%.2f" % diskspace_info['download_dir'][1]
|
||||
header['diskspace2'] = "%.2f" % diskspace_info['complete_dir'][1]
|
||||
header['diskspace1_norm'] = to_units(diskspace_info['download_dir'][1] * GIGI)
|
||||
header['diskspace2_norm'] = to_units(diskspace_info['complete_dir'][1] * GIGI)
|
||||
header['diskspacetotal1'] = "%.2f" % diskspace_info['download_dir'][0]
|
||||
header['diskspacetotal2'] = "%.2f" % diskspace_info['complete_dir'][0]
|
||||
header['loadavg'] = loadavg()
|
||||
header['speedlimit'] = "{1:0.{0}f}".format(int(speed_limit % 1 > 0), speed_limit)
|
||||
header['speedlimit_abs'] = "%s" % speed_limit_abs
|
||||
|
||||
@@ -30,8 +30,8 @@ import hashlib
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.misc import get_filepath, sanitize_filename, get_unique_filename, renamer, \
|
||||
set_permissions, flag_file, long_path, clip_path, has_win_device, get_all_passwords
|
||||
from sabnzbd.constants import QCHECK_FILE, Status
|
||||
set_permissions, long_path, clip_path, has_win_device, get_all_passwords
|
||||
from sabnzbd.constants import Status
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.articlecache import ArticleCache
|
||||
from sabnzbd.postproc import PostProcessor
|
||||
@@ -70,18 +70,16 @@ class Assembler(Thread):
|
||||
|
||||
if nzf:
|
||||
sabnzbd.CheckFreeSpace()
|
||||
# We allow win_devices because otherwise par2cmdline fails to repair
|
||||
filename = sanitize_filename(nzf.filename, allow_win_devices=True)
|
||||
|
||||
filename = sanitize_filename(nzf.filename)
|
||||
nzf.filename = filename
|
||||
|
||||
dupe = nzo.check_for_dupe(nzf)
|
||||
|
||||
filepath = get_filepath(long_path(cfg.download_dir.get_path()), nzo, filename)
|
||||
|
||||
if filepath:
|
||||
logging.info('Decoding %s %s', filepath, nzf.type)
|
||||
try:
|
||||
filepath = _assemble(nzf, filepath, dupe)
|
||||
filepath = self.assemble(nzf, filepath, dupe)
|
||||
except IOError, (errno, strerror):
|
||||
# If job was deleted, ignore error
|
||||
if not nzo.is_gone():
|
||||
@@ -100,7 +98,7 @@ class Assembler(Thread):
|
||||
nzf.remove_admin()
|
||||
setname = nzf.setname
|
||||
if nzf.is_par2 and (nzo.md5packs.get(setname) is None):
|
||||
pack = GetMD5Hashes(filepath)[0]
|
||||
pack = self.parse_par2_file(filepath, nzo.md5of16k)
|
||||
if pack:
|
||||
nzo.md5packs[setname] = pack
|
||||
logging.debug('Got md5pack for set %s', setname)
|
||||
@@ -113,15 +111,15 @@ class Assembler(Thread):
|
||||
rar_encrypted, unwanted_file = check_encrypted_and_unwanted_files(nzo, filepath)
|
||||
if rar_encrypted:
|
||||
if cfg.pause_on_pwrar() == 1:
|
||||
logging.warning(T('WARNING: Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'), nzo.final_name)
|
||||
logging.warning(remove_warning_label(T('WARNING: Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)')), nzo.final_name)
|
||||
nzo.pause()
|
||||
else:
|
||||
logging.warning(T('WARNING: Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'), nzo.final_name)
|
||||
logging.warning(remove_warning_label(T('WARNING: Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)')), nzo.final_name)
|
||||
nzo.fail_msg = T('Aborted, encryption detected')
|
||||
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
|
||||
|
||||
if unwanted_file:
|
||||
logging.warning(T('WARNING: In "%s" unwanted extension in RAR file. Unwanted file is %s '), nzo.final_name, unwanted_file)
|
||||
logging.warning(remove_warning_label(T('WARNING: In "%s" unwanted extension in RAR file. Unwanted file is %s ')), nzo.final_name, unwanted_file)
|
||||
logging.debug(T('Unwanted extension is in rar file %s'), filepath)
|
||||
if cfg.action_on_unwanted_extensions() == 1 and nzo.unwanted_ext == 0:
|
||||
logging.debug('Unwanted extension ... pausing')
|
||||
@@ -134,54 +132,104 @@ class Assembler(Thread):
|
||||
|
||||
filter, reason = nzo_filtered_by_rating(nzo)
|
||||
if filter == 1:
|
||||
logging.warning(T('WARNING: Paused job "%s" because of rating (%s)'), nzo.final_name, reason)
|
||||
logging.warning(remove_warning_label(T('WARNING: Paused job "%s" because of rating (%s)')), nzo.final_name, reason)
|
||||
nzo.pause()
|
||||
elif filter == 2:
|
||||
logging.warning(T('WARNING: Aborted job "%s" because of rating (%s)'), nzo.final_name, reason)
|
||||
logging.warning(remove_warning_label(T('WARNING: Aborted job "%s" because of rating (%s)')), nzo.final_name, reason)
|
||||
nzo.fail_msg = T('Aborted, rating filter matched (%s)') % reason
|
||||
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
|
||||
|
||||
if rarfile.is_rarfile(filepath):
|
||||
nzo.add_to_direct_unpacker(nzf)
|
||||
|
||||
else:
|
||||
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo.nzo_id, add_to_history=False, cleanup=False)
|
||||
PostProcessor.do.process(nzo)
|
||||
|
||||
def assemble(self, nzf, path, dupe):
|
||||
""" Assemble a NZF from its table of articles """
|
||||
if os.path.exists(path):
|
||||
unique_path = get_unique_filename(path)
|
||||
if dupe:
|
||||
path = unique_path
|
||||
else:
|
||||
renamer(path, unique_path)
|
||||
|
||||
def _assemble(nzf, path, dupe):
|
||||
if os.path.exists(path):
|
||||
unique_path = get_unique_filename(path)
|
||||
if dupe:
|
||||
path = unique_path
|
||||
else:
|
||||
renamer(path, unique_path)
|
||||
md5 = hashlib.md5()
|
||||
fout = open(path, 'ab')
|
||||
decodetable = nzf.decodetable
|
||||
|
||||
md5 = hashlib.md5()
|
||||
fout = open(path, 'ab')
|
||||
decodetable = nzf.decodetable
|
||||
for articlenum in decodetable:
|
||||
# Break if deleted during writing
|
||||
if nzf.nzo.status is Status.DELETED:
|
||||
break
|
||||
|
||||
for articlenum in decodetable:
|
||||
# Break if deleted during writing
|
||||
if nzf.nzo.status is Status.DELETED:
|
||||
break
|
||||
# Sleep to allow decoder/assembler switching
|
||||
sleep(0.0001)
|
||||
article = decodetable[articlenum]
|
||||
|
||||
# Sleep to allow decoder/assembler switching
|
||||
sleep(0.0001)
|
||||
article = decodetable[articlenum]
|
||||
data = ArticleCache.do.load_article(article)
|
||||
|
||||
data = ArticleCache.do.load_article(article)
|
||||
if not data:
|
||||
logging.info(T('%s missing'), article)
|
||||
else:
|
||||
# yenc data already decoded, flush it out
|
||||
fout.write(data)
|
||||
md5.update(data)
|
||||
|
||||
if not data:
|
||||
logging.info(T('%s missing'), article)
|
||||
else:
|
||||
# yenc data already decoded, flush it out
|
||||
fout.write(data)
|
||||
md5.update(data)
|
||||
fout.flush()
|
||||
fout.close()
|
||||
set_permissions(path)
|
||||
nzf.md5sum = md5.digest()
|
||||
del md5
|
||||
|
||||
fout.flush()
|
||||
fout.close()
|
||||
set_permissions(path)
|
||||
nzf.md5sum = md5.digest()
|
||||
del md5
|
||||
return path
|
||||
|
||||
return path
|
||||
def parse_par2_file(self, fname, table16k):
|
||||
""" Get the hash table and the first-16k hash table from a PAR2 file
|
||||
Return as dictionary, indexed on names or hashes for the first-16 table
|
||||
For a full description of the par2 specification, visit:
|
||||
http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html
|
||||
"""
|
||||
table = {}
|
||||
duplicates16k = []
|
||||
|
||||
try:
|
||||
f = open(fname, 'rb')
|
||||
except:
|
||||
return table
|
||||
|
||||
try:
|
||||
header = f.read(8)
|
||||
while header:
|
||||
name, hash, hash16k = parse_par2_file_packet(f, header)
|
||||
if name:
|
||||
table[name] = hash
|
||||
if hash16k not in table16k:
|
||||
table16k[hash16k] = name
|
||||
else:
|
||||
# Not unique, remove to avoid false-renames
|
||||
duplicates16k.append(hash16k)
|
||||
|
||||
header = f.read(8)
|
||||
|
||||
except (struct.error, IndexError):
|
||||
logging.info('Cannot use corrupt par2 file for QuickCheck, "%s"', fname)
|
||||
table = {}
|
||||
except:
|
||||
logging.debug('QuickCheck parser crashed in file %s', fname)
|
||||
logging.info('Traceback: ', exc_info=True)
|
||||
table = {}
|
||||
f.close()
|
||||
|
||||
# Have to remove duplicates at the end to make sure
|
||||
# no trace is left in case of multi-duplicates
|
||||
for hash16k in duplicates16k:
|
||||
if hash16k in table16k:
|
||||
old_name = table16k.pop(hash16k)
|
||||
logging.debug('Par2-16k signature of %s not unique, discarding', old_name)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def file_has_articles(nzf):
|
||||
@@ -199,47 +247,10 @@ def file_has_articles(nzf):
|
||||
return has
|
||||
|
||||
|
||||
# For a full description of the par2 specification, visit:
|
||||
# http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html
|
||||
|
||||
def GetMD5Hashes(fname, force=False):
|
||||
""" Get the hash table from a PAR2 file
|
||||
Return as dictionary, indexed on names and True for utf8-encoded names
|
||||
"""
|
||||
new_encoding = True
|
||||
table = {}
|
||||
if force or not flag_file(os.path.split(fname)[0], QCHECK_FILE):
|
||||
try:
|
||||
f = open(fname, 'rb')
|
||||
except:
|
||||
return table, new_encoding
|
||||
|
||||
new_encoding = False
|
||||
try:
|
||||
header = f.read(8)
|
||||
while header:
|
||||
name, hash = ParseFilePacket(f, header)
|
||||
new_encoding |= is_utf8(name)
|
||||
if name:
|
||||
table[name] = hash
|
||||
header = f.read(8)
|
||||
|
||||
except (struct.error, IndexError):
|
||||
logging.info('Cannot use corrupt par2 file for QuickCheck, "%s"', fname)
|
||||
table = {}
|
||||
except:
|
||||
logging.debug('QuickCheck parser crashed in file %s', fname)
|
||||
logging.info('Traceback: ', exc_info=True)
|
||||
table = {}
|
||||
|
||||
f.close()
|
||||
return table, new_encoding
|
||||
|
||||
|
||||
def ParseFilePacket(f, header):
|
||||
def parse_par2_file_packet(f, header):
|
||||
""" Look up and analyze a FileDesc package """
|
||||
|
||||
nothing = None, None
|
||||
nothing = None, None, None
|
||||
|
||||
if header != 'PAR2\0PKT':
|
||||
return nothing
|
||||
@@ -271,8 +282,9 @@ def ParseFilePacket(f, header):
|
||||
for offset in range(0, len, 8):
|
||||
if data[offset:offset + 16] == "PAR 2.0\0FileDesc":
|
||||
hash = data[offset + 32:offset + 48]
|
||||
hash16k = data[offset + 48:offset + 64]
|
||||
filename = data[offset + 72:].strip('\0')
|
||||
return filename, hash
|
||||
return filename, hash, hash16k
|
||||
|
||||
return nothing
|
||||
|
||||
@@ -429,3 +441,11 @@ def rating_filtered(rating, filename, abort):
|
||||
if any(check_keyword(k) for k in keywords.split(',')):
|
||||
return T('keywords')
|
||||
return None
|
||||
|
||||
|
||||
def remove_warning_label(msg):
|
||||
""" Standardize errors by removing obsolete
|
||||
"WARNING:" part in all languages """
|
||||
if ':' in msg:
|
||||
return msg.split(':')[1]
|
||||
return msg
|
||||
|
||||
@@ -74,6 +74,7 @@ email_endjob = OptionNumber('misc', 'email_endjob', 0, 0, 2)
|
||||
email_full = OptionBool('misc', 'email_full', False)
|
||||
email_dir = OptionDir('misc', 'email_dir', create=True)
|
||||
email_rss = OptionBool('misc', 'email_rss', False)
|
||||
email_cats = OptionList('misc', 'email_cats', ['*'])
|
||||
|
||||
version_check = OptionNumber('misc', 'check_new_rel', 1)
|
||||
autobrowser = OptionBool('misc', 'auto_browser', True)
|
||||
@@ -101,9 +102,7 @@ par_option = OptionStr('misc', 'par_option', '', validation=no_nonsense)
|
||||
nice = OptionStr('misc', 'nice', '', validation=no_nonsense)
|
||||
ionice = OptionStr('misc', 'ionice', '', validation=no_nonsense)
|
||||
ignore_wrong_unrar = OptionBool('misc', 'ignore_wrong_unrar', False)
|
||||
par2_multicore = OptionBool('misc', 'par2_multicore', True)
|
||||
multipar = OptionBool('misc', 'multipar', sabnzbd.WIN32)
|
||||
allow_streaming = OptionBool('misc', 'allow_streaming', False)
|
||||
pre_check = OptionBool('misc', 'pre_check', False)
|
||||
fail_hopeless_jobs = OptionBool('misc', 'fail_hopeless_jobs', True)
|
||||
req_completion_rate = OptionNumber('misc', 'req_completion_rate', 100.2, 100, 200)
|
||||
@@ -142,11 +141,12 @@ backup_for_duplicates = OptionBool('misc', 'backup_for_duplicates', True)
|
||||
|
||||
ignore_samples = OptionBool('misc', 'ignore_samples', False)
|
||||
auto_sort = OptionBool('misc', 'auto_sort', False)
|
||||
direct_unpack = OptionBool('misc', 'direct_unpack', False)
|
||||
direct_unpack_tested = OptionBool('misc', 'direct_unpack_tested', False)
|
||||
propagation_delay = OptionNumber('misc', 'propagation_delay', 0)
|
||||
folder_rename = OptionBool('misc', 'folder_rename', True)
|
||||
folder_max_length = OptionNumber('misc', 'folder_max_length', DEF_FOLDER_MAX, 20, 65000)
|
||||
pause_on_pwrar = OptionNumber('misc', 'pause_on_pwrar', 1)
|
||||
enable_meta = OptionBool('misc', 'enable_meta', True)
|
||||
|
||||
safe_postproc = OptionBool('misc', 'safe_postproc', True)
|
||||
empty_postproc = OptionBool('misc', 'empty_postproc', False)
|
||||
@@ -172,7 +172,7 @@ movie_categories = OptionList('misc', 'movie_categories', ['movies'])
|
||||
|
||||
enable_date_sorting = OptionBool('misc', 'enable_date_sorting', False)
|
||||
date_sort_string = OptionStr('misc', 'date_sort_string')
|
||||
date_categories = OptionStr('misc', 'date_categories', ['tv'])
|
||||
date_categories = OptionList('misc', 'date_categories', ['tv'])
|
||||
|
||||
configlock = OptionBool('misc', 'config_lock', 0)
|
||||
|
||||
@@ -245,6 +245,7 @@ fixed_ports = OptionBool('misc', 'fixed_ports', False)
|
||||
|
||||
# [ncenter]
|
||||
ncenter_enable = OptionBool('ncenter', 'ncenter_enable', sabnzbd.DARWIN)
|
||||
ncenter_cats = OptionList('ncenter', 'ncenter_cats', ['*'])
|
||||
ncenter_prio_startup = OptionBool('ncenter', 'ncenter_prio_startup', True)
|
||||
ncenter_prio_download = OptionBool('ncenter', 'ncenter_prio_download', False)
|
||||
ncenter_prio_pp = OptionBool('ncenter', 'ncenter_prio_pp', False)
|
||||
@@ -259,6 +260,7 @@ ncenter_prio_other = OptionBool('ncenter', 'ncenter_prio_other', False)
|
||||
|
||||
# [acenter]
|
||||
acenter_enable = OptionBool('acenter', 'acenter_enable', sabnzbd.WIN32)
|
||||
acenter_cats = OptionList('acenter', 'acenter_cats', ['*'])
|
||||
acenter_prio_startup = OptionBool('acenter', 'acenter_prio_startup', False)
|
||||
acenter_prio_download = OptionBool('acenter', 'acenter_prio_download', False)
|
||||
acenter_prio_pp = OptionBool('acenter', 'acenter_prio_pp', False)
|
||||
@@ -273,6 +275,7 @@ acenter_prio_other = OptionBool('acenter', 'acenter_prio_other', False)
|
||||
|
||||
# [ntfosd]
|
||||
ntfosd_enable = OptionBool('ntfosd', 'ntfosd_enable', not sabnzbd.WIN32 and not sabnzbd.DARWIN)
|
||||
ntfosd_cats = OptionList('ntfosd', 'ntfosd_cats', ['*'])
|
||||
ntfosd_prio_startup = OptionBool('ntfosd', 'ntfosd_prio_startup', True)
|
||||
ntfosd_prio_download = OptionBool('ntfosd', 'ntfosd_prio_download', False)
|
||||
ntfosd_prio_pp = OptionBool('ntfosd', 'ntfosd_prio_pp', False)
|
||||
@@ -287,6 +290,7 @@ ntfosd_prio_other = OptionBool('ntfosd', 'ntfosd_prio_other', False)
|
||||
|
||||
# [growl]
|
||||
growl_enable = OptionBool('growl', 'growl_enable', False)
|
||||
growl_cats = OptionList('growl', 'growl_cats', ['*'])
|
||||
growl_server = OptionStr('growl', 'growl_server')
|
||||
growl_password = OptionPassword('growl', 'growl_password')
|
||||
growl_prio_startup = OptionBool('growl', 'growl_prio_startup', True)
|
||||
@@ -303,6 +307,7 @@ growl_prio_other = OptionBool('growl', 'growl_prio_other', False)
|
||||
|
||||
# [prowl]
|
||||
prowl_enable = OptionBool('prowl', 'prowl_enable', False)
|
||||
prowl_cats = OptionList('prowl', 'prowl_cats', ['*'])
|
||||
prowl_apikey = OptionStr('prowl', 'prowl_apikey')
|
||||
prowl_prio_startup = OptionNumber('prowl', 'prowl_prio_startup', -3)
|
||||
prowl_prio_download = OptionNumber('prowl', 'prowl_prio_download', -3)
|
||||
@@ -321,6 +326,7 @@ pushover_token = OptionStr('pushover', 'pushover_token')
|
||||
pushover_userkey = OptionStr('pushover', 'pushover_userkey')
|
||||
pushover_device = OptionStr('pushover', 'pushover_device')
|
||||
pushover_enable = OptionBool('pushover', 'pushover_enable')
|
||||
pushover_cats = OptionList('pushover', 'pushover_cats', ['*'])
|
||||
pushover_prio_startup = OptionNumber('pushover', 'pushover_prio_startup', -3)
|
||||
pushover_prio_download = OptionNumber('pushover', 'pushover_prio_download', -2)
|
||||
pushover_prio_pp = OptionNumber('pushover', 'pushover_prio_pp', -3)
|
||||
@@ -335,6 +341,7 @@ pushover_prio_other = OptionNumber('pushover', 'pushover_prio_other', -3)
|
||||
|
||||
# [pushbullet]
|
||||
pushbullet_enable = OptionBool('pushbullet', 'pushbullet_enable')
|
||||
pushbullet_cats = OptionList('pushbullet', 'pushbullet_cats', ['*'])
|
||||
pushbullet_apikey = OptionStr('pushbullet', 'pushbullet_apikey')
|
||||
pushbullet_device = OptionStr('pushbullet', 'pushbullet_device')
|
||||
pushbullet_prio_startup = OptionNumber('pushbullet', 'pushbullet_prio_startup', 0)
|
||||
@@ -351,6 +358,7 @@ pushbullet_prio_other = OptionNumber('pushbullet', 'pushbullet_prio_other', 0)
|
||||
|
||||
# [nscript]
|
||||
nscript_enable = OptionBool('nscript', 'nscript_enable')
|
||||
nscript_cats = OptionList('nscript', 'nscript_cats', ['*'])
|
||||
nscript_script = OptionStr('nscript', 'nscript_script')
|
||||
nscript_parameters = OptionStr('nscript', 'nscript_parameters')
|
||||
nscript_prio_startup = OptionBool('nscript', 'nscript_prio_startup', True)
|
||||
|
||||
@@ -24,6 +24,10 @@ import re
|
||||
import logging
|
||||
import threading
|
||||
import shutil
|
||||
import time
|
||||
import random
|
||||
from hashlib import md5
|
||||
from urlparse import urlparse
|
||||
import sabnzbd.misc
|
||||
from sabnzbd.constants import CONFIG_VERSION, NORMAL_PRIORITY, DEFAULT_PRIORITY, MAX_WIN_DFOLDER
|
||||
from sabnzbd.utils import configobj
|
||||
@@ -765,9 +769,6 @@ def _read_config(path, try_backup=False):
|
||||
CFG['__encoding__'] = u'utf-8'
|
||||
CFG['__version__'] = unicode(CONFIG_VERSION)
|
||||
|
||||
if 'misc' in CFG:
|
||||
compatibility_fix(CFG['misc'])
|
||||
|
||||
# Use CFG data to set values for all static options
|
||||
for section in database:
|
||||
if section not in ('servers', 'categories', 'rss'):
|
||||
@@ -977,6 +978,21 @@ def define_rss():
|
||||
def get_rss():
|
||||
global database
|
||||
try:
|
||||
# We have to remove non-seperator commas by detecting if they are valid URL's
|
||||
for feed_key in database['rss']:
|
||||
feed = database['rss'][feed_key]
|
||||
# Create a new corrected list
|
||||
new_feed_uris = []
|
||||
for feed_uri in feed.uri():
|
||||
if new_feed_uris and not urlparse(feed_uri).scheme and urlparse(new_feed_uris[-1]).scheme:
|
||||
# Current one has no scheme but previous one does, append to previous
|
||||
new_feed_uris[-1] += '%2C' + feed_uri
|
||||
continue
|
||||
# Add full working URL
|
||||
new_feed_uris.append(feed_uri)
|
||||
# Set new list
|
||||
feed.uri.set(new_feed_uris)
|
||||
|
||||
return database['rss']
|
||||
except KeyError:
|
||||
return {}
|
||||
@@ -1074,12 +1090,6 @@ def validate_notempty(root, value, default):
|
||||
|
||||
def create_api_key():
|
||||
""" Return a new randomized API_KEY """
|
||||
import time
|
||||
try:
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import md5
|
||||
import random
|
||||
# Create some values to seed md5
|
||||
t = str(time.time())
|
||||
r = str(random.random())
|
||||
@@ -1090,22 +1100,3 @@ def create_api_key():
|
||||
|
||||
# Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
_FIXES = (
|
||||
('enable_par_multicore', 'par2_multicore'),
|
||||
)
|
||||
|
||||
|
||||
def compatibility_fix(cf):
|
||||
""" Convert obsolete INI entries """
|
||||
for item in _FIXES:
|
||||
old, new = item
|
||||
try:
|
||||
cf[new]
|
||||
except KeyError:
|
||||
try:
|
||||
cf[new] = cf[old]
|
||||
del cf[old]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -25,9 +25,9 @@ POSTPROC_QUEUE_VERSION = 2
|
||||
|
||||
REC_RAR_VERSION = 500
|
||||
|
||||
PNFO = namedtuple('PNFO', 'repair unpack delete script nzo_id filename password '
|
||||
'unpackstrht msgid category url bytes_left bytes avg_stamp '
|
||||
'avg_date finished_files active_files queued_files status priority missing')
|
||||
PNFO = namedtuple('PNFO', 'repair unpack delete script nzo_id filename password unpackstrht '
|
||||
'msgid category url bytes_left bytes avg_stamp avg_date finished_files '
|
||||
'active_files queued_files status priority missing bytes_missing direct_unpack')
|
||||
|
||||
QNFO = namedtuple('QNFO', 'bytes bytes_left bytes_left_previous_page list q_size_list q_fullsize')
|
||||
|
||||
@@ -47,7 +47,6 @@ SCAN_FILE_NAME = 'watched_data2.sab'
|
||||
FUTURE_Q_FOLDER = 'future'
|
||||
JOB_ADMIN = '__ADMIN__'
|
||||
VERIFIED_FILE = '__verified__'
|
||||
QCHECK_FILE = '__skip_qcheck__'
|
||||
RENAMES_FILE = '__renames__'
|
||||
ATTRIB_FILE = 'SABnzbd_attrib'
|
||||
REPAIR_REQUEST = 'repair-all.sab'
|
||||
|
||||
@@ -217,6 +217,7 @@ class HistoryDB(object):
|
||||
def remove_completed(self, search=None):
|
||||
""" Remove all completed jobs from the database, optional with `search` pattern """
|
||||
search = convert_search(search)
|
||||
logging.info('Removing all completed jobs from history')
|
||||
return self.execute("""DELETE FROM history WHERE name LIKE ? AND status = 'Completed'""", (search,), save=True)
|
||||
|
||||
def get_failed_paths(self, search=None):
|
||||
@@ -231,6 +232,7 @@ class HistoryDB(object):
|
||||
def remove_failed(self, search=None):
|
||||
""" Remove all failed jobs from the database, optional with `search` pattern """
|
||||
search = convert_search(search)
|
||||
logging.info('Removing all failed jobs from history')
|
||||
return self.execute("""DELETE FROM history WHERE name LIKE ? AND status = 'Failed'""", (search,), save=True)
|
||||
|
||||
def remove_history(self, jobs=None):
|
||||
@@ -243,6 +245,7 @@ class HistoryDB(object):
|
||||
|
||||
for job in jobs:
|
||||
self.execute("""DELETE FROM history WHERE nzo_id=?""", (job,))
|
||||
logging.info('Removing job %s from history', job)
|
||||
|
||||
self.save()
|
||||
|
||||
@@ -255,6 +258,7 @@ class HistoryDB(object):
|
||||
downloaded, completeness, fail_message, url_info, bytes, series, md5sum, password)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", t):
|
||||
self.save()
|
||||
logging.info('Added job %s to history', nzo.final_name)
|
||||
|
||||
def fetch_history(self, start=None, limit=None, search=None, failed_only=0, categories=None):
|
||||
""" Return records for specified jobs """
|
||||
|
||||
@@ -22,6 +22,7 @@ sabnzbd.decoder - article decoder
|
||||
import binascii
|
||||
import logging
|
||||
import re
|
||||
import hashlib
|
||||
from time import sleep
|
||||
from threading import Thread
|
||||
|
||||
@@ -30,8 +31,8 @@ from sabnzbd.constants import Status, MAX_DECODE_QUEUE, LIMIT_DECODE_QUEUE, SABY
|
||||
import sabnzbd.articlecache
|
||||
import sabnzbd.downloader
|
||||
import sabnzbd.nzbqueue
|
||||
from sabnzbd.encoding import yenc_name_fixer
|
||||
from sabnzbd.misc import match_str
|
||||
from sabnzbd.encoding import yenc_name_fixer, platform_encode
|
||||
from sabnzbd.misc import match_str, is_obfuscated_filename
|
||||
|
||||
# Check for basic-yEnc
|
||||
try:
|
||||
@@ -68,6 +69,9 @@ class BadYenc(Exception):
|
||||
Exception.__init__(self)
|
||||
|
||||
|
||||
YDEC_TRANS = ''.join([chr((i + 256 - 42) % 256) for i in xrange(256)])
|
||||
|
||||
|
||||
class Decoder(Thread):
|
||||
|
||||
def __init__(self, servers, queue):
|
||||
@@ -113,7 +117,7 @@ class Decoder(Thread):
|
||||
register = True
|
||||
logging.debug("Decoding %s", art_id)
|
||||
|
||||
data = decode(article, lines, raw_data)
|
||||
data = self.decode(article, lines, raw_data)
|
||||
nzf.article_count += 1
|
||||
found = True
|
||||
|
||||
@@ -176,7 +180,7 @@ class Decoder(Thread):
|
||||
logging.info(logme)
|
||||
|
||||
if not found or killed:
|
||||
new_server_found = self.__search_new_server(article)
|
||||
new_server_found = self.search_new_server(article)
|
||||
if new_server_found:
|
||||
register = False
|
||||
logme = None
|
||||
@@ -185,7 +189,7 @@ class Decoder(Thread):
|
||||
logme = T('Unknown Error while decoding %s') % art_id
|
||||
logging.info(logme)
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
new_server_found = self.__search_new_server(article)
|
||||
new_server_found = self.search_new_server(article)
|
||||
if new_server_found:
|
||||
register = False
|
||||
logme = None
|
||||
@@ -197,7 +201,7 @@ class Decoder(Thread):
|
||||
nzo.inc_log('bad_art_log', art_id)
|
||||
|
||||
else:
|
||||
new_server_found = self.__search_new_server(article)
|
||||
new_server_found = self.search_new_server(article)
|
||||
if new_server_found:
|
||||
register = False
|
||||
elif nzo.precheck:
|
||||
@@ -209,7 +213,100 @@ class Decoder(Thread):
|
||||
if register:
|
||||
sabnzbd.nzbqueue.NzbQueue.do.register_article(article, found)
|
||||
|
||||
def __search_new_server(self, article):
|
||||
def decode(self, article, data, raw_data):
|
||||
# Do we have SABYenc? Let it do all the work
|
||||
if sabnzbd.decoder.SABYENC_ENABLED:
|
||||
decoded_data, output_filename, crc, crc_expected, crc_correct = sabyenc.decode_usenet_chunks(raw_data, article.bytes)
|
||||
|
||||
# Assume it is yenc
|
||||
article.nzf.type = 'yenc'
|
||||
|
||||
# Only set the name if it was found and not obfuscated
|
||||
self.verify_filename(article, decoded_data, output_filename)
|
||||
|
||||
# CRC check
|
||||
if not crc_correct:
|
||||
raise CrcError(crc_expected, crc, decoded_data)
|
||||
|
||||
return decoded_data
|
||||
|
||||
# Continue for _yenc or Python-yEnc
|
||||
# Filter out empty ones
|
||||
data = filter(None, data)
|
||||
# No point in continuing if we don't have any data left
|
||||
if data:
|
||||
nzf = article.nzf
|
||||
yenc, data = yCheck(data)
|
||||
ybegin, ypart, yend = yenc
|
||||
decoded_data = None
|
||||
|
||||
# Deal with non-yencoded posts
|
||||
if not ybegin:
|
||||
found = False
|
||||
try:
|
||||
for i in xrange(min(40, len(data))):
|
||||
if data[i].startswith('begin '):
|
||||
nzf.type = 'uu'
|
||||
found = True
|
||||
# Pause the job and show warning
|
||||
if nzf.nzo.status != Status.PAUSED:
|
||||
nzf.nzo.pause()
|
||||
msg = T('UUencode detected, only yEnc encoding is supported [%s]') % nzf.nzo.final_name
|
||||
logging.warning(msg)
|
||||
break
|
||||
except IndexError:
|
||||
raise BadYenc()
|
||||
|
||||
if found:
|
||||
decoded_data = ''
|
||||
else:
|
||||
raise BadYenc()
|
||||
|
||||
# Deal with yenc encoded posts
|
||||
elif ybegin and yend:
|
||||
if 'name' in ybegin:
|
||||
output_filename = yenc_name_fixer(ybegin['name'])
|
||||
else:
|
||||
output_filename = None
|
||||
logging.debug("Possible corrupt header detected => ybegin: %s", ybegin)
|
||||
nzf.type = 'yenc'
|
||||
# Decode data
|
||||
if HAVE_YENC:
|
||||
decoded_data, crc = _yenc.decode_string(''.join(data))[:2]
|
||||
partcrc = '%08X' % ((crc ^ -1) & 2 ** 32L - 1)
|
||||
else:
|
||||
data = ''.join(data)
|
||||
for i in (0, 9, 10, 13, 27, 32, 46, 61):
|
||||
j = '=%c' % (i + 64)
|
||||
data = data.replace(j, chr(i))
|
||||
decoded_data = data.translate(YDEC_TRANS)
|
||||
crc = binascii.crc32(decoded_data)
|
||||
partcrc = '%08X' % (crc & 2 ** 32L - 1)
|
||||
|
||||
if ypart:
|
||||
crcname = 'pcrc32'
|
||||
else:
|
||||
crcname = 'crc32'
|
||||
|
||||
if crcname in yend:
|
||||
_partcrc = yenc_name_fixer('0' * (8 - len(yend[crcname])) + yend[crcname].upper())
|
||||
else:
|
||||
_partcrc = None
|
||||
logging.debug("Corrupt header detected => yend: %s", yend)
|
||||
|
||||
if not _partcrc == partcrc:
|
||||
raise CrcError(_partcrc, partcrc, decoded_data)
|
||||
else:
|
||||
raise BadYenc()
|
||||
|
||||
# Parse filename if there was data
|
||||
if decoded_data:
|
||||
# Only set the name if it was found and not obfuscated
|
||||
self.verify_filename(article, decoded_data, output_filename)
|
||||
|
||||
return decoded_data
|
||||
|
||||
def search_new_server(self, article):
|
||||
# Search new server
|
||||
article.add_to_try_list(article.fetcher)
|
||||
for server in self.servers:
|
||||
@@ -222,99 +319,43 @@ class Decoder(Thread):
|
||||
return True
|
||||
|
||||
msg = T('%s => missing from all servers, discarding') % article
|
||||
logging.debug(msg)
|
||||
logging.info(msg)
|
||||
article.nzf.nzo.inc_log('missing_art_log', msg)
|
||||
return False
|
||||
|
||||
|
||||
YDEC_TRANS = ''.join([chr((i + 256 - 42) % 256) for i in xrange(256)])
|
||||
def decode(article, data, raw_data):
|
||||
# Do we have SABYenc? Let it do all the work
|
||||
if sabnzbd.decoder.SABYENC_ENABLED:
|
||||
decoded_data, output_filename, crc, crc_expected, crc_correct = sabyenc.decode_usenet_chunks(raw_data, article.bytes)
|
||||
|
||||
# Assume it is yenc
|
||||
article.nzf.type = 'yenc'
|
||||
|
||||
# Only set the name if it was found
|
||||
if output_filename:
|
||||
article.nzf.filename = output_filename
|
||||
|
||||
# CRC check
|
||||
if not crc_correct:
|
||||
raise CrcError(crc_expected, crc, decoded_data)
|
||||
|
||||
return decoded_data
|
||||
|
||||
# Continue for _yenc or Python-yEnc
|
||||
# Filter out empty ones
|
||||
data = filter(None, data)
|
||||
# No point in continuing if we don't have any data left
|
||||
if data:
|
||||
def verify_filename(self, article, decoded_data, yenc_filename):
|
||||
""" Verify the filename provided by yenc by using
|
||||
par2 information and otherwise fall back to NZB name
|
||||
"""
|
||||
nzf = article.nzf
|
||||
yenc, data = yCheck(data)
|
||||
ybegin, ypart, yend = yenc
|
||||
decoded_data = None
|
||||
# Was this file already verified and did we get a name?
|
||||
if nzf.filename_checked or not yenc_filename:
|
||||
return
|
||||
|
||||
# Deal with non-yencoded posts
|
||||
if not ybegin:
|
||||
found = False
|
||||
try:
|
||||
for i in xrange(min(40, len(data))):
|
||||
if data[i].startswith('begin '):
|
||||
nzf.type = 'uu'
|
||||
found = True
|
||||
# Pause the job and show warning
|
||||
if nzf.nzo.status != Status.PAUSED:
|
||||
nzf.nzo.pause()
|
||||
msg = T('UUencode detected, only yEnc encoding is supported [%s]') % nzf.nzo.final_name
|
||||
logging.warning(msg)
|
||||
break
|
||||
except IndexError:
|
||||
raise BadYenc()
|
||||
# Set the md5-of-16k if this is the first article
|
||||
if article.partnum == nzf.lowest_partnum:
|
||||
nzf.md5of16k = hashlib.md5(decoded_data[:16384]).digest()
|
||||
|
||||
if found:
|
||||
decoded_data = ''
|
||||
else:
|
||||
raise BadYenc()
|
||||
# If we have the md5, use it to rename
|
||||
if nzf.md5of16k:
|
||||
# Don't check again, even if no match
|
||||
nzf.filename_checked = True
|
||||
# Find the match and rename
|
||||
if nzf.md5of16k in nzf.nzo.md5of16k:
|
||||
new_filename = platform_encode(nzf.nzo.md5of16k[nzf.md5of16k])
|
||||
# Was it even new?
|
||||
if new_filename != nzf.filename:
|
||||
logging.info('Detected filename based on par2: %s -> %s', nzf.filename, new_filename)
|
||||
nzf.nzo.renamed_file(new_filename, nzf.filename)
|
||||
nzf.filename = new_filename
|
||||
return
|
||||
|
||||
# Deal with yenc encoded posts
|
||||
elif ybegin and yend:
|
||||
if 'name' in ybegin:
|
||||
nzf.filename = yenc_name_fixer(ybegin['name'])
|
||||
else:
|
||||
logging.debug("Possible corrupt header detected => ybegin: %s", ybegin)
|
||||
nzf.type = 'yenc'
|
||||
# Decode data
|
||||
if HAVE_YENC:
|
||||
decoded_data, crc = _yenc.decode_string(''.join(data))[:2]
|
||||
partcrc = '%08X' % ((crc ^ -1) & 2 ** 32L - 1)
|
||||
else:
|
||||
data = ''.join(data)
|
||||
for i in (0, 9, 10, 13, 27, 32, 46, 61):
|
||||
j = '=%c' % (i + 64)
|
||||
data = data.replace(j, chr(i))
|
||||
decoded_data = data.translate(YDEC_TRANS)
|
||||
crc = binascii.crc32(decoded_data)
|
||||
partcrc = '%08X' % (crc & 2 ** 32L - 1)
|
||||
|
||||
if ypart:
|
||||
crcname = 'pcrc32'
|
||||
else:
|
||||
crcname = 'crc32'
|
||||
|
||||
if crcname in yend:
|
||||
_partcrc = yenc_name_fixer('0' * (8 - len(yend[crcname])) + yend[crcname].upper())
|
||||
else:
|
||||
_partcrc = None
|
||||
logging.debug("Corrupt header detected => yend: %s", yend)
|
||||
|
||||
if not _partcrc == partcrc:
|
||||
raise CrcError(_partcrc, partcrc, decoded_data)
|
||||
else:
|
||||
raise BadYenc()
|
||||
|
||||
return decoded_data
|
||||
# Fallback to yenc/nzb name (also when there is no partnum=1)
|
||||
# We also keep the NZB name in case it ends with ".par2" (usually correct)
|
||||
if yenc_filename != nzf.filename and not is_obfuscated_filename(yenc_filename) and not nzf.filename.endswith('.par2'):
|
||||
logging.info('Detected filename from yenc: %s -> %s', nzf.filename, yenc_filename)
|
||||
nzf.nzo.renamed_file(yenc_filename, nzf.filename)
|
||||
nzf.filename = yenc_filename
|
||||
|
||||
|
||||
def yCheck(data):
|
||||
|
||||
379
sabnzbd/directunpacker.py
Normal file
379
sabnzbd/directunpacker.py
Normal file
@@ -0,0 +1,379 @@
|
||||
#!/usr/bin/python -OO
|
||||
# Copyright 2008-2017 The SABnzbd-Team <team@sabnzbd.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
sabnzbd.directunpacker
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.misc import int_conv, clip_path, remove_all, globber, format_time_string, has_win_device
|
||||
from sabnzbd.encoding import unicoder
|
||||
from sabnzbd.newsunpack import build_command
|
||||
from sabnzbd.postproc import prepare_extraction_path
|
||||
from sabnzbd.utils.diskspeed import diskspeedmeasure
|
||||
|
||||
if sabnzbd.WIN32:
|
||||
# Load the POpen from the fixed unicode-subprocess
|
||||
from sabnzbd.utils.subprocess_fix import Popen
|
||||
else:
|
||||
# Load the regular POpen
|
||||
from subprocess import Popen
|
||||
|
||||
MAX_ACTIVE_UNPACKERS = 10
|
||||
ACTIVE_UNPACKERS = []
|
||||
CONCURRENT_LOCK = threading.RLock()
|
||||
|
||||
RAR_NR = re.compile(r'(.*?)(\.part(\d*).rar|\.r(\d*))$', re.IGNORECASE)
|
||||
|
||||
|
||||
class DirectUnpacker(threading.Thread):
|
||||
|
||||
def __init__(self, nzo):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.nzo = nzo
|
||||
self.active_instance = None
|
||||
self.killed = False
|
||||
self.next_file_lock = threading.Condition(threading.RLock())
|
||||
|
||||
self.unpack_dir_info = None
|
||||
self.cur_setname = None
|
||||
self.cur_volume = 0
|
||||
self.total_volumes = {}
|
||||
|
||||
self.success_sets = []
|
||||
self.next_sets = []
|
||||
|
||||
nzo.direct_unpacker = self
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
def release_concurrent_lock(self):
|
||||
""" Let other unpackers go """
|
||||
try:
|
||||
CONCURRENT_LOCK.release()
|
||||
except:
|
||||
pass
|
||||
|
||||
def reset_active(self):
|
||||
self.active_instance = None
|
||||
self.cur_setname = None
|
||||
self.cur_volume = 0
|
||||
# Release lock to be sure
|
||||
self.release_concurrent_lock()
|
||||
|
||||
def check_requirements(self):
|
||||
if self.killed or not self.nzo.unpack or cfg.direct_unpack() < 1 or sabnzbd.newsunpack.RAR_PROBLEM:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_volumes_for_nzo(self):
|
||||
""" Loop over all files to detect the names """
|
||||
none_counter = 0
|
||||
found_counter = 0
|
||||
for nzf in self.nzo.files + self.nzo.finished_files:
|
||||
nzf.setname, nzf.vol = analyze_rar_filename(nzf.filename)
|
||||
# We matched?
|
||||
if nzf.setname:
|
||||
found_counter += 1
|
||||
if nzf.setname not in self.total_volumes:
|
||||
self.total_volumes[nzf.setname] = 0
|
||||
self.total_volumes[nzf.setname] += 1
|
||||
else:
|
||||
none_counter += 1
|
||||
|
||||
# Too much not found? Obfuscated, ignore results
|
||||
if none_counter > found_counter:
|
||||
self.total_volumes = {}
|
||||
|
||||
def add(self, nzf):
|
||||
""" Add jobs and start instance of DirectUnpack """
|
||||
if not cfg.direct_unpack_tested():
|
||||
test_disk_performance()
|
||||
|
||||
# Stop if something is wrong
|
||||
if not self.check_requirements():
|
||||
return
|
||||
|
||||
# Is this the first set?
|
||||
if not self.cur_setname:
|
||||
self.set_volumes_for_nzo()
|
||||
self.cur_setname = nzf.setname
|
||||
|
||||
# Analyze updated filenames
|
||||
nzf.setname, nzf.vol = analyze_rar_filename(nzf.filename)
|
||||
|
||||
# Are we doing this set?
|
||||
if self.cur_setname == nzf.setname:
|
||||
logging.debug('DirectUnpack queued %s for %s', nzf.filename, self.cur_setname)
|
||||
# Is this the first one of the first set?
|
||||
if not self.active_instance and not self.is_alive() and self.have_next_volume():
|
||||
# Too many runners already?
|
||||
if len(ACTIVE_UNPACKERS) >= MAX_ACTIVE_UNPACKERS:
|
||||
logging.info('Too many DirectUnpackers currently to start %s', self.cur_setname)
|
||||
return
|
||||
|
||||
# Start the unrar command and the loop
|
||||
self.create_unrar_instance(nzf)
|
||||
self.start()
|
||||
elif not any(test_nzf.setname == nzf.setname for test_nzf in self.next_sets):
|
||||
# Need to store this for the future, only once per set!
|
||||
self.next_sets.append(nzf)
|
||||
|
||||
# Wake up the thread to see if this is good to go
|
||||
with self.next_file_lock:
|
||||
self.next_file_lock.notify()
|
||||
|
||||
def run(self):
|
||||
# Input and output
|
||||
linebuf = ''
|
||||
unrar_log = []
|
||||
|
||||
# Need to read char-by-char because there's no newline after new-disk message
|
||||
while 1:
|
||||
if not self.active_instance:
|
||||
break
|
||||
|
||||
char = self.active_instance.stdout.read(1)
|
||||
linebuf += char
|
||||
|
||||
if not char:
|
||||
# End of program
|
||||
break
|
||||
|
||||
# Error? Let PP-handle it
|
||||
if linebuf.endswith(('ERROR: ', 'Cannot create', 'in the encrypted file', 'CRC failed', \
|
||||
'checksum failed', 'You need to start extraction from a previous volume', \
|
||||
'password is incorrect', 'Write error', 'checksum error', \
|
||||
'start extraction from a previous volume')):
|
||||
logging.info('Error in DirectUnpack of %s', self.cur_setname)
|
||||
self.abort()
|
||||
|
||||
# Did we reach the end?
|
||||
if linebuf.endswith('All OK'):
|
||||
# Add to success
|
||||
self.success_sets.append(self.cur_setname)
|
||||
logging.info('DirectUnpack completed for %s', self.cur_setname)
|
||||
|
||||
# Make sure to release the lock
|
||||
self.release_concurrent_lock()
|
||||
|
||||
# Are there more files left?
|
||||
if self.nzo.files:
|
||||
with self.next_file_lock:
|
||||
self.next_file_lock.wait()
|
||||
|
||||
# Is there another set to do?
|
||||
if self.next_sets:
|
||||
# Write current log
|
||||
unrar_log.append(linebuf.strip())
|
||||
linebuf = ''
|
||||
logging.debug('DirectUnpack Unrar output %s', '\n'.join(unrar_log))
|
||||
unrar_log = []
|
||||
|
||||
# Start new instance
|
||||
nzf = self.next_sets.pop(0)
|
||||
self.reset_active()
|
||||
self.cur_setname = nzf.setname
|
||||
# Wait for the 1st volume to appear
|
||||
self.wait_for_next_volume()
|
||||
self.create_unrar_instance(nzf)
|
||||
else:
|
||||
break
|
||||
|
||||
if linebuf.endswith('[C]ontinue, [Q]uit '):
|
||||
# Next one can go now
|
||||
self.release_concurrent_lock()
|
||||
|
||||
# Wait for the next one..
|
||||
self.wait_for_next_volume()
|
||||
|
||||
# Send "Enter" to proceed, only 1 at a time via lock
|
||||
CONCURRENT_LOCK.acquire()
|
||||
# Possible that the instance was deleted while locked
|
||||
if not self.killed:
|
||||
# Next volume
|
||||
self.cur_volume += 1
|
||||
self.active_instance.stdin.write('\n')
|
||||
self.nzo.set_action_line(T('Unpacking'), self.get_formatted_stats())
|
||||
logging.info('DirectUnpacked volume %s for %s', self.cur_volume, self.cur_setname)
|
||||
|
||||
if linebuf.endswith('\n'):
|
||||
unrar_log.append(linebuf.strip())
|
||||
linebuf = ''
|
||||
|
||||
# Add last line
|
||||
unrar_log.append(linebuf.strip())
|
||||
logging.debug('DirectUnpack Unrar output %s', '\n'.join(unrar_log))
|
||||
|
||||
# Save information if success
|
||||
if self.success_sets:
|
||||
msg = T('Unpacked %s files/folders in %s') % (len(globber(self.unpack_dir_info[0])), format_time_string(0))
|
||||
self.nzo.set_unpack_info('Unpack', '[%s] %s' % (unicoder(self.cur_setname), msg))
|
||||
|
||||
# Make more space
|
||||
self.reset_active()
|
||||
ACTIVE_UNPACKERS.remove(self)
|
||||
|
||||
# Make sure to release the lock
|
||||
self.release_concurrent_lock()
|
||||
|
||||
def have_next_volume(self):
|
||||
""" Check if next volume of set is available, start
|
||||
from the end of the list where latest completed files are """
|
||||
for nzf_search in reversed(self.nzo.finished_files):
|
||||
if nzf_search.setname == self.cur_setname and nzf_search.vol == self.cur_volume+1:
|
||||
return True
|
||||
return False
|
||||
|
||||
def wait_for_next_volume(self):
|
||||
""" Wait for the correct volume to appear
|
||||
But stop if it was killed or the NZB is done """
|
||||
while not self.have_next_volume() and not self.killed and self.nzo.files:
|
||||
with self.next_file_lock:
|
||||
self.next_file_lock.wait()
|
||||
|
||||
def create_unrar_instance(self, rarfile_nzf):
|
||||
""" Start the unrar instance using the user's options """
|
||||
# Generate extraction path and save for post-proc
|
||||
if not self.unpack_dir_info:
|
||||
self.unpack_dir_info = prepare_extraction_path(self.nzo)
|
||||
extraction_path, _, _, one_folder, _ = self.unpack_dir_info
|
||||
|
||||
# Set options
|
||||
if self.nzo.password:
|
||||
password_command = '-p%s' % self.nzo.password
|
||||
else:
|
||||
password_command = '-p-'
|
||||
|
||||
if one_folder or cfg.flat_unpack():
|
||||
action = 'e'
|
||||
else:
|
||||
action = 'x'
|
||||
|
||||
# Generate command
|
||||
rarfile_path = os.path.join(self.nzo.downpath, rarfile_nzf.filename)
|
||||
if sabnzbd.WIN32:
|
||||
if not has_win_device(rarfile_path):
|
||||
command = ['%s' % sabnzbd.newsunpack.RAR_COMMAND, action, '-vp', '-idp', '-o+', '-ai', password_command,
|
||||
'%s' % clip_path(rarfile_path), clip_path(extraction_path)]
|
||||
else:
|
||||
# Need long-path notation in case of forbidden-names
|
||||
command = ['%s' % sabnzbd.newsunpack.RAR_COMMAND, action, '-vp', '-idp', '-o+', '-ai', password_command,
|
||||
'%s' % clip_path(rarfile_path), '%s\\' % extraction_path]
|
||||
else:
|
||||
# Don't use "-ai" (not needed for non-Windows)
|
||||
command = ['%s' % sabnzbd.newsunpack.RAR_COMMAND, action, '-vp', '-idp', '-o+', password_command,
|
||||
'%s' % rarfile_path, '%s/' % extraction_path]
|
||||
|
||||
if cfg.ignore_unrar_dates():
|
||||
command.insert(3, '-tsm-')
|
||||
|
||||
stup, need_shell, command, creationflags = build_command(command)
|
||||
logging.debug('Running unrar for DirectUnpack %s', command)
|
||||
|
||||
# Aquire lock and go
|
||||
self.active_instance = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, creationflags=creationflags)
|
||||
# Add to runners
|
||||
ACTIVE_UNPACKERS.append(self)
|
||||
|
||||
# Doing the first
|
||||
logging.info('DirectUnpacked volume %s for %s', self.cur_volume, self.cur_setname)
|
||||
|
||||
def abort(self):
|
||||
""" Abort running instance and delete generated files """
|
||||
if not self.killed:
|
||||
logging.info('Aborting DirectUnpack for %s', self.cur_setname)
|
||||
self.killed = True
|
||||
|
||||
# Abort Unrar
|
||||
if self.active_instance:
|
||||
self.active_instance.kill()
|
||||
# We need to wait for it to kill the process
|
||||
self.active_instance.wait()
|
||||
|
||||
# Wake up the thread
|
||||
with self.next_file_lock:
|
||||
self.next_file_lock.notify()
|
||||
|
||||
# No new sets
|
||||
self.next_sets = []
|
||||
self.success_sets = []
|
||||
|
||||
# Remove files
|
||||
if self.unpack_dir_info:
|
||||
extraction_path, _, _, _, _ = self.unpack_dir_info
|
||||
remove_all(extraction_path, recursive=True)
|
||||
# Remove dir-info
|
||||
self.unpack_dir_info = None
|
||||
|
||||
# Reset settings
|
||||
self.reset_active()
|
||||
|
||||
def get_formatted_stats(self):
|
||||
""" Get percentage or number of rar's done """
|
||||
if self.cur_setname and self.cur_setname in self.total_volumes:
|
||||
# This won't work on obfuscated posts
|
||||
if self.total_volumes[self.cur_setname] > self.cur_volume and self.cur_volume:
|
||||
return '%.0f%%' % (100*float(self.cur_volume)/self.total_volumes[self.cur_setname])
|
||||
return self.cur_volume
|
||||
|
||||
|
||||
def analyze_rar_filename(filename):
|
||||
""" Extract volume number and setname from rar-filenames
|
||||
Both ".part01.rar" and ".r01" """
|
||||
m = RAR_NR.search(filename)
|
||||
if m:
|
||||
if m.group(4):
|
||||
# Special since starts with ".rar", ".r00"
|
||||
return m.group(1), int_conv(m.group(4)) + 2
|
||||
return m.group(1), int_conv(m.group(3))
|
||||
else:
|
||||
# Detect if first of "rxx" set
|
||||
if filename.endswith('.rar') and '.part' not in filename:
|
||||
return os.path.splitext(filename)[0], 1
|
||||
return None, None
|
||||
|
||||
|
||||
def abort_all():
|
||||
""" Abort all running DirectUnpackers """
|
||||
logging.info('Aborting all DirectUnpackers')
|
||||
for direct_unpacker in ACTIVE_UNPACKERS:
|
||||
direct_unpacker.abort()
|
||||
|
||||
|
||||
def test_disk_performance():
|
||||
""" Test the incomplete-dir performance and enable
|
||||
Direct Unpack if good enough (> 60MB/s)
|
||||
"""
|
||||
if diskspeedmeasure(sabnzbd.cfg.download_dir.get_path()) > 60:
|
||||
cfg.direct_unpack.set(True)
|
||||
logging.warning(T('Enabled Direct Unpack:') + ' ' + T('Jobs will start unpacking during the download, reduces post-processing time but requires capable hard drive. Only works for jobs that do not need repair.'))
|
||||
cfg.direct_unpack_tested.set(True)
|
||||
@@ -353,7 +353,6 @@ class Downloader(Thread):
|
||||
""" Return True when this server has the highest priority of the active ones
|
||||
0 is the highest priority
|
||||
"""
|
||||
|
||||
for server in self.servers:
|
||||
if server is not me and server.active and server.priority < me.priority:
|
||||
return False
|
||||
@@ -466,7 +465,7 @@ class Downloader(Thread):
|
||||
|
||||
if server.retention and article.nzf.nzo.avg_stamp < time.time() - server.retention:
|
||||
# Let's get rid of all the articles for this server at once
|
||||
logging.debug('Job %s too old for %s, moving on', article.nzf.nzo.work_name, server.id)
|
||||
logging.info('Job %s too old for %s, moving on', article.nzf.nzo.work_name, server.id)
|
||||
while article:
|
||||
self.decode(article, None, None)
|
||||
article = article.nzf.nzo.get_article(server, self.servers)
|
||||
@@ -709,7 +708,7 @@ class Downloader(Thread):
|
||||
|
||||
elif nw.status_code in ('411', '423', '430'):
|
||||
done = True
|
||||
logging.info('Thread %s@%s: Article %s missing (error=%s)',
|
||||
logging.debug('Thread %s@%s: Article %s missing (error=%s)',
|
||||
nw.thrdnum, nw.server.id, article.article, nw.status_code)
|
||||
nw.clear_data()
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from sabnzbd.constants import *
|
||||
import sabnzbd
|
||||
from sabnzbd.misc import to_units, split_host, time_format
|
||||
from sabnzbd.encoding import EmailFilter
|
||||
from sabnzbd.notifier import check_cat
|
||||
import sabnzbd.cfg as cfg
|
||||
|
||||
|
||||
@@ -216,6 +217,9 @@ def send_with_template(prefix, parm, test=None):
|
||||
|
||||
def endjob(filename, cat, status, path, bytes, fail_msg, stages, script, script_output, script_ret, test=None):
|
||||
""" Send end-of-job email """
|
||||
# Is it allowed?
|
||||
if not check_cat('email', cat):
|
||||
return None
|
||||
|
||||
# Translate the stage names
|
||||
tr = sabnzbd.api.Ttemplate
|
||||
|
||||
@@ -1216,6 +1216,7 @@ def orphan_delete(kwargs):
|
||||
if path:
|
||||
path = platform_encode(path)
|
||||
path = os.path.join(long_path(cfg.download_dir.get_path()), path)
|
||||
logging.info('Removing orphaned job %s', path)
|
||||
remove_all(path, recursive=True)
|
||||
|
||||
def orphan_delete_all():
|
||||
@@ -1229,6 +1230,7 @@ def orphan_add(kwargs):
|
||||
if path:
|
||||
path = platform_encode(path)
|
||||
path = os.path.join(long_path(cfg.download_dir.get_path()), path)
|
||||
logging.info('Re-adding orphaned job %s', path)
|
||||
NzbQueue.do.repair_job(path, None, None)
|
||||
|
||||
def orphan_add_all():
|
||||
@@ -1298,7 +1300,7 @@ class ConfigFolders(object):
|
||||
|
||||
##############################################################################
|
||||
SWITCH_LIST = \
|
||||
('par2_multicore', 'multipar', 'par_option', 'top_only', 'ssl_ciphers',
|
||||
('par_option', 'top_only', 'ssl_ciphers', 'direct_unpack',
|
||||
'auto_sort', 'propagation_delay', 'auto_disconnect', 'flat_unpack',
|
||||
'safe_postproc', 'no_dupes', 'replace_spaces', 'replace_dots',
|
||||
'ignore_samples', 'pause_on_post_processing', 'nice', 'ionice',
|
||||
@@ -1374,11 +1376,11 @@ class ConfigSwitches(object):
|
||||
SPECIAL_BOOL_LIST = \
|
||||
('start_paused', 'no_penalties', 'ignore_wrong_unrar', 'overwrite_files', 'enable_par_cleanup',
|
||||
'queue_complete_pers', 'api_warnings', 'ampm', 'enable_unrar', 'enable_unzip', 'enable_7zip',
|
||||
'enable_filejoin', 'enable_tsjoin', 'allow_streaming', 'ignore_unrar_dates', 'par2_multicore',
|
||||
'osx_menu', 'osx_speed', 'win_menu', 'use_pickle', 'allow_incomplete_nzb', 'rss_filenames',
|
||||
'ipv6_hosting', 'keep_awake', 'empty_postproc', 'html_login', 'wait_for_dfolder', 'max_art_opt',
|
||||
'warn_empty_nzb', 'enable_bonjour','allow_duplicate_files', 'warn_dupl_jobs', 'replace_illegal',
|
||||
'backup_for_duplicates', 'disable_api_key', 'api_logging', 'enable_meta',
|
||||
'enable_filejoin', 'enable_tsjoin', 'ignore_unrar_dates',
|
||||
'multipar', 'osx_menu', 'osx_speed', 'win_menu', 'use_pickle', 'allow_incomplete_nzb',
|
||||
'rss_filenames', 'ipv6_hosting', 'keep_awake', 'empty_postproc', 'html_login', 'wait_for_dfolder',
|
||||
'max_art_opt', 'warn_empty_nzb', 'enable_bonjour','allow_duplicate_files', 'warn_dupl_jobs',
|
||||
'replace_illegal', 'backup_for_duplicates', 'disable_api_key', 'api_logging',
|
||||
)
|
||||
SPECIAL_VALUE_LIST = \
|
||||
('size_limit', 'folder_max_length', 'fsys_type', 'movie_rename_limit', 'nomedia_marker',
|
||||
@@ -2693,39 +2695,39 @@ def GetRssLog(feed):
|
||||
|
||||
##############################################################################
|
||||
LIST_EMAIL = (
|
||||
'email_endjob', 'email_full',
|
||||
'email_endjob', 'email_cats', 'email_full',
|
||||
'email_server', 'email_to', 'email_from',
|
||||
'email_account', 'email_pwd', 'email_dir', 'email_rss'
|
||||
)
|
||||
LIST_GROWL = ('growl_enable', 'growl_server', 'growl_password',
|
||||
LIST_GROWL = ('growl_enable', 'growl_cats', 'growl_server', 'growl_password',
|
||||
'growl_prio_startup', 'growl_prio_download', 'growl_prio_pp', 'growl_prio_complete', 'growl_prio_failed',
|
||||
'growl_prio_disk_full', 'growl_prio_warning', 'growl_prio_error', 'growl_prio_queue_done', 'growl_prio_other',
|
||||
'growl_prio_new_login')
|
||||
LIST_NCENTER = ('ncenter_enable',
|
||||
LIST_NCENTER = ('ncenter_enable', 'ncenter_cats',
|
||||
'ncenter_prio_startup', 'ncenter_prio_download', 'ncenter_prio_pp', 'ncenter_prio_complete', 'ncenter_prio_failed',
|
||||
'ncenter_prio_disk_full', 'ncenter_prio_warning', 'ncenter_prio_error', 'ncenter_prio_queue_done', 'ncenter_prio_other',
|
||||
'ncenter_prio_new_login')
|
||||
LIST_ACENTER = ('acenter_enable',
|
||||
LIST_ACENTER = ('acenter_enable', 'acenter_cats',
|
||||
'acenter_prio_startup', 'acenter_prio_download', 'acenter_prio_pp', 'acenter_prio_complete', 'acenter_prio_failed',
|
||||
'acenter_prio_disk_full', 'acenter_prio_warning', 'acenter_prio_error', 'acenter_prio_queue_done', 'acenter_prio_other',
|
||||
'acenter_prio_new_login')
|
||||
LIST_NTFOSD = ('ntfosd_enable',
|
||||
LIST_NTFOSD = ('ntfosd_enable', 'ntfosd_cats',
|
||||
'ntfosd_prio_startup', 'ntfosd_prio_download', 'ntfosd_prio_pp', 'ntfosd_prio_complete', 'ntfosd_prio_failed',
|
||||
'ntfosd_prio_disk_full', 'ntfosd_prio_warning', 'ntfosd_prio_error', 'ntfosd_prio_queue_done', 'ntfosd_prio_other',
|
||||
'ntfosd_prio_new_login')
|
||||
LIST_PROWL = ('prowl_enable', 'prowl_apikey',
|
||||
LIST_PROWL = ('prowl_enable', 'prowl_cats', 'prowl_apikey',
|
||||
'prowl_prio_startup', 'prowl_prio_download', 'prowl_prio_pp', 'prowl_prio_complete', 'prowl_prio_failed',
|
||||
'prowl_prio_disk_full', 'prowl_prio_warning', 'prowl_prio_error', 'prowl_prio_queue_done', 'prowl_prio_other',
|
||||
'prowl_prio_new_login')
|
||||
LIST_PUSHOVER = ('pushover_enable', 'pushover_token', 'pushover_userkey', 'pushover_device',
|
||||
LIST_PUSHOVER = ('pushover_enable', 'pushover_cats', 'pushover_token', 'pushover_userkey', 'pushover_device',
|
||||
'pushover_prio_startup', 'pushover_prio_download', 'pushover_prio_pp', 'pushover_prio_complete', 'pushover_prio_failed',
|
||||
'pushover_prio_disk_full', 'pushover_prio_warning', 'pushover_prio_error', 'pushover_prio_queue_done', 'pushover_prio_other',
|
||||
'pushover_prio_new_login')
|
||||
LIST_PUSHBULLET = ('pushbullet_enable', 'pushbullet_apikey', 'pushbullet_device',
|
||||
LIST_PUSHBULLET = ('pushbullet_enable', 'pushbullet_cats', 'pushbullet_apikey', 'pushbullet_device',
|
||||
'pushbullet_prio_startup', 'pushbullet_prio_download', 'pushbullet_prio_pp', 'pushbullet_prio_complete', 'pushbullet_prio_failed',
|
||||
'pushbullet_prio_disk_full', 'pushbullet_prio_warning', 'pushbullet_prio_error', 'pushbullet_prio_queue_done', 'pushbullet_prio_other',
|
||||
'pushbullet_prio_new_login')
|
||||
LIST_NSCRIPT = ('nscript_enable', 'nscript_script', 'nscript_parameters',
|
||||
LIST_NSCRIPT = ('nscript_enable', 'nscript_cats', 'nscript_script', 'nscript_parameters',
|
||||
'nscript_prio_startup', 'nscript_prio_download', 'nscript_prio_pp', 'nscript_prio_complete', 'nscript_prio_failed',
|
||||
'nscript_prio_disk_full', 'nscript_prio_warning', 'nscript_prio_error', 'nscript_prio_queue_done', 'nscript_prio_other',
|
||||
'nscript_prio_new_login')
|
||||
@@ -2747,6 +2749,7 @@ class ConfigNotify(object):
|
||||
conf = build_header(sabnzbd.WEB_DIR_CONFIG)
|
||||
|
||||
conf['my_home'] = sabnzbd.DIR_HOME
|
||||
conf['categories'] = list_cats(False)
|
||||
conf['lastmail'] = self.__lastmail
|
||||
conf['have_growl'] = True
|
||||
conf['have_ntfosd'] = sabnzbd.notifier.have_ntfosd()
|
||||
@@ -2857,7 +2860,7 @@ def rss_history(url, limit=50, search=None):
|
||||
rss = RSS()
|
||||
rss.channel.title = "SABnzbd History"
|
||||
rss.channel.description = "Overview of completed downloads"
|
||||
rss.channel.link = "http://sabnzbd.org/"
|
||||
rss.channel.link = "https://sabnzbd.org/"
|
||||
rss.channel.language = "en"
|
||||
|
||||
items, _fetched_items, _max_items = build_history(limit=limit, search=search)
|
||||
@@ -2904,7 +2907,7 @@ def rss_warnings():
|
||||
rss = RSS()
|
||||
rss.channel.title = "SABnzbd Warnings"
|
||||
rss.channel.description = "Overview of warnings/errors"
|
||||
rss.channel.link = "http://sabnzbd.org/"
|
||||
rss.channel.link = "https://sabnzbd.org/"
|
||||
rss.channel.language = "en"
|
||||
|
||||
for warn in sabnzbd.GUIHANDLER.content():
|
||||
|
||||
@@ -247,10 +247,11 @@ def replace_win_devices(name):
|
||||
|
||||
def has_win_device(p):
|
||||
""" Return True if filename part contains forbidden name
|
||||
Before and after sanitizing
|
||||
"""
|
||||
p = os.path.split(p)[1].lower()
|
||||
for dev in _DEVICES:
|
||||
if p == dev or p.startswith(dev + '.'):
|
||||
if p == dev or p.startswith(dev + '.') or p.startswith('_' + dev + '.'):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -264,7 +265,7 @@ else:
|
||||
CH_LEGAL = '+'
|
||||
|
||||
|
||||
def sanitize_filename(name, allow_win_devices=False):
|
||||
def sanitize_filename(name):
|
||||
""" Return filename with illegal chars converted to legal ones
|
||||
and with the par2 extension always in lowercase
|
||||
"""
|
||||
@@ -281,7 +282,7 @@ def sanitize_filename(name, allow_win_devices=False):
|
||||
# Compensate for the foolish way par2 on OSX handles a colon character
|
||||
name = name[name.rfind(':') + 1:]
|
||||
|
||||
if sabnzbd.WIN32 and not allow_win_devices:
|
||||
if sabnzbd.WIN32 or cfg.sanitize_safe():
|
||||
name = replace_win_devices(name)
|
||||
|
||||
lst = []
|
||||
@@ -397,20 +398,11 @@ def sanitize_files_in_folder(folder):
|
||||
return lst
|
||||
|
||||
|
||||
def flag_file(path, flag, create=False):
|
||||
""" Create verify flag file or return True if it already exists """
|
||||
path = os.path.join(path, JOB_ADMIN)
|
||||
path = os.path.join(path, flag)
|
||||
if create:
|
||||
try:
|
||||
f = open(path, 'w')
|
||||
f.write('ok\n')
|
||||
f.close()
|
||||
return True
|
||||
except IOError:
|
||||
return False
|
||||
else:
|
||||
return os.path.exists(path)
|
||||
def is_obfuscated_filename(filename):
|
||||
""" Check if this file has an extension, if not, it's
|
||||
probably obfuscated and we don't use it
|
||||
"""
|
||||
return (os.path.splitext(filename)[1] == '')
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -911,8 +903,7 @@ def move_to_path(path, new_path):
|
||||
new_path = get_unique_filename(new_path)
|
||||
|
||||
if new_path:
|
||||
logging.debug("Moving. Old path: %s New path: %s Overwrite: %s",
|
||||
path, new_path, overwrite)
|
||||
logging.debug("Moving (overwrite: %s) %s => %s", overwrite, path, new_path)
|
||||
try:
|
||||
# First try cheap rename
|
||||
renamer(path, new_path)
|
||||
@@ -1183,24 +1174,40 @@ else:
|
||||
return 20.0, 10.0
|
||||
|
||||
|
||||
__LAST_DISK_RESULT = {}
|
||||
__LAST_DISK_CALL = {}
|
||||
def diskspace(_dir, force=False):
|
||||
# Store all results to speed things up
|
||||
__DIRS_CHECKED = []
|
||||
__DISKS_SAME = None
|
||||
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []}
|
||||
__LAST_DISK_CALL = 0
|
||||
|
||||
def diskspace(force=False):
|
||||
""" Wrapper to cache results """
|
||||
if _dir not in __LAST_DISK_RESULT:
|
||||
__LAST_DISK_RESULT[_dir] = [0.0, 0.0]
|
||||
__LAST_DISK_CALL[_dir] = 0.0
|
||||
global __DIRS_CHECKED, __DISKS_SAME, __LAST_DISK_RESULT, __LAST_DISK_CALL
|
||||
|
||||
# Reset everything when folders changed
|
||||
dirs_to_check = [cfg.download_dir.get_path(), cfg.complete_dir.get_path()]
|
||||
if __DIRS_CHECKED != dirs_to_check:
|
||||
__DIRS_CHECKED = dirs_to_check
|
||||
__DISKS_SAME = None
|
||||
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []}
|
||||
__LAST_DISK_CALL = 0
|
||||
|
||||
# When forced, ignore any cache to avoid problems in UI
|
||||
if force:
|
||||
return diskspace_base(_dir)
|
||||
__LAST_DISK_CALL = 0
|
||||
|
||||
# Check against cache
|
||||
if time.time() > __LAST_DISK_CALL[_dir] + 10.0:
|
||||
__LAST_DISK_RESULT[_dir] = diskspace_base(_dir)
|
||||
__LAST_DISK_CALL[_dir] = time.time()
|
||||
if time.time() > __LAST_DISK_CALL + 10.0:
|
||||
# Same disk? Then copy-paste
|
||||
__LAST_DISK_RESULT['download_dir'] = diskspace_base(cfg.download_dir.get_path())
|
||||
__LAST_DISK_RESULT['complete_dir'] = __LAST_DISK_RESULT['download_dir'] if __DISKS_SAME else diskspace_base(cfg.complete_dir.get_path())
|
||||
__LAST_DISK_CALL = time.time()
|
||||
|
||||
return __LAST_DISK_RESULT[_dir]
|
||||
# Do we know if it's same disk?
|
||||
if __DISKS_SAME is None:
|
||||
__DISKS_SAME = (__LAST_DISK_RESULT['download_dir'] == __LAST_DISK_RESULT['complete_dir'])
|
||||
|
||||
return __LAST_DISK_RESULT
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -1365,6 +1372,7 @@ def renamer(old, new):
|
||||
|
||||
def remove_dir(path):
|
||||
""" Remove directory with retries for Win32 """
|
||||
logging.debug('Removing dir %s', path)
|
||||
if sabnzbd.WIN32:
|
||||
retries = 15
|
||||
while retries > 0:
|
||||
@@ -1393,6 +1401,7 @@ def remove_all(path, pattern='*', keep_folder=False, recursive=False):
|
||||
for f in files:
|
||||
if os.path.isfile(f):
|
||||
try:
|
||||
logging.debug('Removing file %s', f)
|
||||
os.remove(f)
|
||||
except:
|
||||
logging.info('Cannot remove file %s', f)
|
||||
@@ -1400,6 +1409,7 @@ def remove_all(path, pattern='*', keep_folder=False, recursive=False):
|
||||
remove_all(f, pattern, False, True)
|
||||
if not keep_folder:
|
||||
try:
|
||||
logging.debug('Removing dir %s', path)
|
||||
os.rmdir(path)
|
||||
except:
|
||||
logging.info('Cannot remove folder %s', path)
|
||||
@@ -1451,6 +1461,7 @@ def starts_with_path(path, prefix):
|
||||
def set_chmod(path, permissions, report):
|
||||
""" Set 'permissions' on 'path', report any errors when 'report' is True """
|
||||
try:
|
||||
logging.debug('Applying %s to %s', permissions, path)
|
||||
os.chmod(path, permissions)
|
||||
except:
|
||||
lpath = path.lower()
|
||||
|
||||
@@ -24,7 +24,7 @@ import sys
|
||||
import re
|
||||
import subprocess
|
||||
import logging
|
||||
from time import time
|
||||
import time
|
||||
import binascii
|
||||
import shutil
|
||||
|
||||
@@ -32,11 +32,11 @@ import sabnzbd
|
||||
from sabnzbd.encoding import TRANS, UNTRANS, unicoder, platform_encode, deunicode
|
||||
import sabnzbd.utils.rarfile as rarfile
|
||||
from sabnzbd.misc import format_time_string, find_on_path, make_script_path, int_conv, \
|
||||
flag_file, real_path, globber, globber_full, get_all_passwords, renamer, clip_path, \
|
||||
real_path, globber, globber_full, get_all_passwords, renamer, clip_path, \
|
||||
has_win_device, calc_age
|
||||
from sabnzbd.tvsort import SeriesSorter
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.constants import Status, QCHECK_FILE, RENAMES_FILE
|
||||
from sabnzbd.constants import Status
|
||||
|
||||
if sabnzbd.WIN32:
|
||||
try:
|
||||
@@ -45,15 +45,18 @@ if sabnzbd.WIN32:
|
||||
from win32process import STARTF_USESHOWWINDOW, IDLE_PRIORITY_CLASS
|
||||
except ImportError:
|
||||
pass
|
||||
# Load the POpen from the fixed unicode-subprocess
|
||||
from sabnzbd.utils.subprocess_fix import Popen
|
||||
else:
|
||||
# Define dummy WindowsError for non-Windows
|
||||
class WindowsError(Exception):
|
||||
|
||||
def __init__(self, value):
|
||||
self.parameter = value
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.parameter)
|
||||
# Load the regular POpen
|
||||
from subprocess import Popen
|
||||
|
||||
# Regex globals
|
||||
RAR_RE = re.compile(r'\.(?P<ext>part\d*\.rar|rar|r\d\d|s\d\d|t\d\d|u\d\d|v\d\d|\d\d\d)$', re.I)
|
||||
@@ -70,7 +73,6 @@ FULLVOLPAR2_RE = re.compile(r'(.*[^.])(\.*vol[0-9]+\+[0-9]+\.par2)', re.I)
|
||||
TS_RE = re.compile(r'\.(\d+)\.(ts$)', re.I)
|
||||
|
||||
PAR2_COMMAND = None
|
||||
PAR2C_COMMAND = None
|
||||
MULTIPAR_COMMAND = None
|
||||
RAR_COMMAND = None
|
||||
NICE_COMMAND = None
|
||||
@@ -92,7 +94,6 @@ def find_programs(curdir):
|
||||
return None
|
||||
|
||||
if sabnzbd.DARWIN:
|
||||
sabnzbd.newsunpack.PAR2C_COMMAND = check(curdir, 'osx/par2/par2-classic')
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'osx/par2/par2-sl64')
|
||||
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'osx/unrar/unrar')
|
||||
sabnzbd.newsunpack.SEVEN_COMMAND = check(curdir, 'osx/7zip/7za')
|
||||
@@ -100,15 +101,13 @@ def find_programs(curdir):
|
||||
if sabnzbd.WIN32:
|
||||
if sabnzbd.WIN64:
|
||||
# 64 bit versions
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'win/par2/x64/par2.exe')
|
||||
sabnzbd.newsunpack.MULTIPAR_COMMAND = check(curdir, 'win/par2/multipar/par2j64.exe')
|
||||
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'win/unrar/x64/UnRAR.exe')
|
||||
else:
|
||||
# 32 bit versions
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'win/par2/par2.exe')
|
||||
sabnzbd.newsunpack.MULTIPAR_COMMAND = check(curdir, 'win/par2/multipar/par2j.exe')
|
||||
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'win/unrar/UnRAR.exe')
|
||||
sabnzbd.newsunpack.PAR2C_COMMAND = check(curdir, 'win/par2/par2cmdline.exe')
|
||||
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'win/par2/par2.exe')
|
||||
sabnzbd.newsunpack.ZIP_COMMAND = check(curdir, 'win/unzip/unzip.exe')
|
||||
sabnzbd.newsunpack.SEVEN_COMMAND = check(curdir, 'win/7zip/7za.exe')
|
||||
else:
|
||||
@@ -125,9 +124,6 @@ def find_programs(curdir):
|
||||
if not sabnzbd.newsunpack.SEVEN_COMMAND:
|
||||
sabnzbd.newsunpack.SEVEN_COMMAND = find_on_path('7z')
|
||||
|
||||
if not sabnzbd.newsunpack.PAR2C_COMMAND:
|
||||
sabnzbd.newsunpack.PAR2C_COMMAND = sabnzbd.newsunpack.PAR2_COMMAND
|
||||
|
||||
if not (sabnzbd.WIN32 or sabnzbd.DARWIN):
|
||||
# Run check on rar version
|
||||
version, original = unrar_check(sabnzbd.newsunpack.RAR_COMMAND)
|
||||
@@ -166,7 +162,7 @@ def external_processing(extern_proc, nzo, complete_dir, nicename, status):
|
||||
|
||||
logging.info('Running external script %s(%s, %s, %s, %s, %s, %s, %s, %s)',
|
||||
extern_proc, complete_dir, nzo.filename, nicename, '', nzo.cat, nzo.group, status, failure_url)
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, env=env, creationflags=creationflags)
|
||||
|
||||
@@ -207,7 +203,7 @@ def external_script(script, p1, p2, p3=None, p4=None):
|
||||
stup, need_shell, command, creationflags = build_command(command)
|
||||
env = create_env()
|
||||
logging.info('Running user script %s(%s, %s)', script, p1, p2)
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, env=env, creationflags=creationflags)
|
||||
except:
|
||||
@@ -475,27 +471,45 @@ def rar_unpack(nzo, workdir, workdir_complete, delete, one_folder, rars):
|
||||
else:
|
||||
extraction_path = os.path.split(rarpath)[0]
|
||||
|
||||
logging.info("Extracting rarfile %s (belonging to %s) to %s",
|
||||
rarpath, rar_set, extraction_path)
|
||||
# Is the direct-unpacker still running? We wait for it
|
||||
if nzo.direct_unpacker:
|
||||
while nzo.direct_unpacker.is_alive():
|
||||
logging.debug('DirectUnpacker still alive for %s', nzo)
|
||||
time.sleep(2)
|
||||
|
||||
try:
|
||||
fail, newfiles, rars = rar_extract(rarpath, len(rar_sets[rar_set]),
|
||||
one_folder, nzo, rar_set, extraction_path)
|
||||
# Was it aborted?
|
||||
if not nzo.pp_active:
|
||||
fail = 1
|
||||
break
|
||||
success = not fail
|
||||
except:
|
||||
success = False
|
||||
fail = True
|
||||
msg = sys.exc_info()[1]
|
||||
nzo.fail_msg = T('Unpacking failed, %s') % msg
|
||||
setname = nzo.final_name
|
||||
nzo.set_unpack_info('Unpack', T('[%s] Error "%s" while unpacking RAR files') % (unicoder(setname), msg))
|
||||
# Bump the file-lock in case it's stuck
|
||||
with nzo.direct_unpacker.next_file_lock:
|
||||
nzo.direct_unpacker.next_file_lock.notify()
|
||||
|
||||
logging.error(T('Error "%s" while running rar_unpack on %s'), msg, setname)
|
||||
logging.debug("Traceback: ", exc_info=True)
|
||||
# Did we already direct-unpack it? Not when recursive-unpacking
|
||||
if nzo.direct_unpacker and rar_set in nzo.direct_unpacker.success_sets:
|
||||
logging.info("Set %s completed by DirectUnpack", rar_set)
|
||||
fail = 0
|
||||
success = 1
|
||||
rars = rar_sets[rar_set]
|
||||
newfiles = globber(extraction_path)
|
||||
nzo.direct_unpacker.success_sets.remove(rar_set)
|
||||
else:
|
||||
logging.info("Extracting rarfile %s (belonging to %s) to %s",
|
||||
rarpath, rar_set, extraction_path)
|
||||
try:
|
||||
fail, newfiles, rars = rar_extract(rarpath, len(rar_sets[rar_set]),
|
||||
one_folder, nzo, rar_set, extraction_path)
|
||||
# Was it aborted?
|
||||
if not nzo.pp_active:
|
||||
fail = 1
|
||||
break
|
||||
success = not fail
|
||||
except:
|
||||
success = False
|
||||
fail = True
|
||||
msg = sys.exc_info()[1]
|
||||
nzo.fail_msg = T('Unpacking failed, %s') % msg
|
||||
setname = nzo.final_name
|
||||
nzo.set_unpack_info('Unpack', T('[%s] Error "%s" while unpacking RAR files') % (unicoder(setname), msg))
|
||||
|
||||
logging.error(T('Error "%s" while running rar_unpack on %s'), msg, setname)
|
||||
logging.debug("Traceback: ", exc_info=True)
|
||||
|
||||
if success:
|
||||
logging.debug('rar_unpack(): Rars: %s', rars)
|
||||
@@ -530,7 +544,6 @@ def rar_extract(rarfile_path, numrars, one_folder, nzo, setname, extraction_path
|
||||
with password tries
|
||||
Return fail==0(ok)/fail==1(error)/fail==2(wrong password), new_files, rars
|
||||
"""
|
||||
|
||||
fail = 0
|
||||
new_files = None
|
||||
rars = []
|
||||
@@ -555,7 +568,7 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
""" Unpack single rar set 'rarfile_path' to 'extraction_path'
|
||||
Return fail==0(ok)/fail==1(error)/fail==2(wrong password)/fail==3(crc-error), new_files, rars
|
||||
"""
|
||||
start = time()
|
||||
start = time.time()
|
||||
|
||||
logging.debug("rar_extract(): Extractionpath: %s", extraction_path)
|
||||
|
||||
@@ -579,13 +592,14 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
|
||||
if sabnzbd.WIN32:
|
||||
# Use all flags
|
||||
# See: https://github.com/sabnzbd/sabnzbd/pull/771
|
||||
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, rename, '-ai', password_command,
|
||||
'%s' % clip_path(rarfile_path), '%s\\' % extraction_path]
|
||||
if not has_win_device(rarfile_path):
|
||||
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, rename, '-ai', password_command,
|
||||
'%s' % clip_path(rarfile_path), clip_path(extraction_path)]
|
||||
else:
|
||||
# Need long-path notation in case of forbidden-names
|
||||
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, rename, '-ai', password_command,
|
||||
'%s' % clip_path(rarfile_path), '%s\\' % extraction_path]
|
||||
|
||||
# If this is the retry without leading \\.\, we need to remove the \ at the end (yes..)
|
||||
if not extraction_path.startswith('\\\\?\\'):
|
||||
command[-1] = command[-1][:-1]
|
||||
elif RAR_PROBLEM:
|
||||
# Use only oldest options (specifically no "-or")
|
||||
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, password_command,
|
||||
@@ -600,9 +614,12 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
|
||||
stup, need_shell, command, creationflags = build_command(command)
|
||||
|
||||
# Get list of all the volumes part of this set
|
||||
logging.debug("Analyzing rar file ... %s found", rarfile.is_rarfile(rarfile_path))
|
||||
rarfiles = rarfile.RarFile(rarfile_path).volumelist()
|
||||
|
||||
logging.debug("Running unrar %s", command)
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, creationflags=creationflags)
|
||||
|
||||
@@ -615,7 +632,6 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
# Loop over the output from rar!
|
||||
curr = 0
|
||||
extracted = []
|
||||
rarfiles = []
|
||||
fail = 0
|
||||
inrecovery = False
|
||||
lines = []
|
||||
@@ -638,9 +654,6 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
lines.append(line)
|
||||
|
||||
if line.startswith('Extracting from'):
|
||||
filename = TRANS((re.search(EXTRACTFROM_RE, line).group(1)))
|
||||
if filename not in rarfiles:
|
||||
rarfiles.append(filename)
|
||||
curr += 1
|
||||
nzo.set_action_line(T('Unpacking'), '%02d/%02d' % (curr, numrars))
|
||||
|
||||
@@ -683,12 +696,6 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
logging.error(T('ERROR: write error (%s)'), line[11:])
|
||||
fail = 1
|
||||
|
||||
elif line.startswith('Cannot create') and sabnzbd.WIN32 and extraction_path.startswith('\\\\?\\'):
|
||||
# Can be due to Unicode problems on Windows, let's retry
|
||||
fail = 4
|
||||
# Kill the process (can stay in endless loop on Windows Server)
|
||||
p.kill()
|
||||
|
||||
elif line.startswith('Cannot create'):
|
||||
line2 = proc.readline()
|
||||
if 'must not exceed 260' in line2:
|
||||
@@ -701,6 +708,8 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
logging.error(T('ERROR: write error (%s)'), unicoder(line[13:]))
|
||||
nzo.set_unpack_info('Unpack', unicoder(msg))
|
||||
fail = 1
|
||||
# Kill the process (can stay in endless loop on Windows Server)
|
||||
p.kill()
|
||||
|
||||
elif line.startswith('ERROR: '):
|
||||
nzo.fail_msg = T('Unpacking failed, see log')
|
||||
@@ -763,13 +772,7 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
proc.close()
|
||||
p.wait()
|
||||
logging.debug('UNRAR output %s', '\n'.join(lines))
|
||||
|
||||
# Unicode problems, lets start again but now we try without \\?\
|
||||
# This will only fail if the download contains forbidden-Windows-names
|
||||
if fail == 4:
|
||||
return rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, clip_path(extraction_path), password)
|
||||
else:
|
||||
return fail, (), ()
|
||||
return fail, (), ()
|
||||
|
||||
if proc:
|
||||
proc.close()
|
||||
@@ -777,7 +780,7 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
|
||||
|
||||
logging.debug('UNRAR output %s', '\n'.join(lines))
|
||||
nzo.fail_msg = ''
|
||||
msg = T('Unpacked %s files/folders in %s') % (str(len(extracted)), format_time_string(time() - start))
|
||||
msg = T('Unpacked %s files/folders in %s') % (str(len(extracted)), format_time_string(time.time() - start))
|
||||
nzo.set_unpack_info('Unpack', '[%s] %s' % (unicoder(setname), msg))
|
||||
logging.info('%s', msg)
|
||||
|
||||
@@ -795,7 +798,7 @@ def unzip(nzo, workdir, workdir_complete, delete, one_folder, zips):
|
||||
try:
|
||||
i = 0
|
||||
unzip_failed = False
|
||||
tms = time()
|
||||
tms = time.time()
|
||||
|
||||
for _zip in zips:
|
||||
logging.info("Starting extract on zipfile: %s ", _zip)
|
||||
@@ -811,7 +814,7 @@ def unzip(nzo, workdir, workdir_complete, delete, one_folder, zips):
|
||||
else:
|
||||
i += 1
|
||||
|
||||
msg = T('%s files in %s') % (str(i), format_time_string(time() - tms))
|
||||
msg = T('%s files in %s') % (str(i), format_time_string(time.time() - tms))
|
||||
nzo.set_unpack_info('Unpack', msg)
|
||||
|
||||
# Delete the old files if we have to
|
||||
@@ -854,7 +857,7 @@ def ZIP_Extract(zipfile, extraction_path, one_folder):
|
||||
|
||||
stup, need_shell, command, creationflags = build_command(command)
|
||||
logging.debug('Starting unzip: %s', command)
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, creationflags=creationflags)
|
||||
|
||||
@@ -875,7 +878,7 @@ def unseven(nzo, workdir, workdir_complete, delete, one_folder, sevens):
|
||||
"""
|
||||
i = 0
|
||||
unseven_failed = False
|
||||
tms = time()
|
||||
tms = time.time()
|
||||
|
||||
# Find multi-volume sets, because 7zip will not provide actual set members
|
||||
sets = {}
|
||||
@@ -909,7 +912,7 @@ def unseven(nzo, workdir, workdir_complete, delete, one_folder, sevens):
|
||||
i += 1
|
||||
|
||||
if not unseven_failed:
|
||||
msg = T('%s files in %s') % (str(i), format_time_string(time() - tms))
|
||||
msg = T('%s files in %s') % (str(i), format_time_string(time.time() - tms))
|
||||
nzo.set_unpack_info('Unpack', msg)
|
||||
|
||||
return unseven_failed
|
||||
@@ -976,7 +979,7 @@ def seven_extract_core(sevenset, extensions, extraction_path, one_folder, delete
|
||||
|
||||
stup, need_shell, command, creationflags = build_command(command)
|
||||
logging.debug('Starting 7za: %s', command)
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, creationflags=creationflags)
|
||||
|
||||
@@ -1020,6 +1023,9 @@ def par2_repair(parfile_nzf, nzo, workdir, setname, single):
|
||||
if os.path.exists(test_parfile):
|
||||
parfile_nzf = new_par
|
||||
break
|
||||
else:
|
||||
# No file was found, we assume this set already finished
|
||||
return False, True
|
||||
|
||||
# Shorten just the workdir on Windows
|
||||
parfile = os.path.join(workdir, parfile_nzf.filename)
|
||||
@@ -1053,7 +1059,6 @@ def par2_repair(parfile_nzf, nzo, workdir, setname, single):
|
||||
return readd, result
|
||||
|
||||
if not result:
|
||||
flag_file(workdir, QCHECK_FILE, True)
|
||||
nzo.status = Status.REPAIRING
|
||||
result = False
|
||||
readd = False
|
||||
@@ -1073,8 +1078,7 @@ def par2_repair(parfile_nzf, nzo, workdir, setname, single):
|
||||
|
||||
if finished:
|
||||
result = True
|
||||
logging.info('Par verify finished ok on %s!',
|
||||
parfile)
|
||||
logging.info('Par verify finished ok on %s!', parfile)
|
||||
|
||||
# Remove this set so we don't try to check it again
|
||||
nzo.remove_parset(parfile_nzf.setname)
|
||||
@@ -1097,45 +1101,20 @@ def par2_repair(parfile_nzf, nzo, workdir, setname, single):
|
||||
|
||||
try:
|
||||
if cfg.enable_par_cleanup():
|
||||
deletables = []
|
||||
new_dir_content = os.listdir(workdir)
|
||||
|
||||
# Remove extra files created during repair and par2 base files
|
||||
for path in new_dir_content:
|
||||
if os.path.splitext(path)[1] == '.1' and path not in old_dir_content:
|
||||
try:
|
||||
path = os.path.join(workdir, path)
|
||||
deletables.append(os.path.join(workdir, path))
|
||||
deletables.append(os.path.join(workdir, setname + '.par2'))
|
||||
deletables.append(os.path.join(workdir, setname + '.PAR2'))
|
||||
deletables.append(parfile)
|
||||
|
||||
logging.info("Deleting %s", path)
|
||||
os.remove(path)
|
||||
except:
|
||||
logging.warning(T('Deleting %s failed!'), path)
|
||||
|
||||
path = os.path.join(workdir, setname + '.par2')
|
||||
path2 = os.path.join(workdir, setname + '.PAR2')
|
||||
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
logging.info("Deleting %s", path)
|
||||
os.remove(path)
|
||||
except:
|
||||
logging.warning(T('Deleting %s failed!'), path)
|
||||
|
||||
if os.path.exists(path2):
|
||||
try:
|
||||
logging.info("Deleting %s", path2)
|
||||
os.remove(path2)
|
||||
except:
|
||||
logging.warning(T('Deleting %s failed!'), path2)
|
||||
|
||||
if os.path.exists(parfile):
|
||||
try:
|
||||
logging.info("Deleting %s", parfile)
|
||||
os.remove(parfile)
|
||||
except OSError:
|
||||
logging.warning(T('Deleting %s failed!'), parfile)
|
||||
|
||||
deletables = []
|
||||
# Add output of par2-repair to remove
|
||||
deletables.extend(used_joinables)
|
||||
deletables.extend(used_for_repair)
|
||||
deletables.extend([os.path.join(workdir, f) for f in used_for_repair])
|
||||
|
||||
# Delete pars of the set and maybe extra ones that par2 found
|
||||
deletables.extend([os.path.join(workdir, f) for f in setpars])
|
||||
@@ -1164,36 +1143,17 @@ _RE_LOADING_PAR2 = re.compile(r'Loading "([^"]+)"\.')
|
||||
_RE_LOADED_PAR2 = re.compile(r'Loaded (\d+) new packets')
|
||||
|
||||
|
||||
def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, single=False):
|
||||
def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, single=False):
|
||||
""" Run par2 on par-set """
|
||||
retry_classic = False
|
||||
used_joinables = []
|
||||
used_for_repair = []
|
||||
extra_par2_name = None
|
||||
# set the current nzo status to "Verifying...". Used in History
|
||||
nzo.status = Status.VERIFYING
|
||||
start = time()
|
||||
start = time.time()
|
||||
|
||||
options = cfg.par_option().strip()
|
||||
|
||||
classic = classic or not cfg.par2_multicore()
|
||||
if sabnzbd.WIN32:
|
||||
# If filenames are UTF-8 then we must use par2-tbb, unless this is a retry with classic
|
||||
tbb = (sabnzbd.assembler.GetMD5Hashes(parfile, True)[1] and not classic) or not PAR2C_COMMAND
|
||||
else:
|
||||
tbb = False
|
||||
if tbb and options:
|
||||
command = [str(PAR2_COMMAND), 'r', options, parfile]
|
||||
else:
|
||||
if classic:
|
||||
command = [str(PAR2C_COMMAND), 'r', parfile]
|
||||
else:
|
||||
command = [str(PAR2_COMMAND), 'r', parfile]
|
||||
|
||||
# Allow options if not classic or when classic and non-classic are the same
|
||||
if (not classic or (PAR2_COMMAND == PAR2C_COMMAND)):
|
||||
command.insert(2, options)
|
||||
|
||||
logging.debug('Par2-classic/cmdline = %s', classic)
|
||||
command = [str(PAR2_COMMAND), 'r', options, parfile]
|
||||
|
||||
# Append the wildcard for this set
|
||||
parfolder = os.path.split(parfile)[0]
|
||||
@@ -1214,7 +1174,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
# We need to check for the bad par2cmdline that skips blocks
|
||||
# Or the one that complains about basepath
|
||||
# Only if we're not doing multicore
|
||||
if not tbb:
|
||||
if not sabnzbd.WIN32 and not sabnzbd.DARWIN:
|
||||
par2text = run_simple([command[0], '-h'])
|
||||
if 'No data skipping' in par2text:
|
||||
logging.info('Detected par2cmdline version that skips blocks, adding -N parameter')
|
||||
@@ -1227,19 +1187,15 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
stup, need_shell, command, creationflags = build_command(command)
|
||||
|
||||
# par2multicore wants to see \\.\ paths on Windows
|
||||
# But par2cmdline doesn't support that notation, or \\?\ notation
|
||||
# See: https://github.com/sabnzbd/sabnzbd/pull/771
|
||||
if sabnzbd.WIN32 and (tbb or has_win_device(parfile)):
|
||||
if sabnzbd.WIN32:
|
||||
command = [x.replace('\\\\?\\', '\\\\.\\', 1) if x.startswith('\\\\?\\') else x for x in command]
|
||||
elif sabnzbd.WIN32:
|
||||
# For par2cmdline on Windows we need clipped paths
|
||||
command = [clip_path(x) if x.startswith('\\\\?\\') else x for x in command]
|
||||
|
||||
# Run the external command
|
||||
logging.debug('Starting par2: %s', command)
|
||||
lines = []
|
||||
try:
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, creationflags=creationflags)
|
||||
|
||||
@@ -1298,7 +1254,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
if extra_par2_name and line.startswith('Loaded '):
|
||||
m = _RE_LOADED_PAR2.search(line)
|
||||
if m and int(m.group(1)) > 0:
|
||||
used_for_repair.append(os.path.join(nzo.downpath, extra_par2_name))
|
||||
used_for_repair.append(extra_par2_name)
|
||||
extra_par2_name = None
|
||||
continue
|
||||
extra_par2_name = None
|
||||
@@ -1310,18 +1266,18 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
logging.error(msg)
|
||||
|
||||
elif line.startswith('All files are correct'):
|
||||
msg = T('[%s] Verified in %s, all files correct') % (unicoder(setname), format_time_string(time() - start))
|
||||
msg = T('[%s] Verified in %s, all files correct') % (unicoder(setname), format_time_string(time.time() - start))
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
logging.info('Verified in %s, all files correct',
|
||||
format_time_string(time() - start))
|
||||
format_time_string(time.time() - start))
|
||||
finished = 1
|
||||
|
||||
elif line.startswith('Repair is required'):
|
||||
msg = T('[%s] Verified in %s, repair is required') % (unicoder(setname), format_time_string(time() - start))
|
||||
msg = T('[%s] Verified in %s, repair is required') % (unicoder(setname), format_time_string(time.time() - start))
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
logging.info('Verified in %s, repair is required',
|
||||
format_time_string(time() - start))
|
||||
start = time()
|
||||
format_time_string(time.time() - start))
|
||||
start = time.time()
|
||||
verified = 1
|
||||
|
||||
elif line.startswith('Loading "'):
|
||||
@@ -1362,22 +1318,6 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
nzo.status = Status.FAILED
|
||||
|
||||
elif line.startswith('You need'):
|
||||
# Because par2cmdline doesn't handle split files correctly
|
||||
# if there are joinables, let's join them first and try again
|
||||
# Only when in the par2-detection also only 1 output-file was mentioned
|
||||
if joinables and len(datafiles) == 1:
|
||||
error, newf = file_join(nzo, parfolder, parfolder, True, joinables)
|
||||
# Only do it again if we had a good join
|
||||
if newf:
|
||||
retry_classic = True
|
||||
# Save the renames in case of retry
|
||||
for jn in joinables:
|
||||
renames[datafiles[0]] = os.path.split(jn)[1]
|
||||
joinables = []
|
||||
# Need to set it to 1 so the renames get saved
|
||||
finished = 1
|
||||
break
|
||||
|
||||
chunks = line.split()
|
||||
needed_blocks = int(chunks[2])
|
||||
avail_blocks = 0
|
||||
@@ -1447,7 +1387,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
nzo.status = Status.FAILED
|
||||
|
||||
elif line.startswith('Repair is possible'):
|
||||
start = time()
|
||||
start = time.time()
|
||||
nzo.set_action_line(T('Repairing'), '%2d%%' % (0))
|
||||
|
||||
elif line.startswith('Repairing:'):
|
||||
@@ -1457,9 +1397,9 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
nzo.status = Status.REPAIRING
|
||||
|
||||
elif line.startswith('Repair complete'):
|
||||
msg = T('[%s] Repaired in %s') % (unicoder(setname), format_time_string(time() - start))
|
||||
msg = T('[%s] Repaired in %s') % (unicoder(setname), format_time_string(time.time() - start))
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
logging.info('Repaired in %s', format_time_string(time() - start))
|
||||
logging.info('Repaired in %s', format_time_string(time.time() - start))
|
||||
finished = 1
|
||||
|
||||
elif line.startswith('File:') and line.find('data blocks from') > 0:
|
||||
@@ -1483,27 +1423,20 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
logging.debug('PAR2 will reconstruct "%s" from "%s"', new_name, old_name)
|
||||
reconstructed.append(os.path.join(workdir, old_name))
|
||||
|
||||
elif 'Could not write' in line and 'at offset 0:' in line and not classic:
|
||||
elif 'Could not write' in line and 'at offset 0:' in line:
|
||||
# If there are joinables, this error will only happen in case of 100% complete files
|
||||
# We can just skip the retry, because par2cmdline will fail in those cases
|
||||
# becauses it refuses to scan the ".001" file
|
||||
if joinables:
|
||||
finished = 1
|
||||
used_joinables = []
|
||||
else:
|
||||
# Hit a bug in par2-tbb, retry with par2-classic
|
||||
retry_classic = sabnzbd.WIN32
|
||||
|
||||
elif ' cannot be renamed to ' in line:
|
||||
if not classic and sabnzbd.WIN32:
|
||||
# Hit a bug in par2-tbb, retry with par2-classic
|
||||
retry_classic = True
|
||||
else:
|
||||
msg = unicoder(line.strip())
|
||||
nzo.fail_msg = msg
|
||||
msg = u'[%s] %s' % (unicoder(setname), msg)
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
nzo.status = Status.FAILED
|
||||
msg = unicoder(line.strip())
|
||||
nzo.fail_msg = msg
|
||||
msg = u'[%s] %s' % (unicoder(setname), msg)
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
nzo.status = Status.FAILED
|
||||
|
||||
elif 'There is not enough space on the disk' in line:
|
||||
# Oops, disk is full!
|
||||
@@ -1570,47 +1503,36 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
|
||||
|
||||
p.wait()
|
||||
except WindowsError, err:
|
||||
if err[0] == '87' and not classic:
|
||||
# Hit a bug in par2-tbb, retry with par2-classic
|
||||
retry_classic = True
|
||||
else:
|
||||
raise WindowsError(err)
|
||||
raise WindowsError(err)
|
||||
|
||||
logging.debug('PAR2 output was\n%s', '\n'.join(lines))
|
||||
|
||||
# If successful, add renamed files to the collection
|
||||
if finished and renames:
|
||||
previous = sabnzbd.load_data(RENAMES_FILE, nzo.workpath, remove=False)
|
||||
for name in previous or {}:
|
||||
renames[name] = previous[name]
|
||||
sabnzbd.save_data(renames, RENAMES_FILE, nzo.workpath)
|
||||
nzo.renamed_file(renames)
|
||||
|
||||
# If successful and files were reconstructed, remove incomplete original files
|
||||
if finished and reconstructed:
|
||||
# Use 'used_joinables' as a vehicle to get rid of the files
|
||||
used_joinables.extend(reconstructed)
|
||||
|
||||
if retry_classic:
|
||||
logging.debug('Retry PAR2-joining with par2-classic/cmdline')
|
||||
return PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=True, single=single)
|
||||
else:
|
||||
return finished, readd, pars, datafiles, used_joinables, used_for_repair
|
||||
return finished, readd, pars, datafiles, used_joinables, used_for_repair
|
||||
|
||||
_RE_FILENAME = re.compile(r'"([^"]+)"')
|
||||
|
||||
def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, single=False):
|
||||
def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, single=False):
|
||||
""" Run par2 on par-set """
|
||||
parfolder = os.path.split(parfile)[0]
|
||||
used_joinables = []
|
||||
used_for_repair = []
|
||||
|
||||
# set the current nzo status to "Verifying...". Used in History
|
||||
nzo.status = Status.VERIFYING
|
||||
start = time()
|
||||
start = time.time()
|
||||
|
||||
# Can implement caching of verification by adding:
|
||||
# '-vs2', '-vd%s' % parfolder
|
||||
# Caching of verification implemented by adding:
|
||||
# But not really required due to prospective-par2
|
||||
command = [str(MULTIPAR_COMMAND), 'r', parfile]
|
||||
command = [str(MULTIPAR_COMMAND), 'r', '-vs2', '-vd%s' % parfolder, parfile]
|
||||
|
||||
# Only add user-options if supplied
|
||||
options = cfg.par_option().strip()
|
||||
@@ -1618,7 +1540,6 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
command.insert(2, options)
|
||||
|
||||
# Append the wildcard for this set
|
||||
parfolder = os.path.split(parfile)[0]
|
||||
if single or len(globber(parfolder, setname + '*')) < 2:
|
||||
# Support bizarre naming conventions
|
||||
wildcard = '*'
|
||||
@@ -1632,7 +1553,7 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
logging.info('Starting MultiPar: %s', command)
|
||||
|
||||
lines = []
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, creationflags=creationflags)
|
||||
|
||||
@@ -1658,6 +1579,7 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
in_check = False
|
||||
in_verify = False
|
||||
in_repair = False
|
||||
in_verify_repaired = False
|
||||
misnamed_files = False
|
||||
old_name = None
|
||||
|
||||
@@ -1687,8 +1609,6 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
|
||||
# Skip empty lines
|
||||
if line == '':
|
||||
# Empty lines also end every section
|
||||
in_repair = False
|
||||
continue
|
||||
|
||||
# Save it all
|
||||
@@ -1740,7 +1660,7 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
nzo.status = Status.FAILED
|
||||
|
||||
# ----------------- Verify stage
|
||||
# ----------------- Start check/verify stage
|
||||
# List of Par2 files we will use today
|
||||
if line.startswith('PAR File list'):
|
||||
in_parlist = True
|
||||
@@ -1750,9 +1670,20 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
elif in_parlist:
|
||||
m = _RE_FILENAME.search(line)
|
||||
if m:
|
||||
used_for_repair.append(os.path.join(nzo.downpath, TRANS(m.group(1))))
|
||||
used_for_repair.append(TRANS(m.group(1)))
|
||||
pars.append(TRANS(m.group(1)))
|
||||
|
||||
elif line.startswith('Recovery Set ID'):
|
||||
# Remove files were MultiPar stores verification result when repaired succesfull
|
||||
recovery_id = line.split()[-1]
|
||||
used_for_repair.append('2_%s.bin' % recovery_id)
|
||||
used_for_repair.append('2_%s.ini' % recovery_id)
|
||||
|
||||
elif line.startswith('Input File total count'):
|
||||
# How many files will it try to find?
|
||||
verifytotal = int(line.split()[-1])
|
||||
|
||||
# ----------------- Misnamed-detection stage
|
||||
# Misnamed files
|
||||
elif line.startswith('Searching misnamed file'):
|
||||
# We are in the misnamed files block
|
||||
@@ -1776,22 +1707,31 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
datafiles.append(new_name)
|
||||
reconstructed.append(old_name)
|
||||
|
||||
# How many files will it try to find?
|
||||
elif line.startswith('Input File total count'):
|
||||
verifytotal = int(line.split()[-1])
|
||||
|
||||
# ----------------- Checking stage
|
||||
# Checking input files
|
||||
elif line.startswith('Complete file count'):
|
||||
in_check = False
|
||||
verifynum = 0
|
||||
old_name = None
|
||||
elif line.startswith('Verifying Input File'):
|
||||
in_check = True
|
||||
nzo.status = Status.VERIFYING
|
||||
elif in_check:
|
||||
m = _RE_FILENAME.search(line)
|
||||
if m:
|
||||
verifynum += 1
|
||||
# Only increase counter if it was really the detection line
|
||||
if line.startswith('= ') or '%' not in line:
|
||||
verifynum += 1
|
||||
nzo.set_action_line(T('Checking'), '%02d/%02d' % (verifynum, verifytotal))
|
||||
old_name = TRANS(m.group(1))
|
||||
|
||||
# ----------------- Verify stage
|
||||
# Which files need extra verification?
|
||||
elif line.startswith('Damaged file count'):
|
||||
verifytotal = int(line.split()[-1])
|
||||
|
||||
elif line.startswith('Missing file count'):
|
||||
verifytotal += int(line.split()[-1])
|
||||
|
||||
# Actual verification
|
||||
elif line.startswith('Input File Slice found'):
|
||||
@@ -1833,7 +1773,7 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
datafiles.pop()
|
||||
datafiles.append(new_name)
|
||||
# Need to remove the old file after repair (Multipar keeps it)
|
||||
used_for_repair.append(os.path.join(parfolder, old_name))
|
||||
used_for_repair.append(old_name)
|
||||
# Need to reset it to avoid collision
|
||||
old_name = None
|
||||
|
||||
@@ -1912,19 +1852,20 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
# Result of verification
|
||||
elif line.startswith('All Files Complete'):
|
||||
# Completed without damage!
|
||||
msg = T('[%s] Verified in %s, all files correct') % (unicoder(setname), format_time_string(time() - start))
|
||||
msg = T('[%s] Verified in %s, all files correct') % (unicoder(setname), format_time_string(time.time() - start))
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
logging.info('Verified in %s, all files correct',
|
||||
format_time_string(time() - start))
|
||||
format_time_string(time.time() - start))
|
||||
finished = 1
|
||||
|
||||
elif line.startswith(('Ready to repair', 'Ready to rejoin')):
|
||||
# Ready to repair!
|
||||
# Or we are re-joining a split file when there's no damage but takes time
|
||||
msg = T('[%s] Verified in %s, repair is required') % (unicoder(setname), format_time_string(time() - start))
|
||||
msg = T('[%s] Verified in %s, repair is required') % (unicoder(setname), format_time_string(time.time() - start))
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
logging.info('Verified in %s, repair is required',
|
||||
format_time_string(time() - start))
|
||||
start = time()
|
||||
format_time_string(time.time() - start))
|
||||
start = time.time()
|
||||
|
||||
# Set message for user in case of joining
|
||||
if line.startswith('Ready to rejoin'):
|
||||
@@ -1933,9 +1874,17 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
# ----------------- Repair stage
|
||||
elif 'Recovering slice' in line:
|
||||
# Before this it will calculate matrix, here is where it starts
|
||||
start = time()
|
||||
start = time.time()
|
||||
in_repair = True
|
||||
nzo.set_action_line(T('Repairing'), '%2d%%' % (0))
|
||||
|
||||
elif in_repair and line.startswith('Verifying repair'):
|
||||
in_repair = False
|
||||
in_verify_repaired = True
|
||||
# How many will be checked?
|
||||
verifytotal = int(line.split()[-1])
|
||||
verifynum = 0
|
||||
|
||||
elif in_repair:
|
||||
# Line with percentage of repair (nothing else)
|
||||
per = float(line[:-1])
|
||||
@@ -1943,11 +1892,17 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
nzo.status = Status.REPAIRING
|
||||
|
||||
elif line.startswith('Repaired successfully'):
|
||||
msg = T('[%s] Repaired in %s') % (unicoder(setname), format_time_string(time() - start))
|
||||
msg = T('[%s] Repaired in %s') % (unicoder(setname), format_time_string(time.time() - start))
|
||||
nzo.set_unpack_info('Repair', msg)
|
||||
logging.info('Repaired in %s', format_time_string(time() - start))
|
||||
logging.info('Repaired in %s', format_time_string(time.time() - start))
|
||||
finished = 1
|
||||
|
||||
elif in_verify_repaired and line.startswith('Repaired :'):
|
||||
# Track verification of repaired files (can sometimes take a while)
|
||||
verifynum += 1
|
||||
nzo.set_action_line(T('Verifying repair'), '%02d/%02d' % (verifynum, verifytotal))
|
||||
|
||||
|
||||
p.wait()
|
||||
|
||||
logging.debug('MultiPar output was\n%s', '\n'.join(lines))
|
||||
@@ -1957,15 +1912,12 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
|
||||
# Even if the repair did not complete fully it will rename those!
|
||||
# But the ones in 'Finding available slices'-section will only be renamed after succesfull repair
|
||||
if renames:
|
||||
# Adding to the collection
|
||||
previous = sabnzbd.load_data(RENAMES_FILE, nzo.workpath, remove=False)
|
||||
for name in previous or {}:
|
||||
renames[name] = previous[name]
|
||||
sabnzbd.save_data(renames, RENAMES_FILE, nzo.workpath)
|
||||
|
||||
# If succes, we also remove the possibly previously renamed ones
|
||||
if finished and previous:
|
||||
reconstructed.extend(previous.values())
|
||||
if finished:
|
||||
reconstructed.extend(nzo.renames)
|
||||
|
||||
# Adding to the collection
|
||||
nzo.renamed_file(renames)
|
||||
|
||||
# Remove renamed original files
|
||||
workdir = os.path.split(parfile)[0]
|
||||
@@ -2029,10 +1981,6 @@ def userxbit(filename):
|
||||
|
||||
def build_command(command):
|
||||
""" Prepare list from running an external program """
|
||||
for n in xrange(len(command)):
|
||||
if isinstance(command[n], unicode):
|
||||
command[n] = deunicode(command[n])
|
||||
|
||||
if not sabnzbd.WIN32:
|
||||
if command[0].endswith('.py'):
|
||||
with open(command[0], 'r') as script_file:
|
||||
@@ -2222,10 +2170,8 @@ def QuickCheck(set, nzo):
|
||||
|
||||
# Save renames
|
||||
if renames:
|
||||
previous = sabnzbd.load_data(RENAMES_FILE, nzo.workpath, remove=False)
|
||||
for name in previous or {}:
|
||||
renames[name] = previous[name]
|
||||
sabnzbd.save_data(renames, RENAMES_FILE, nzo.workpath)
|
||||
nzo.renamed_file(renames)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -2356,7 +2302,7 @@ def pre_queue(name, pp, cat, script, priority, size, groups):
|
||||
stup, need_shell, command, creationflags = build_command(command)
|
||||
env = create_env()
|
||||
logging.info('Running pre-queue script %s', command)
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, env=env, creationflags=creationflags)
|
||||
except:
|
||||
@@ -2426,7 +2372,7 @@ class SevenZip(object):
|
||||
command = [SEVEN_COMMAND, 'l', '-p', '-y', '-slt', self.path]
|
||||
stup, need_shell, command, creationflags = build_command(command)
|
||||
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
startupinfo=stup, creationflags=creationflags)
|
||||
|
||||
@@ -2453,7 +2399,7 @@ class SevenZip(object):
|
||||
else:
|
||||
stderr = open('/dev/null', 'w')
|
||||
|
||||
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=stderr,
|
||||
startupinfo=stup, creationflags=creationflags)
|
||||
|
||||
@@ -2469,7 +2415,7 @@ class SevenZip(object):
|
||||
|
||||
def run_simple(cmd):
|
||||
""" Run simple external command and return output """
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
txt = p.stdout.read()
|
||||
p.wait()
|
||||
return txt
|
||||
|
||||
@@ -39,8 +39,9 @@ from sabnzbd.constants import NOTIFY_KEYS
|
||||
from sabnzbd.misc import split_host, make_script_path
|
||||
from sabnzbd.newsunpack import external_script
|
||||
|
||||
from gntp import GNTPRegister
|
||||
from gntp.core import GNTPRegister
|
||||
from gntp.notifier import GrowlNotifier
|
||||
import gntp.errors
|
||||
try:
|
||||
import Growl
|
||||
# Detect classic Growl (older than 1.3)
|
||||
@@ -58,6 +59,7 @@ try:
|
||||
except:
|
||||
_HAVE_NTFOSD = False
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Define translatable message table
|
||||
##############################################################################
|
||||
@@ -89,13 +91,9 @@ def get_icon():
|
||||
if not os.path.isfile(icon):
|
||||
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico')
|
||||
if os.path.isfile(icon):
|
||||
if sabnzbd.WIN32 or sabnzbd.DARWIN:
|
||||
fp = open(icon, 'rb')
|
||||
icon = fp.read()
|
||||
fp.close()
|
||||
else:
|
||||
# Due to a bug in GNTP, need this work-around for Linux/Unix
|
||||
icon = 'http://sabnzbdplus.sourceforge.net/version/sabnzbd.ico'
|
||||
fp = open(icon, 'rb')
|
||||
icon = fp.read()
|
||||
fp.close()
|
||||
else:
|
||||
icon = None
|
||||
return icon
|
||||
@@ -122,7 +120,7 @@ def check_classes(gtype, section):
|
||||
|
||||
|
||||
def get_prio(gtype, section):
|
||||
""" Check if `gtype` is enabled in `section` """
|
||||
""" Check prio of `gtype` in `section` """
|
||||
try:
|
||||
return sabnzbd.config.get_config(section, '%s_prio_%s' % (section, gtype))()
|
||||
except TypeError:
|
||||
@@ -130,20 +128,32 @@ def get_prio(gtype, section):
|
||||
return -1000
|
||||
|
||||
|
||||
def send_notification(title, msg, gtype):
|
||||
def check_cat(section, job_cat):
|
||||
""" Check if `job_cat` is enabled in `section`. * = All """
|
||||
if not job_cat:
|
||||
return True
|
||||
try:
|
||||
section_cats = sabnzbd.config.get_config(section, '%s_cats' % section)()
|
||||
return ('*' in section_cats or job_cat in section_cats)
|
||||
except TypeError:
|
||||
logging.debug('Incorrect Notify option %s:%s_prio_%s', section, section, gtype)
|
||||
return False
|
||||
|
||||
|
||||
def send_notification(title, msg, gtype, job_cat=None):
|
||||
""" Send Notification message """
|
||||
# Notification Center
|
||||
if sabnzbd.DARWIN and sabnzbd.cfg.ncenter_enable():
|
||||
if check_classes(gtype, 'ncenter'):
|
||||
if check_classes(gtype, 'ncenter') and check_cat('ncenter', job_cat):
|
||||
send_notification_center(title, msg, gtype)
|
||||
|
||||
# Windows
|
||||
if sabnzbd.WIN32 and sabnzbd.cfg.acenter_enable():
|
||||
if check_classes(gtype, 'acenter'):
|
||||
if check_classes(gtype, 'acenter') and check_cat('acenter', job_cat):
|
||||
send_windows(title, msg, gtype)
|
||||
|
||||
# Growl
|
||||
if sabnzbd.cfg.growl_enable() and check_classes(gtype, 'growl'):
|
||||
if sabnzbd.cfg.growl_enable() and check_classes(gtype, 'growl') and check_cat('growl', job_cat):
|
||||
if _HAVE_CLASSIC_GROWL and not sabnzbd.cfg.growl_server():
|
||||
return send_local_growl(title, msg, gtype)
|
||||
else:
|
||||
@@ -151,32 +161,33 @@ def send_notification(title, msg, gtype):
|
||||
time.sleep(0.5)
|
||||
|
||||
# Prowl
|
||||
if sabnzbd.cfg.prowl_enable():
|
||||
if sabnzbd.cfg.prowl_enable() and check_cat('prowl', job_cat):
|
||||
if sabnzbd.cfg.prowl_apikey():
|
||||
Thread(target=send_prowl, args=(title, msg, gtype)).start()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Pushover
|
||||
if sabnzbd.cfg.pushover_enable():
|
||||
if sabnzbd.cfg.pushover_enable() and check_cat('pushover', job_cat):
|
||||
if sabnzbd.cfg.pushover_token():
|
||||
Thread(target=send_pushover, args=(title, msg, gtype)).start()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Pushbullet
|
||||
if sabnzbd.cfg.pushbullet_enable():
|
||||
if sabnzbd.cfg.pushbullet_enable() and check_cat('pushbullet', job_cat):
|
||||
if sabnzbd.cfg.pushbullet_apikey() and check_classes(gtype, 'pushbullet'):
|
||||
Thread(target=send_pushbullet, args=(title, msg, gtype)).start()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Notification script.
|
||||
if sabnzbd.cfg.nscript_enable():
|
||||
if sabnzbd.cfg.nscript_enable() and check_cat('nscript', job_cat):
|
||||
if sabnzbd.cfg.nscript_script():
|
||||
Thread(target=send_nscript, args=(title, msg, gtype)).start()
|
||||
time.sleep(0.5)
|
||||
|
||||
# NTFOSD
|
||||
if have_ntfosd() and sabnzbd.cfg.ntfosd_enable() and check_classes(gtype, 'ntfosd'):
|
||||
send_notify_osd(title, msg)
|
||||
if have_ntfosd() and sabnzbd.cfg.ntfosd_enable():
|
||||
if check_classes(gtype, 'ntfosd') and check_cat('ntfosd', job_cat):
|
||||
send_notify_osd(title, msg)
|
||||
|
||||
|
||||
def reset_growl():
|
||||
@@ -193,6 +204,9 @@ def register_growl(growl_server, growl_password):
|
||||
|
||||
sys_name = hostname(host)
|
||||
|
||||
# Reduce logging of Growl in Debug/Info mode
|
||||
logging.getLogger('gntp').setLevel(logging.WARNING)
|
||||
|
||||
# Clean up persistent data in GNTP to make re-registration work
|
||||
GNTPRegister.notifications = []
|
||||
GNTPRegister.headers = {}
|
||||
@@ -216,7 +230,7 @@ def register_growl(growl_server, growl_password):
|
||||
logging.debug(error)
|
||||
del growler
|
||||
ret = None
|
||||
except socket.error, err:
|
||||
except (gntp.errors.NetworkError, gntp.errors.AuthError) as err:
|
||||
error = 'Cannot register with Growl %s' % str(err)
|
||||
logging.debug(error)
|
||||
del growler
|
||||
@@ -270,7 +284,7 @@ def send_growl(title, msg, gtype, test=None):
|
||||
else:
|
||||
logging.debug('Growl error %s', ret)
|
||||
return 'Growl error %s', ret
|
||||
except socket.error, err:
|
||||
except (gntp.errors.NetworkError, gntp.errors.AuthError) as err:
|
||||
error = 'Growl error %s' % err
|
||||
logging.debug(error)
|
||||
return error
|
||||
|
||||
@@ -259,6 +259,7 @@ class NzbQueue(object):
|
||||
del self.__nzo_table[nzo_id]
|
||||
# And attach the new nzo to the old nzo_id
|
||||
self.__nzo_table[old_id] = new_nzo
|
||||
logging.info('Replacing in queue %s by %s', nzo.final_name, new_nzo.final_name)
|
||||
del nzo
|
||||
return new_nzo
|
||||
except:
|
||||
@@ -289,6 +290,7 @@ class NzbQueue(object):
|
||||
|
||||
def generate_future(self, msg, pp=None, script=None, cat=None, url=None, priority=NORMAL_PRIORITY, nzbname=None):
|
||||
""" Create and return a placeholder nzo object """
|
||||
logging.debug('Creating placeholder NZO')
|
||||
future_nzo = NzbObject(msg, pp, script, None, True, cat=cat, url=url, priority=priority, nzbname=nzbname, status=Status.GRABBING)
|
||||
self.add(future_nzo)
|
||||
return future_nzo
|
||||
@@ -306,6 +308,7 @@ class NzbQueue(object):
|
||||
for nzo_id in [item.strip() for item in nzo_ids.split(',')]:
|
||||
if nzo_id in self.__nzo_table:
|
||||
self.__nzo_table[nzo_id].script = script
|
||||
logging.info('Set script=%s for job %s', script, self.__nzo_table[nzo_id].final_name)
|
||||
result += 1
|
||||
return result
|
||||
|
||||
@@ -315,15 +318,21 @@ class NzbQueue(object):
|
||||
if nzo_id in self.__nzo_table:
|
||||
nzo = self.__nzo_table[nzo_id]
|
||||
nzo.cat, pp, nzo.script, prio = cat_to_opts(cat)
|
||||
logging.info('Set cat=%s for job %s', cat, nzo.final_name)
|
||||
nzo.set_pp(pp)
|
||||
if explicit_priority is None:
|
||||
self.set_priority(nzo_id, prio)
|
||||
# Abort any ongoing unpacking if the category changed
|
||||
nzo.abort_direct_unpacker()
|
||||
result += 1
|
||||
return result
|
||||
|
||||
def change_name(self, nzo_id, name, password=None):
|
||||
if nzo_id in self.__nzo_table:
|
||||
nzo = self.__nzo_table[nzo_id]
|
||||
logging.info('Renaming %s to %s', nzo.final_name, name)
|
||||
# Abort any ongoing unpacking if the name changed (dirs change)
|
||||
nzo.abort_direct_unpacker()
|
||||
if not nzo.futuretype:
|
||||
nzo.set_final_name_pw(name, password)
|
||||
else:
|
||||
@@ -389,7 +398,7 @@ class NzbQueue(object):
|
||||
self.save(nzo)
|
||||
|
||||
if not (quiet or nzo.status in ('Fetching',)):
|
||||
notifier.send_notification(T('NZB added to queue'), nzo.filename, 'download')
|
||||
notifier.send_notification(T('NZB added to queue'), nzo.filename, 'download', nzo.cat)
|
||||
|
||||
if not quiet and cfg.auto_sort():
|
||||
self.sort_by_avg_age()
|
||||
@@ -410,20 +419,17 @@ class NzbQueue(object):
|
||||
# Other information is obtained from the nzo
|
||||
history_db.add_history_db(nzo, '', '', 0, '', '')
|
||||
history_db.close()
|
||||
sabnzbd.history_updated()
|
||||
|
||||
elif cleanup:
|
||||
self.cleanup_nzo(nzo, keep_basic, del_files)
|
||||
|
||||
sabnzbd.remove_data(nzo_id, nzo.workpath)
|
||||
|
||||
logging.info('Removed job %s', nzo.final_name)
|
||||
if save:
|
||||
self.save(nzo)
|
||||
else:
|
||||
nzo_id = None
|
||||
|
||||
# Update the last check time, since history was updated
|
||||
sabnzbd.LAST_HISTORY_UPDATE = time.time()
|
||||
|
||||
return nzo_id
|
||||
|
||||
def remove_multiple(self, nzo_ids, del_files=False):
|
||||
@@ -458,6 +464,7 @@ class NzbQueue(object):
|
||||
|
||||
if nzf:
|
||||
removed.append(nzf_id)
|
||||
nzo.abort_direct_unpacker()
|
||||
post_done = nzo.remove_nzf(nzf)
|
||||
if post_done:
|
||||
if nzo.finished_files:
|
||||
@@ -470,7 +477,7 @@ class NzbQueue(object):
|
||||
nzo.bytes_tried -= (nzf.bytes - nzf.bytes_left)
|
||||
del nzo.files_table[nzf_id]
|
||||
nzo.finished_files.remove(nzf)
|
||||
|
||||
logging.info('Removed NZFs %s from job %s', removed, nzo.final_name)
|
||||
return removed
|
||||
|
||||
def pause_multiple_nzo(self, nzo_ids):
|
||||
@@ -485,7 +492,7 @@ class NzbQueue(object):
|
||||
if nzo_id in self.__nzo_table:
|
||||
nzo = self.__nzo_table[nzo_id]
|
||||
nzo.pause()
|
||||
logging.debug("Paused nzo: %s", nzo_id)
|
||||
logging.info("Paused nzo: %s", nzo_id)
|
||||
handled.append(nzo_id)
|
||||
return handled
|
||||
|
||||
@@ -503,7 +510,7 @@ class NzbQueue(object):
|
||||
nzo = self.__nzo_table[nzo_id]
|
||||
nzo.resume()
|
||||
nzo.reset_all_try_lists()
|
||||
logging.debug("Resumed nzo: %s", nzo_id)
|
||||
logging.info("Resumed nzo: %s", nzo_id)
|
||||
handled.append(nzo_id)
|
||||
return handled
|
||||
|
||||
@@ -548,6 +555,7 @@ class NzbQueue(object):
|
||||
item_id_pos2 = i
|
||||
if (item_id_pos1 > -1) and (item_id_pos2 > -1):
|
||||
item = self.__nzo_list[item_id_pos1]
|
||||
logging.info('Switching job [%s] %s => [%s] %s', item_id_pos1, item.final_name, item_id_pos2, self.__nzo_list[item_id_pos2].final_name)
|
||||
del self.__nzo_list[item_id_pos1]
|
||||
self.__nzo_list.insert(item_id_pos2, item)
|
||||
return (item_id_pos2, nzo1.priority)
|
||||
@@ -573,15 +581,15 @@ class NzbQueue(object):
|
||||
self.__nzo_table[nzo_id].move_bottom_bulk(nzf_ids)
|
||||
|
||||
def sort_by_avg_age(self, reverse=False):
|
||||
logging.info("Sorting by average date...(reversed:%s)", reverse)
|
||||
logging.info("Sorting by average date... (reversed:%s)", reverse)
|
||||
self.__nzo_list = sort_queue_function(self.__nzo_list, _nzo_date_cmp, reverse)
|
||||
|
||||
def sort_by_name(self, reverse=False):
|
||||
logging.info("Sorting by name...(reversed:%s)", reverse)
|
||||
logging.info("Sorting by name... (reversed:%s)", reverse)
|
||||
self.__nzo_list = sort_queue_function(self.__nzo_list, _nzo_name_cmp, reverse)
|
||||
|
||||
def sort_by_size(self, reverse=False):
|
||||
logging.info("Sorting by size...(reversed:%s)", reverse)
|
||||
logging.info("Sorting by size... (reversed:%s)", reverse)
|
||||
self.__nzo_list = sort_queue_function(self.__nzo_list, _nzo_size_cmp, reverse)
|
||||
|
||||
def sort_queue(self, field, reverse=None):
|
||||
@@ -666,6 +674,8 @@ class NzbQueue(object):
|
||||
# if the queue is empty then simple append the item to the bottom
|
||||
self.__nzo_list.append(nzo)
|
||||
pos = 0
|
||||
|
||||
logging.info('Set priority=%s for job %s => position=%s ', priority, self.__nzo_table[nzo_id].final_name, pos)
|
||||
return pos
|
||||
|
||||
except:
|
||||
@@ -733,7 +743,7 @@ class NzbQueue(object):
|
||||
else:
|
||||
if file_done:
|
||||
if nzo.next_save is None or time.time() > nzo.next_save:
|
||||
sabnzbd.save_data(nzo, nzo.nzo_id, nzo.workpath)
|
||||
nzo.save_to_disk()
|
||||
BPSMeter.do.save()
|
||||
if nzo.save_timeout is None:
|
||||
nzo.next_save = None
|
||||
@@ -755,6 +765,7 @@ class NzbQueue(object):
|
||||
|
||||
def end_job(self, nzo):
|
||||
""" Send NZO to the post-processing queue """
|
||||
logging.info('Ending job %s', nzo.final_name)
|
||||
if self.actives(grabs=False) < 2 and cfg.autodisconnect():
|
||||
# This was the last job, close server connections
|
||||
if sabnzbd.downloader.Downloader.do:
|
||||
@@ -837,6 +848,8 @@ class NzbQueue(object):
|
||||
return empty
|
||||
|
||||
def cleanup_nzo(self, nzo, keep_basic=False, del_files=False):
|
||||
# Abort DirectUnpack and let it remove files
|
||||
nzo.abort_direct_unpacker()
|
||||
nzo.purge_data(keep_basic, del_files)
|
||||
ArticleCache.do.purge_articles(nzo.saved_articles)
|
||||
|
||||
|
||||
@@ -66,9 +66,10 @@ RE_NORMAL_NAME = re.compile(r'\.\w{2,5}$') # Test reasonably sized extension at
|
||||
# Trylist
|
||||
##############################################################################
|
||||
|
||||
TRYLIST_LOCK = threading.Lock()
|
||||
|
||||
class TryList(object):
|
||||
""" TryList keeps track of which servers have been tried for a specific article
|
||||
This used to have a Lock, but it's not needed (all atomic) and faster without
|
||||
"""
|
||||
# Pre-define attributes to save memory
|
||||
__slots__ = ('__try_list', 'fetcher_priority')
|
||||
@@ -79,16 +80,19 @@ class TryList(object):
|
||||
|
||||
def server_in_try_list(self, server):
|
||||
""" Return whether specified server has been tried """
|
||||
return server in self.__try_list
|
||||
with TRYLIST_LOCK:
|
||||
return server in self.__try_list
|
||||
|
||||
def add_to_try_list(self, server):
|
||||
""" Register server as having been tried already """
|
||||
self.__try_list.append(server)
|
||||
with TRYLIST_LOCK:
|
||||
if server not in self.__try_list:
|
||||
self.__try_list.append(server)
|
||||
|
||||
def reset_try_list(self):
|
||||
""" Clean the list """
|
||||
self.__try_list = []
|
||||
self.fetcher_priority = 0
|
||||
with TRYLIST_LOCK:
|
||||
self.__try_list = []
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -209,10 +213,10 @@ class Article(TryList):
|
||||
# NzbFile
|
||||
##############################################################################
|
||||
NzbFileSaver = (
|
||||
'date', 'subject', 'filename', 'type', 'is_par2', 'vol', 'blocks', 'setname',
|
||||
'extrapars', 'articles', 'decodetable', 'bytes', 'bytes_left',
|
||||
'date', 'subject', 'filename', 'filename_checked', 'type', 'is_par2', 'vol',
|
||||
'blocks', 'setname','extrapars', 'articles', 'decodetable', 'bytes', 'bytes_left',
|
||||
'article_count', 'nzo', 'nzf_id', 'deleted', 'valid', 'import_finished',
|
||||
'md5sum'
|
||||
'md5sum', 'md5of16k'
|
||||
)
|
||||
|
||||
|
||||
@@ -229,6 +233,7 @@ class NzbFile(TryList):
|
||||
self.subject = subject
|
||||
self.type = None
|
||||
self.filename = name_extractor(subject)
|
||||
self.filename_checked = False
|
||||
|
||||
self.is_par2 = False
|
||||
self.vol = None
|
||||
@@ -250,6 +255,7 @@ class NzbFile(TryList):
|
||||
self.import_finished = False
|
||||
|
||||
self.md5sum = None
|
||||
self.md5of16k = None
|
||||
|
||||
self.valid = bool(article_db)
|
||||
|
||||
@@ -271,6 +277,13 @@ class NzbFile(TryList):
|
||||
self.decodetable[partnum] = article
|
||||
|
||||
self.import_finished = True
|
||||
else:
|
||||
# TEMPORARY ERRORS
|
||||
if not os.path.exists(os.path.join(self.nzf_id, self.nzo.workpath)):
|
||||
logging.warning('Article DB file not found %s', self)
|
||||
else:
|
||||
# It was there, but empty
|
||||
logging.warning('Article DB empty %s', self)
|
||||
|
||||
def remove_article(self, article, found):
|
||||
""" Handle completed article, possibly end of file """
|
||||
@@ -278,8 +291,10 @@ class NzbFile(TryList):
|
||||
self.articles.remove(article)
|
||||
if found:
|
||||
self.bytes_left -= article.bytes
|
||||
# To keep counter correct for pre-check
|
||||
if self.nzo.precheck:
|
||||
self.nzo.bytes_downloaded += article.bytes
|
||||
self.nzo.bytes_tried += article.bytes
|
||||
|
||||
return (not self.articles)
|
||||
|
||||
def set_par2(self, setname, vol, blocks):
|
||||
@@ -309,6 +324,11 @@ class NzbFile(TryList):
|
||||
""" Is this file completed? """
|
||||
return self.import_finished and not bool(self.articles)
|
||||
|
||||
@property
|
||||
def lowest_partnum(self):
|
||||
""" Get lowest article number of this file """
|
||||
return min(self.decodetable)
|
||||
|
||||
def remove_admin(self):
|
||||
""" Remove article database from disk (sabnzbd_nzf_<id>)"""
|
||||
try:
|
||||
@@ -529,13 +549,13 @@ class NzbParser(xml.sax.handler.ContentHandler):
|
||||
##############################################################################
|
||||
NzbObjectSaver = (
|
||||
'filename', 'work_name', 'final_name', 'created', 'bytes', 'bytes_downloaded', 'bytes_tried',
|
||||
'repair', 'unpack', 'delete', 'script', 'cat', 'url', 'groups', 'avg_date',
|
||||
'repair', 'unpack', 'delete', 'script', 'cat', 'url', 'groups', 'avg_date', 'md5of16k',
|
||||
'partable', 'extrapars', 'md5packs', 'files', 'files_table', 'finished_files', 'status',
|
||||
'avg_bps_freq', 'avg_bps_total', 'priority', 'dupe_table', 'saved_articles', 'nzo_id',
|
||||
'futuretype', 'deleted', 'parsed', 'action_line', 'unpack_info', 'fail_msg', 'nzo_info',
|
||||
'custom_name', 'password', 'next_save', 'save_timeout', 'encrypted',
|
||||
'duplicate', 'oversized', 'precheck', 'incomplete', 'reuse', 'meta',
|
||||
'md5sum', 'servercount', 'unwanted_ext', 'rating_filtered'
|
||||
'md5sum', 'servercount', 'unwanted_ext', 'renames', 'rating_filtered'
|
||||
)
|
||||
|
||||
# Lock to prevent errors when saving the NZO data
|
||||
@@ -584,6 +604,7 @@ class NzbObject(TryList):
|
||||
self.meta = {}
|
||||
self.servercount = {} # Dict to keep bytes per server
|
||||
self.created = False # dirprefixes + work_name created
|
||||
self.direct_unpacker = None # Holds the DirectUnpacker instance
|
||||
self.bytes = 0 # Original bytesize
|
||||
self.bytes_downloaded = 0 # Downloaded byte
|
||||
self.bytes_tried = 0 # Which bytes did we try
|
||||
@@ -602,10 +623,12 @@ class NzbObject(TryList):
|
||||
|
||||
self.partable = {} # Holds one parfile-name for each set
|
||||
self.extrapars = {} # Holds the extra parfile names for all sets
|
||||
self.md5packs = {} # Holds the md5pack for each set
|
||||
self.md5packs = {} # Holds the md5pack for each set (name: hash)
|
||||
self.md5of16k = {} # Holds the md5s of the first-16k of all files in the NZB (hash: name)
|
||||
|
||||
self.files = [] # List of all NZFs
|
||||
self.files_table = {} # Dictionary of NZFs indexed using NZF_ID
|
||||
self.renames = {} # Dictionary of all renamed files
|
||||
|
||||
self.finished_files = [] # List of all finished NZFs
|
||||
|
||||
@@ -934,7 +957,6 @@ class NzbObject(TryList):
|
||||
nzf.deleted = True
|
||||
return not bool(self.files)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def reset_all_try_lists(self):
|
||||
for nzf in self.files:
|
||||
nzf.reset_all_try_lists()
|
||||
@@ -954,8 +976,9 @@ class NzbObject(TryList):
|
||||
head, vol, block = analyse_par2(name)
|
||||
if head and matcher(lparset, head.lower()):
|
||||
xnzf.set_par2(parset, vol, block)
|
||||
self.extrapars[parset].append(xnzf)
|
||||
if not self.precheck:
|
||||
# Don't postpone if all par2 should be kept
|
||||
if cfg.enable_par_cleanup():
|
||||
self.extrapars[parset].append(xnzf)
|
||||
self.files.remove(xnzf)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
@@ -968,7 +991,7 @@ class NzbObject(TryList):
|
||||
if not nzf.is_par2:
|
||||
head, vol, block = analyse_par2(fn)
|
||||
# Is a par2file and repair mode activated
|
||||
if head and (self.repair or cfg.allow_streaming()):
|
||||
if head and self.repair:
|
||||
# Skip if mini-par2 is not complete and there are more par2 files
|
||||
if not block and nzf.bytes_left and self.extrapars.get(head):
|
||||
return
|
||||
@@ -1023,8 +1046,6 @@ class NzbObject(TryList):
|
||||
self.fail_msg = T('Aborted, cannot be completed') + ' - https://sabnzbd.org/not-complete'
|
||||
self.set_unpack_info('Download', self.fail_msg, unique=False)
|
||||
logging.debug('Abort job "%s", due to impossibility to complete it', self.final_name_pw_clean)
|
||||
# Update the last check time
|
||||
sabnzbd.LAST_HISTORY_UPDATE = time.time()
|
||||
return True, True
|
||||
|
||||
if file_done:
|
||||
@@ -1033,6 +1054,7 @@ class NzbObject(TryList):
|
||||
if not found:
|
||||
# Add extra parfiles when there was a damaged article and not pre-checking
|
||||
if self.extrapars and not self.precheck:
|
||||
self.abort_direct_unpacker()
|
||||
self.prospective_add(nzf)
|
||||
|
||||
post_done = False
|
||||
@@ -1063,6 +1085,7 @@ class NzbObject(TryList):
|
||||
if name in files:
|
||||
files.remove(name)
|
||||
files.append(renames[name])
|
||||
self.renames = renames
|
||||
|
||||
# Looking for the longest name first, minimizes the chance on a mismatch
|
||||
files.sort(lambda x, y: len(y) - len(x))
|
||||
@@ -1083,6 +1106,7 @@ class NzbObject(TryList):
|
||||
nzfs.remove(nzf)
|
||||
files.remove(filename)
|
||||
self.bytes_tried += nzf.bytes
|
||||
self.bytes_downloaded += nzf.bytes
|
||||
break
|
||||
|
||||
try:
|
||||
@@ -1112,6 +1136,10 @@ class NzbObject(TryList):
|
||||
|
||||
def set_pp(self, value):
|
||||
self.repair, self.unpack, self.delete = sabnzbd.pp_to_opts(value)
|
||||
logging.info('Set pp=%s for job %s', value, self.final_name)
|
||||
# Abort unpacking if not desired anymore
|
||||
if not self.unpack:
|
||||
self.abort_direct_unpacker()
|
||||
self.save_to_disk()
|
||||
|
||||
@property
|
||||
@@ -1162,7 +1190,7 @@ class NzbObject(TryList):
|
||||
self.status = Status.PAUSED
|
||||
# Prevent loss of paused state when terminated
|
||||
if self.nzo_id and not self.is_gone():
|
||||
sabnzbd.save_data(self, self.nzo_id, self.workpath)
|
||||
self.save_to_disk()
|
||||
|
||||
def resume(self):
|
||||
self.status = Status.QUEUED
|
||||
@@ -1190,7 +1218,8 @@ class NzbObject(TryList):
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def remove_parset(self, setname):
|
||||
self.partable.pop(setname)
|
||||
if setname in self.partable:
|
||||
self.partable.pop(setname)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def remove_extrapar(self, parfile):
|
||||
@@ -1221,22 +1250,37 @@ class NzbObject(TryList):
|
||||
if nzf_check.blocks:
|
||||
blocks_already = blocks_already + int_conv(nzf_check.blocks)
|
||||
|
||||
# Make sure to also select a parset if it was in the original filename
|
||||
original_filename = self.renames.get(nzf.filename, '')
|
||||
|
||||
# Need more?
|
||||
if not nzf.is_par2 and blocks_already < total_need:
|
||||
# We have to find the right par-set
|
||||
for parset in self.extrapars.keys():
|
||||
if parset in nzf.filename and self.extrapars[parset]:
|
||||
if (parset in nzf.filename or parset in original_filename) and self.extrapars[parset]:
|
||||
extrapars_sorted = sorted(self.extrapars[parset], key=lambda x: x.blocks, reverse=True)
|
||||
# Loop until we have enough
|
||||
while blocks_already < total_need and extrapars_sorted:
|
||||
# Add the first one
|
||||
new_nzf = extrapars_sorted.pop()
|
||||
# Reset NZF TryList, in case something was on it before it became extrapar
|
||||
new_nzf.reset_try_list()
|
||||
self.add_parfile(new_nzf)
|
||||
self.extrapars[parset] = extrapars_sorted
|
||||
blocks_already = blocks_already + int_conv(new_nzf.blocks)
|
||||
logging.info('Prospectively added %s repair blocks to %s', new_nzf.blocks, self.final_name)
|
||||
# Reset all try lists
|
||||
self.reset_all_try_lists()
|
||||
# Reset NZO TryList
|
||||
self.reset_try_list()
|
||||
|
||||
def add_to_direct_unpacker(self, nzf):
|
||||
""" Start or add to DirectUnpacker """
|
||||
if not self.direct_unpacker:
|
||||
sabnzbd.directunpacker.DirectUnpacker(self)
|
||||
self.direct_unpacker.add(nzf)
|
||||
|
||||
def abort_direct_unpacker(self):
|
||||
""" Abort any running DirectUnpackers """
|
||||
if self.direct_unpacker:
|
||||
self.direct_unpacker.abort()
|
||||
|
||||
def check_quality(self, req_ratio=0):
|
||||
""" Determine amount of articles present on servers
|
||||
@@ -1338,7 +1382,7 @@ class NzbObject(TryList):
|
||||
if sabnzbd.highest_server(server):
|
||||
nzf.finish_import()
|
||||
# Still not finished? Something went wrong...
|
||||
if not nzf.import_finished:
|
||||
if not nzf.import_finished and not self.is_gone():
|
||||
logging.error(T('Error importing %s'), nzf)
|
||||
nzf_remove_list.append(nzf)
|
||||
continue
|
||||
@@ -1430,6 +1474,17 @@ class NzbObject(TryList):
|
||||
self.files[pos + 1] = nzf
|
||||
self.files[pos] = tmp_nzf
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def renamed_file(self, name_set, old_name=None):
|
||||
""" Save renames at various stages (Download/PP)
|
||||
to be used on Retry. Accepts strings and dicts.
|
||||
"""
|
||||
if not old_name:
|
||||
# Add to dict
|
||||
self.renames.update(name_set)
|
||||
else:
|
||||
self.renames[name_set] = old_name
|
||||
|
||||
# Determine if rating information (including site identifier so rating can be updated)
|
||||
# is present in metadata and if so store it
|
||||
@synchronized(NZO_LOCK)
|
||||
@@ -1489,6 +1544,8 @@ class NzbObject(TryList):
|
||||
if keep_basic:
|
||||
remove_all(wpath, 'SABnzbd_nz?_*', keep_folder=True)
|
||||
remove_all(wpath, 'SABnzbd_article_*', keep_folder=True)
|
||||
# We save the renames file
|
||||
sabnzbd.save_data(self.renames, RENAMES_FILE, self.workpath)
|
||||
else:
|
||||
remove_all(wpath, recursive=True)
|
||||
if del_files:
|
||||
@@ -1525,8 +1582,9 @@ class NzbObject(TryList):
|
||||
self.files if full else [],
|
||||
queued_files,
|
||||
self.status, self.priority,
|
||||
len(self.nzo_info.get('missing_art_log', []))
|
||||
)
|
||||
len(self.nzo_info.get('missing_art_log', [])),
|
||||
self.bytes_tried - self.bytes_downloaded,
|
||||
self.direct_unpacker.get_formatted_stats() if self.direct_unpacker else 0)
|
||||
|
||||
def get_nzf_by_id(self, nzf_id):
|
||||
if nzf_id in self.files_table:
|
||||
@@ -1545,19 +1603,19 @@ class NzbObject(TryList):
|
||||
else:
|
||||
self.unpack_info[key] = [msg]
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def set_action_line(self, action=None, msg=None):
|
||||
# Update the last check time
|
||||
sabnzbd.LAST_HISTORY_UPDATE = time.time()
|
||||
if action and msg:
|
||||
self.action_line = '%s: %s' % (action, msg)
|
||||
else:
|
||||
self.action_line = ''
|
||||
# Make sure it's updated in the interface
|
||||
sabnzbd.history_updated()
|
||||
|
||||
@property
|
||||
def repair_opts(self):
|
||||
return self.repair, self.unpack, self.delete
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def save_to_disk(self):
|
||||
""" Save job's admin to disk """
|
||||
self.save_attribs()
|
||||
@@ -1624,7 +1682,6 @@ class NzbObject(TryList):
|
||||
""" Is this job still going somehow? """
|
||||
return self.status in (Status.COMPLETED, Status.DELETED, Status.FAILED)
|
||||
|
||||
@synchronized(NZO_LOCK)
|
||||
def __getstate__(self):
|
||||
""" Save to pickle file, selecting attributes """
|
||||
dict_ = {}
|
||||
@@ -1644,10 +1701,15 @@ class NzbObject(TryList):
|
||||
self.avg_stamp = time.mktime(self.avg_date.timetuple())
|
||||
self.wait = None
|
||||
self.to_be_removed = False
|
||||
self.direct_unpacker = None
|
||||
if self.meta is None:
|
||||
self.meta = {}
|
||||
if self.servercount is None:
|
||||
self.servercount = {}
|
||||
if self.md5of16k is None:
|
||||
self.md5of16k = {}
|
||||
if self.renames is None:
|
||||
self.renames = {}
|
||||
if self.bytes_tried is None:
|
||||
# Fill with old info
|
||||
self.bytes_tried = 0
|
||||
|
||||
@@ -618,8 +618,8 @@ class SABnzbdDelegate(NSObject):
|
||||
|
||||
def diskspaceUpdate(self):
|
||||
try:
|
||||
self.completefolder_menu_item.setTitle_("%s%.2f GB" % (T('Complete Folder') + '\t\t\t', diskspace(sabnzbd.cfg.complete_dir.get_path())[1]))
|
||||
self.incompletefolder_menu_item.setTitle_("%s%.2f GB" % (T('Incomplete Folder') + '\t\t', diskspace(sabnzbd.cfg.download_dir.get_path())[1]))
|
||||
self.completefolder_menu_item.setTitle_("%s%.2f GB" % (T('Complete Folder') + '\t\t\t', diskspace()['complete_dir'][1]))
|
||||
self.incompletefolder_menu_item.setTitle_("%s%.2f GB" % (T('Incomplete Folder') + '\t\t', diskspace()['download_dir'][1]))
|
||||
except:
|
||||
logging.info("[osx] diskspaceUpdate Exception %s" % (sys.exc_info()[0]))
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user