mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-01-05 22:20:21 -05:00
Compare commits
158 Commits
2.1.x
...
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 | ||
|
|
8483e4ab8a | ||
|
|
f6c163b505 | ||
|
|
8f30173db0 | ||
|
|
0372ff95bb | ||
|
|
6fa29c7877 | ||
|
|
d4c9121593 | ||
|
|
76a8df0282 | ||
|
|
0b6d8309a0 | ||
|
|
10a9bc0817 | ||
|
|
2a14af4ffa | ||
|
|
d1a4a292e3 | ||
|
|
14c0efa151 | ||
|
|
4fc03f2581 | ||
|
|
3205b9fda9 | ||
|
|
953e0d6c22 | ||
|
|
b50ce54ca9 | ||
|
|
5e7558ce4a | ||
|
|
8aa6362432 | ||
|
|
02ebb97a8b | ||
|
|
b36063403d | ||
|
|
526ffa2afb | ||
|
|
5b3fd812d8 | ||
|
|
af6dac9cdc | ||
|
|
bc25d936bb | ||
|
|
b497fe1444 | ||
|
|
3f456cce05 | ||
|
|
4dd2f089ec | ||
|
|
b1b1bc248d | ||
|
|
d9e675469c | ||
|
|
ede0ca1772 | ||
|
|
2d098a1477 | ||
|
|
e5f014b68e | ||
|
|
b3a9dc9eeb | ||
|
|
2a06cec27c | ||
|
|
19230c889d | ||
|
|
c969ce552c | ||
|
|
2def600d21 | ||
|
|
02aa8f18c8 | ||
|
|
fcd9522dae | ||
|
|
72d3ce885e | ||
|
|
b428996eb7 | ||
|
|
2b4eb58fad | ||
|
|
240e8dff60 | ||
|
|
1c286afde6 | ||
|
|
2eeb908540 | ||
|
|
562e6ecce9 | ||
|
|
4bd0d32508 | ||
|
|
6f2ccbef80 |
@@ -1,5 +1,5 @@
|
||||
*******************************************
|
||||
*** This is SABnzbd 2.1.0 ***
|
||||
*** This is SABnzbd 2.2.0 ***
|
||||
*******************************************
|
||||
SABnzbd is an open-source cross-platform binary newsreader.
|
||||
It simplifies the process of downloading from Usenet dramatically,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
SABnzbd 2.1.0
|
||||
SABnzbd 2.2.0
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
0) LICENSE
|
||||
|
||||
6
PKG-INFO
6
PKG-INFO
@@ -1,8 +1,8 @@
|
||||
Metadata-Version: 1.0
|
||||
Name: SABnzbd
|
||||
Version: 2.1.0
|
||||
Summary: SABnzbd-2.1.0
|
||||
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
|
||||
|
||||
|
||||
64
README.mkd
64
README.mkd
@@ -1,22 +1,54 @@
|
||||
Release Notes - SABnzbd 2.1.0
|
||||
Release Notes - SABnzbd 2.2.0 Alpha 3
|
||||
=========================================================
|
||||
|
||||
## Changes since 2.0.1
|
||||
- Windows-only: Will now use MultiPar for verification and repair. MultiPar uses
|
||||
the latest optimizations, multiple cores and can utilize the GPU, resulting in
|
||||
repairs often being twice as fast. Created by Yutaka Sawada (Windows only)
|
||||
"Extra Par2 Parameters" are also passed to MultiPar
|
||||
For example: Set "/lc32" to enable GPU for repair (can also be slower!)
|
||||
- Minimal macOS version set to 10.9 (Mavericks)
|
||||
- Human readable history date/time default setting in Glitter (x hours ago, etc)
|
||||
- Post-processing script execution can be aborted
|
||||
- Removed Specials settings: never_repair, nr_decoders, prio_sort_list,
|
||||
create_group_folder, enable_par_cleanup, enable_quickcheck, allow_64bit_tools
|
||||
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 all jobs will be unpaused and URL's that did not finish
|
||||
fetching before the upgrade will be lost!
|
||||
|
||||
# Bugfixes
|
||||
- File join would fail incorrectly
|
||||
- Catch and show 'Disk full' messages from PAR2/MultiPar
|
||||
- MemoryError occurred when opening the interface after long period of inactivity
|
||||
## 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
|
||||
- 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)
|
||||
- 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 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
|
||||
- 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.
|
||||
|
||||
## Upgrading from 0.7.x and older
|
||||
- Finish queue
|
||||
|
||||
26
SABnzbd.py
26
SABnzbd.py
@@ -56,8 +56,6 @@ if [int(n) for n in cherrypy.__version__.split('.')] < [8, 1, 2]:
|
||||
print 'Sorry, requires Python module Cherrypy 8.1.2+ (use the included version)'
|
||||
sys.exit(1)
|
||||
|
||||
from cherrypy import _cpserver
|
||||
|
||||
SQLITE_DLL = True
|
||||
try:
|
||||
from sqlite3 import version as sqlite3_version
|
||||
@@ -90,7 +88,7 @@ from sabnzbd.misc import real_path, \
|
||||
check_latest_version, exit_sab, \
|
||||
split_host, get_ext, create_https_certificates, \
|
||||
windows_variant, ip_extract, set_serv_parms, get_serv_parms, globber_full
|
||||
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, panic_fwall, \
|
||||
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, \
|
||||
panic_sqlite, panic, launch_a_browser
|
||||
import sabnzbd.scheduler as scheduler
|
||||
import sabnzbd.config as config
|
||||
@@ -430,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)
|
||||
|
||||
@@ -727,24 +722,6 @@ def evaluate_inipath(path):
|
||||
return path
|
||||
|
||||
|
||||
def cherrypy_logging(log_path, log_handler):
|
||||
""" Setup CherryPy logging """
|
||||
log = cherrypy.log
|
||||
log.access_file = ''
|
||||
log.error_file = ''
|
||||
# Max size of 512KB
|
||||
maxBytes = getattr(log, "rot_maxBytes", 524288)
|
||||
# cherrypy.log cherrypy.log.1 cherrypy.log.2
|
||||
backupCount = getattr(log, "rot_backupCount", 3)
|
||||
|
||||
# Make a new RotatingFileHandler for the error log.
|
||||
fname = getattr(log, "rot_error_file", log_path)
|
||||
h = log_handler(fname, 'a', maxBytes, backupCount)
|
||||
h.setLevel(logging.DEBUG)
|
||||
h.setFormatter(cherrypy._cplogging.logfmt)
|
||||
log.error_log.addHandler(h)
|
||||
|
||||
|
||||
def commandline_handler(frozen=True):
|
||||
""" Split win32-service commands are true parameters
|
||||
Returns:
|
||||
@@ -1353,7 +1330,6 @@ def main():
|
||||
'error_page.404': sabnzbd.panic.error_page_404
|
||||
})
|
||||
|
||||
|
||||
# Do we want CherryPy Logging? Cannot be done via the config
|
||||
if cherrypylogging:
|
||||
sabnzbd.WEBLOGFILE = os.path.join(logdir, DEF_LOG_CHERRY)
|
||||
|
||||
@@ -196,12 +196,13 @@ socket_errors_to_ignore = plat_specific_errors(
|
||||
)
|
||||
socket_errors_to_ignore.append('timed out')
|
||||
socket_errors_to_ignore.append('The read operation timed out')
|
||||
if sys.platform == 'darwin':
|
||||
socket_errors_to_ignore.append(plat_specific_errors('EPROTOTYPE'))
|
||||
|
||||
socket_errors_nonblocking = plat_specific_errors(
|
||||
'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
socket_errors_to_ignore.append(plat_specific_errors('EPROTOTYPE'))
|
||||
socket_errors_nonblocking.append(plat_specific_errors('EPROTOTYPE'))
|
||||
|
||||
comma_separated_headers = [
|
||||
ntob(h) for h in
|
||||
['Accept', 'Accept-Charset', 'Accept-Encoding',
|
||||
|
||||
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'
|
||||
@@ -41,12 +41,16 @@
|
||||
</td>
|
||||
</tr>
|
||||
<!--#end if#-->
|
||||
<!--#if not $have_mt_par2#-->
|
||||
<!--#if not $nt and not $darwin#-->
|
||||
<tr>
|
||||
<th scope="row">Multicore Par2</th>
|
||||
<th scope="row">$T('opt-multicore-par2')</th>
|
||||
<td>
|
||||
<!--#if $have_mt_par2#-->
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
<!--#else#-->
|
||||
<span class="label label-warning">$T('notAvailable')</span> $T('explain-getpar2mt')
|
||||
<a href="${helpuri}installation/multicore-par2" target="_blank">${helpuri}installation/multicore-par2</a>
|
||||
<!--#end if#-->
|
||||
</td>
|
||||
</tr>
|
||||
<!--#end if#-->
|
||||
@@ -105,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>
|
||||
@@ -113,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>
|
||||
@@ -127,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">
|
||||
@@ -240,7 +240,17 @@
|
||||
\$('.alert-translate').show()
|
||||
}
|
||||
}
|
||||
\$('#language').on('change', hideOrShowTranslate)
|
||||
\$('#language').on('change', function() {
|
||||
// Show message
|
||||
hideOrShowTranslate()
|
||||
// Re-load page on submit
|
||||
\$('.fullform').submit(function() {
|
||||
// Skip the fancy stuff, just submit
|
||||
this.submit()
|
||||
})
|
||||
// No JSON reponse
|
||||
\$('#ajax').val('')
|
||||
})
|
||||
hideOrShowTranslate()
|
||||
|
||||
\$('#apikey, #nzbkey').click(function () { \$(this).select() });
|
||||
@@ -314,14 +324,20 @@
|
||||
if(bandwidthLimit) {
|
||||
var bandwithLimitNumber = parseFloat(bandwidthLimit)
|
||||
var bandwithLimitText = bandwidthLimit.replace(/[^a-zA-Z]+/g, '');
|
||||
\$('#bandwidth_max_value').val(bandwithLimitNumber)
|
||||
\$('#bandwidth_max_dropdown').val(bandwithLimitText)
|
||||
if(bandwithLimitNumber) {
|
||||
\$('#bandwidth_max_value').val(bandwithLimitNumber)
|
||||
\$('#bandwidth_max_dropdown').val(bandwithLimitText)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update the value
|
||||
\$('#bandwidth_max_value, #bandwidth_max_dropdown').on('change', function() {
|
||||
\$('#bandwidth_max').val(\$('#bandwidth_max_value').val() + \$('#bandwidth_max_dropdown').val())
|
||||
if(\$('#bandwidth_max_value').val()) {
|
||||
\$('#bandwidth_max').val(\$('#bandwidth_max_value').val() + \$('#bandwidth_max_dropdown').val())
|
||||
} else {
|
||||
\$('#bandwidth_max').val('')
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
}).then(function(data) {
|
||||
// Let's replace the link
|
||||
msg = data.value.message.replace('https://sabnzbd.org/certificate-errors', '<a href="https://sabnzbd.org/certificate-errors" class="alert-link" target="_blank">https://sabnzbd.org/certificate-errors</a>')
|
||||
msg = msg.replace('-', '<br>')
|
||||
// Fill the box and enable the button
|
||||
resultBox.removeClass('alert-success alert-danger').show()
|
||||
resultBox.html(msg)
|
||||
|
||||
@@ -31,12 +31,6 @@
|
||||
<input type="number" name="max_art_tries" id="max_art_tries" value="$max_art_tries" min="2" max="2000" />
|
||||
<span class="desc">$T('explain-max_art_tries')</span>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<label class="config" for="max_art_opt">$T('opt-max_art_opt')</label>
|
||||
<input type="checkbox" name="max_art_opt" id="max_art_opt" value="1" <!--#if int($max_art_opt) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-max_art_opt')</span>
|
||||
</div>
|
||||
|
||||
<div class="field-pair">
|
||||
<label class="config" for="auto_disconnect">$T('opt-auto_disconnect')</label>
|
||||
<input type="checkbox" name="auto_disconnect" id="auto_disconnect" value="1" <!--#if int($auto_disconnect) > 0 then 'checked="checked"' else ""#--> />
|
||||
@@ -136,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>
|
||||
@@ -159,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" />
|
||||
@@ -251,11 +243,6 @@
|
||||
<input type="checkbox" name="replace_dots" id="replace_dots" value="1" <!--#if int($replace_dots) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-replace_dots')</span>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<label class="config" for="replace_illegal">$T('opt-replace_illegal')</label>
|
||||
<input type="checkbox" name="replace_illegal" id="replace_illegal" value="1" <!--#if int($replace_illegal) > 0 then 'checked="checked"' else ""#--> />
|
||||
<span class="desc">$T('explain-replace_illegal')</span>
|
||||
</div>
|
||||
<!--#if not $nt#-->
|
||||
<div class="field-pair">
|
||||
<label class="config" for="sanitize_safe">$T('opt-sanitize_safe')</label>
|
||||
|
||||
@@ -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())) {
|
||||
@@ -263,7 +260,7 @@ function do_restart() {
|
||||
// Keep counter of failures
|
||||
var failureCounter = 0;
|
||||
|
||||
// Now we try untill we can connect
|
||||
// Now we try until we can connect
|
||||
var refreshInterval = setInterval(function() {
|
||||
// We skip the first one
|
||||
if(failureCounter == 0) {
|
||||
@@ -409,6 +406,9 @@ $(document).ready(function () {
|
||||
$('#enable_https').on('change', function() {
|
||||
$('.enable_https_options').toggle()
|
||||
})
|
||||
if(!$('#enable_https').is(':checked')) {
|
||||
$('.enable_https_options').hide()
|
||||
}
|
||||
|
||||
$('.advancedButton').click(function(event){
|
||||
$('.advanced-settings').toggle()
|
||||
|
||||
@@ -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) {
|
||||
@@ -1921,49 +1961,6 @@ input[name="nzbURL"] {
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
SPECIAL FOR FIREFOX
|
||||
It uses very high CPU for anything animated (Sep 2015)
|
||||
Disable animations on progress-bar and make the History-'processing' a block animation
|
||||
Can be removed if it's performance gets better in the future..
|
||||
***/
|
||||
@supports (-moz-transform: translate(0, 0)) {
|
||||
.progress-bar {
|
||||
transform: none !important;
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@keyframes stretchdelay {
|
||||
0%, 60% {
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
|
||||
61%, 100% {
|
||||
transform: scaleY(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
.processing-download > div {
|
||||
animation: stretchdelay 2s infinite linear;
|
||||
}
|
||||
|
||||
.processing-download .loader-bar-two {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.processing-download .loader-bar-three {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.processing-download .loader-bar-four {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
.queue-table td.name input {
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
Bootstrap overwrites
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
2468
po/main/SABnzbd.pot
2468
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,80 +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."
|
||||
|
||||
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."
|
||||
|
||||
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
4812
po/main/he.po
Normal file
4812
po/main/he.po
Normal file
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."
|
||||
|
||||
|
||||
98
po/nsis/he.po
Normal file
98
po/nsis/he.po
Normal file
@@ -0,0 +1,98 @@
|
||||
# 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-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-06-23 05:56+0000\n"
|
||||
"X-Generator: Launchpad (build 18416)\n"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Show Release Notes"
|
||||
msgstr "הראה הערות שחרור"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Start SABnzbd"
|
||||
msgstr "התחל את SABnzbd"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Support the project, Donate!"
|
||||
msgstr "תמוך במיזם, תרום!"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Please close \"SABnzbd.exe\" first"
|
||||
msgstr "אנא סגור את \"SABnzbd.exe\" תחילה"
|
||||
|
||||
#: 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אם תריץ את SABnzbd בתור "
|
||||
"שירות, אתה צריך לעדכן את הגדרות השירות."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "זה יסיר את SABnzbd ממערכתך"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "הפעל באתחול"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "צלמית שולחן עבודה"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "שיוך קבצי NZB"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
msgstr "מחק תכנית"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Delete Settings"
|
||||
msgstr "מחק הגדרות"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid ""
|
||||
"This system requires the Microsoft runtime library VC90 to be installed "
|
||||
"first. Do you want to do that now?"
|
||||
msgstr ""
|
||||
"מערכת זו דורשת את ספרית זמן-אמת VC90 של Microsoft שתהיה מותקנת תחילה. האם "
|
||||
"ברצונך להתקין אותה כעת?"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Downloading Microsoft runtime installer..."
|
||||
msgstr "מוריד מתקין זמן-אמת של Microsoft..."
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Download error, retry?"
|
||||
msgstr "שגיאת הורדה, לנסות שוב?"
|
||||
|
||||
#: NSIS_Installer.nsi
|
||||
msgid "Cannot install without runtime library, retry?"
|
||||
msgstr "לא ניתן להתקין ללא ספרית זמן-אמת, לנסות שוב?"
|
||||
|
||||
#: 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
|
||||
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 "您的设置及数据将会保留。"
|
||||
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Imported to be referenced from other files directly
|
||||
from sabnzbd.version import __version__, __baseline__
|
||||
__configversion__ = 18
|
||||
__queueversion__ = 8
|
||||
|
||||
import os
|
||||
import logging
|
||||
@@ -25,8 +24,6 @@ import datetime
|
||||
import tempfile
|
||||
import cPickle
|
||||
import pickle
|
||||
import zipfile
|
||||
import glob
|
||||
import gzip
|
||||
import subprocess
|
||||
import time
|
||||
@@ -34,7 +31,7 @@ import socket
|
||||
import cherrypy
|
||||
import sys
|
||||
import re
|
||||
from threading import RLock, Lock, Condition, Thread
|
||||
from threading import Lock, Thread
|
||||
try:
|
||||
import sleepless
|
||||
except ImportError:
|
||||
@@ -43,7 +40,7 @@ except ImportError:
|
||||
##############################################################################
|
||||
# Determine platform flags
|
||||
##############################################################################
|
||||
WIN32 = DARWIN = POSIX = FOUNDATION = WIN64 = False
|
||||
WIN32 = DARWIN = FOUNDATION = WIN64 = False
|
||||
KERNEL32 = None
|
||||
|
||||
if os.name == 'nt':
|
||||
@@ -57,7 +54,6 @@ if os.name == 'nt':
|
||||
elif os.name == 'posix':
|
||||
ORG_UMASK = os.umask(18)
|
||||
os.umask(ORG_UMASK)
|
||||
POSIX = True
|
||||
import platform
|
||||
if platform.system().lower() == 'darwin':
|
||||
DARWIN = True
|
||||
@@ -110,9 +106,10 @@ import sabnzbd.cfg as cfg
|
||||
import sabnzbd.database
|
||||
import sabnzbd.lang as lang
|
||||
import sabnzbd.api
|
||||
from sabnzbd.decorators import synchronized, synchronized_CV, IO_LOCK
|
||||
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
|
||||
REPAIR_REQUEST, QUEUE_FILE_NAME, QUEUE_VERSION, QUEUE_FILE_TMPL
|
||||
import sabnzbd.getipaddress as getipaddress
|
||||
|
||||
LINUX_POWER = powersup.HAVE_DBUS
|
||||
@@ -161,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
|
||||
@@ -177,10 +174,10 @@ __SHUTTING_DOWN__ = False
|
||||
##############################################################################
|
||||
def sig_handler(signum=None, frame=None):
|
||||
global SABSTOP, WINTRAY
|
||||
if sabnzbd.WIN32 and type(signum) != type(None) and DAEMON and signum == 5:
|
||||
if sabnzbd.WIN32 and signum is not None and DAEMON and signum == 5:
|
||||
# Ignore the "logoff" event when running as a Win32 daemon
|
||||
return True
|
||||
if type(signum) != type(None):
|
||||
if signum is not None:
|
||||
logging.warning(T('Signal %s caught, saving and exiting...'), signum)
|
||||
try:
|
||||
save_state()
|
||||
@@ -387,6 +384,8 @@ def halt():
|
||||
|
||||
sabnzbd.zconfig.remove_server()
|
||||
|
||||
sabnzbd.directunpacker.abort_all()
|
||||
|
||||
rss.stop()
|
||||
|
||||
logging.debug('Stopping URLGrabber')
|
||||
@@ -580,12 +579,9 @@ def unpause_all():
|
||||
|
||||
|
||||
##############################################################################
|
||||
# NZB_LOCK Methods
|
||||
# NZB Saving Methods
|
||||
##############################################################################
|
||||
NZB_LOCK = Lock()
|
||||
|
||||
|
||||
@synchronized(NZB_LOCK)
|
||||
def backup_exists(filename):
|
||||
""" Return True if backup exists and no_dupes is set """
|
||||
path = cfg.nzb_backup_dir.get_path()
|
||||
@@ -599,7 +595,6 @@ def backup_nzb(filename, data):
|
||||
save_compressed(path, filename, data)
|
||||
|
||||
|
||||
@synchronized(NZB_LOCK)
|
||||
def save_compressed(folder, filename, data):
|
||||
""" Save compressed NZB file in folder """
|
||||
# Need to go to the save folder to
|
||||
@@ -625,9 +620,9 @@ def save_compressed(folder, filename, data):
|
||||
|
||||
|
||||
##############################################################################
|
||||
# CV synchronized (notifies downloader)
|
||||
# Unsynchronized methods
|
||||
##############################################################################
|
||||
@synchronized_CV
|
||||
|
||||
def add_nzbfile(nzbfile, pp=None, script=None, cat=None, priority=NORMAL_PRIORITY, nzbname=None, reuse=False, password=None):
|
||||
""" Add disk-based NZB file, optional attributes,
|
||||
'reuse' flag will suppress duplicate detection
|
||||
@@ -697,9 +692,6 @@ def add_nzbfile(nzbfile, pp=None, script=None, cat=None, priority=NORMAL_PRIORIT
|
||||
keep=keep, reuse=reuse, password=password)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Unsynchronized methods
|
||||
##############################################################################
|
||||
def enable_server(server):
|
||||
""" Enable server (scheduler only) """
|
||||
try:
|
||||
@@ -809,7 +801,6 @@ def change_queue_complete_action(action, new=True):
|
||||
|
||||
# keep the name of the action for matching the current select in queue.tmpl
|
||||
QUEUECOMPLETE = action
|
||||
|
||||
QUEUECOMPLETEACTION = _action
|
||||
QUEUECOMPLETEARG = _argument
|
||||
|
||||
@@ -854,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)
|
||||
@@ -865,7 +856,6 @@ def CheckFreeSpace():
|
||||
# Data IO #
|
||||
################################################################################
|
||||
|
||||
@synchronized(IO_LOCK)
|
||||
def get_new_id(prefix, folder, check_list=None):
|
||||
""" Return unique prefixed admin identifier within folder
|
||||
optionally making sure that id is not in the check_list.
|
||||
@@ -886,7 +876,6 @@ def get_new_id(prefix, folder, check_list=None):
|
||||
raise IOError
|
||||
|
||||
|
||||
@synchronized(IO_LOCK)
|
||||
def save_data(data, _id, path, do_pickle=True, silent=False):
|
||||
""" Save data to a diskfile """
|
||||
if not silent:
|
||||
@@ -899,14 +888,17 @@ 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
|
||||
except:
|
||||
if t == 2:
|
||||
if silent:
|
||||
# This can happen, probably a removed folder
|
||||
pass
|
||||
elif t == 2:
|
||||
logging.error(T('Saving %s failed'), path)
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
else:
|
||||
@@ -914,7 +906,6 @@ def save_data(data, _id, path, do_pickle=True, silent=False):
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
@synchronized(IO_LOCK)
|
||||
def load_data(_id, path, remove=True, do_pickle=True, silent=False):
|
||||
""" Read data from disk file """
|
||||
path = os.path.join(path, _id)
|
||||
@@ -946,7 +937,6 @@ def load_data(_id, path, remove=True, do_pickle=True, silent=False):
|
||||
return data
|
||||
|
||||
|
||||
@synchronized(IO_LOCK)
|
||||
def remove_data(_id, path):
|
||||
""" Remove admin file """
|
||||
path = os.path.join(path, _id)
|
||||
@@ -958,7 +948,6 @@ def remove_data(_id, path):
|
||||
logging.debug("Failed to remove %s", path)
|
||||
|
||||
|
||||
@synchronized(IO_LOCK)
|
||||
def save_admin(data, _id):
|
||||
""" Save data in admin folder in specified format """
|
||||
path = os.path.join(cfg.admin_dir.get_path(), _id)
|
||||
@@ -982,7 +971,6 @@ def save_admin(data, _id):
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
@synchronized(IO_LOCK)
|
||||
def load_admin(_id, remove=False, silent=False):
|
||||
""" Read data in admin folder in specified format """
|
||||
path = os.path.join(cfg.admin_dir.get_path(), _id)
|
||||
@@ -1170,10 +1158,6 @@ def highest_server(me):
|
||||
return sabnzbd.downloader.Downloader.do.highest_server(me)
|
||||
|
||||
|
||||
def proxy_pre_queue(name, pp, cat, script, priority, size, groups):
|
||||
return sabnzbd.newsunpack.pre_queue(name, pp, cat, script, priority, size, groups)
|
||||
|
||||
|
||||
def test_ipv6():
|
||||
""" Check if external IPv6 addresses are reachable """
|
||||
if not cfg.selftest_host():
|
||||
@@ -1199,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
|
||||
|
||||
@@ -27,7 +27,7 @@ import time
|
||||
import json
|
||||
import cherrypy
|
||||
import locale
|
||||
import socket
|
||||
|
||||
from threading import Thread
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
@@ -40,15 +40,14 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.constants import VALID_ARCHIVES, Status, \
|
||||
TOP_PRIORITY, REPAIR_PRIORITY, HIGH_PRIORITY, HIGH_PRIORITY, NORMAL_PRIORITY, LOW_PRIORITY, \
|
||||
TOP_PRIORITY, REPAIR_PRIORITY, HIGH_PRIORITY, NORMAL_PRIORITY, LOW_PRIORITY, \
|
||||
KIBI, MEBI, GIGI, JOB_ADMIN
|
||||
import sabnzbd.config as config
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.downloader import Downloader
|
||||
from sabnzbd.nzbqueue import NzbQueue, set_priority, sort_queue, scan_jobs, repair_job
|
||||
from sabnzbd.nzbqueue import NzbQueue
|
||||
import sabnzbd.scheduler as scheduler
|
||||
from sabnzbd.skintext import SKIN_TEXT
|
||||
from sabnzbd.utils.json import JsonWriter
|
||||
@@ -165,14 +164,8 @@ def _api_del_config(name, output, kwargs):
|
||||
|
||||
def _api_qstatus(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
if output == 'json':
|
||||
# Compatibility Fix:
|
||||
# Old qstatus did not have a keyword, so do not use one now.
|
||||
keyword = ''
|
||||
else:
|
||||
keyword = 'queue'
|
||||
info, pnfo_list, bytespersec = build_queue()
|
||||
return report(output, keyword='', data=remove_callable(info))
|
||||
return report(output, data=remove_callable(info))
|
||||
|
||||
|
||||
def _api_queue(name, output, kwargs):
|
||||
@@ -253,7 +246,7 @@ def _api_queue_priority(output, value, kwargs):
|
||||
priority = int(value2)
|
||||
except:
|
||||
return report(output, _MSG_INT_VALUE)
|
||||
pos = set_priority(value, priority)
|
||||
pos = NzbQueue.do.set_priority(value, priority)
|
||||
# Returns the position in the queue, -1 is incorrect job-id
|
||||
return report(output, keyword='position', data=pos)
|
||||
except:
|
||||
@@ -267,7 +260,7 @@ def _api_queue_sort(output, value, kwargs):
|
||||
sort = kwargs.get('sort')
|
||||
direction = kwargs.get('dir', '')
|
||||
if sort:
|
||||
sort_queue(sort, direction)
|
||||
NzbQueue.do.sort_queue(sort, direction)
|
||||
return report(output)
|
||||
else:
|
||||
return report(output, _MSG_NO_VALUE2)
|
||||
@@ -491,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):
|
||||
@@ -517,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)
|
||||
@@ -539,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:
|
||||
@@ -686,7 +678,7 @@ def _api_osx_icon(name, output, kwargs):
|
||||
|
||||
def _api_rescan(name, output, kwargs):
|
||||
""" API: accepts output """
|
||||
scan_jobs(all=False, action=True)
|
||||
NzbQueue.do.scan_jobs(all=False, action=True)
|
||||
return report(output)
|
||||
|
||||
|
||||
@@ -1196,7 +1188,7 @@ def build_status(skip_dashboard=False, output=None):
|
||||
info['logfile'] = sabnzbd.LOGFILE
|
||||
info['weblogfile'] = sabnzbd.WEBLOGFILE
|
||||
info['loglevel'] = str(cfg.log_level())
|
||||
info['folders'] = [xml_name(item) for item in sabnzbd.nzbqueue.scan_jobs(all=False, action=False)]
|
||||
info['folders'] = [xml_name(item) for item in NzbQueue.do.scan_jobs(all=False, action=False)]
|
||||
info['configfn'] = xml_name(config.get_filename())
|
||||
|
||||
# Dashboard: Speed of System
|
||||
@@ -1340,7 +1332,6 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
|
||||
priority = pnfo.priority
|
||||
mbleft = (bytesleft / MEBI)
|
||||
mb = (bytes / MEBI)
|
||||
missing = pnfo.missing
|
||||
|
||||
slot = {'index': n, 'nzo_id': str(nzo_id)}
|
||||
slot['unpackopts'] = str(sabnzbd.opts_to_pp(pnfo.repair, pnfo.unpack, pnfo.delete))
|
||||
@@ -1355,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)
|
||||
@@ -1525,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,
|
||||
@@ -1548,7 +1540,7 @@ def retry_job(job, new_nzb, password):
|
||||
else:
|
||||
path = history_db.get_path(job)
|
||||
if path:
|
||||
nzo_id = repair_job(platform_encode(path), new_nzb, password)
|
||||
nzo_id = NzbQueue.do.repair_job(platform_encode(path), new_nzb, password)
|
||||
history_db.remove_history(job)
|
||||
return nzo_id
|
||||
return None
|
||||
@@ -1592,6 +1584,7 @@ def Tspec(txt):
|
||||
else:
|
||||
return txt
|
||||
|
||||
|
||||
_SKIN_CACHE = {} # Stores pre-translated acronyms
|
||||
# This special is to be used in interface.py for template processing
|
||||
# to be passed for the $T function: so { ..., 'T' : Ttemplate, ...}
|
||||
@@ -1629,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 = {}
|
||||
|
||||
@@ -1662,18 +1654,17 @@ def build_header(webdir='', output=None):
|
||||
header['session'] = cfg.api_key()
|
||||
header['new_release'], header['new_rel_url'] = sabnzbd.NEW_VERSION
|
||||
|
||||
|
||||
header['version'] = sabnzbd.__version__
|
||||
header['paused'] = Downloader.do.paused or Downloader.do.postproc
|
||||
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
|
||||
@@ -1812,9 +1803,6 @@ def build_history(start=None, limit=None, verbose=False, verbose_list=None, sear
|
||||
cookie = cherrypy.request.cookie
|
||||
if 'history_verbosity' in cookie:
|
||||
k = cookie['history_verbosity'].value
|
||||
c_path = cookie['history_verbosity']['path']
|
||||
c_age = cookie['history_verbosity']['max-age']
|
||||
c_version = cookie['history_verbosity']['version']
|
||||
|
||||
if k == 'all':
|
||||
details_show_all = True
|
||||
|
||||
@@ -30,24 +30,26 @@ from sabnzbd.constants import GIGI, ANFO
|
||||
|
||||
ARTICLE_LOCK = threading.Lock()
|
||||
|
||||
|
||||
class ArticleCache(object):
|
||||
""" Operations on lists/dicts are atomic enough that we
|
||||
do not have to put locks. Only the cache-size needs
|
||||
a lock since the integer needs to stay synced.
|
||||
With less locking, the decoder and assembler do not
|
||||
have to wait on each other.
|
||||
"""
|
||||
do = None
|
||||
|
||||
def __init__(self):
|
||||
self.__cache_limit_org = 0
|
||||
self.__cache_limit = 0
|
||||
self.__cache_size = 0
|
||||
|
||||
self.__article_list = [] # List of buffered articles
|
||||
self.__article_table = {} # Dict of buffered articles
|
||||
ArticleCache.do = self
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def cache_info(self):
|
||||
return ANFO(len(self.__article_list), abs(self.__cache_size), self.__cache_limit_org)
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def new_limit(self, limit):
|
||||
""" Called when cache limit changes """
|
||||
self.__cache_limit_org = limit
|
||||
@@ -57,23 +59,28 @@ class ArticleCache(object):
|
||||
self.__cache_limit = min(limit, GIGI)
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def increase_cache_size(self, value):
|
||||
self.__cache_size += value
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def decrease_cache_size(self, value):
|
||||
self.__cache_size -= value
|
||||
|
||||
def reserve_space(self, data):
|
||||
""" Is there space left in the set limit? """
|
||||
data_size = sys.getsizeof(data) * 64
|
||||
self.__cache_size += data_size
|
||||
self.increase_cache_size(data_size)
|
||||
if self.__cache_size + data_size > self.__cache_limit:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def free_reserve_space(self, data):
|
||||
""" Remove previously reserved space """
|
||||
data_size = sys.getsizeof(data) * 64
|
||||
self.__cache_size -= data_size
|
||||
self.decrease_cache_size(data_size)
|
||||
return self.__cache_size + data_size < self.__cache_limit
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def save_article(self, article, data):
|
||||
nzf = article.nzf
|
||||
nzo = nzf.nzo
|
||||
@@ -81,8 +88,6 @@ class ArticleCache(object):
|
||||
if nzo.is_gone():
|
||||
# Do not discard this article because the
|
||||
# file might still be processed at this moment!!
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("%s is discarded", article)
|
||||
return
|
||||
|
||||
saved_articles = article.nzf.nzo.saved_articles
|
||||
@@ -93,7 +98,6 @@ class ArticleCache(object):
|
||||
if self.__cache_limit:
|
||||
if self.__cache_limit < 0:
|
||||
self.__add_to_cache(article, data)
|
||||
|
||||
else:
|
||||
data_size = len(data)
|
||||
|
||||
@@ -102,7 +106,7 @@ class ArticleCache(object):
|
||||
# Flush oldest article in cache
|
||||
old_article = self.__article_list.pop(0)
|
||||
old_data = self.__article_table.pop(old_article)
|
||||
self.__cache_size -= len(old_data)
|
||||
self.decrease_cache_size(len(old_data))
|
||||
# No need to flush if this is a refreshment article
|
||||
if old_article != article:
|
||||
self.__flush_article(old_article, old_data)
|
||||
@@ -116,7 +120,6 @@ class ArticleCache(object):
|
||||
else:
|
||||
self.__flush_article(article, data)
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def load_article(self, article):
|
||||
data = None
|
||||
nzo = article.nzf.nzo
|
||||
@@ -124,9 +127,7 @@ class ArticleCache(object):
|
||||
if article in self.__article_list:
|
||||
data = self.__article_table.pop(article)
|
||||
self.__article_list.remove(article)
|
||||
self.__cache_size -= len(data)
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Loaded %s from cache", article)
|
||||
self.decrease_cache_size(len(data))
|
||||
elif article.art_id:
|
||||
data = sabnzbd.load_data(article.art_id, nzo.workpath, remove=True,
|
||||
do_pickle=False, silent=True)
|
||||
@@ -136,23 +137,19 @@ class ArticleCache(object):
|
||||
|
||||
return data
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def flush_articles(self):
|
||||
self.__cache_size = 0
|
||||
while self.__article_list:
|
||||
article = self.__article_list.pop(0)
|
||||
data = self.__article_table.pop(article)
|
||||
self.__flush_article(article, data)
|
||||
self.__cache_size = 0
|
||||
|
||||
@synchronized(ARTICLE_LOCK)
|
||||
def purge_articles(self, articles):
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Purgeable articles -> %s", articles)
|
||||
for article in articles:
|
||||
if article in self.__article_list:
|
||||
self.__article_list.remove(article)
|
||||
data = self.__article_table.pop(article)
|
||||
self.__cache_size -= len(data)
|
||||
self.decrease_cache_size(len(data))
|
||||
if article.art_id:
|
||||
sabnzbd.remove_data(article.art_id, article.nzf.nzo.workpath)
|
||||
|
||||
@@ -163,14 +160,10 @@ class ArticleCache(object):
|
||||
if nzo.is_gone():
|
||||
# Do not discard this article because the
|
||||
# file might still be processed at this moment!!
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("%s is discarded", article)
|
||||
return
|
||||
|
||||
art_id = article.get_art_id()
|
||||
if art_id:
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Flushing %s to disk", article)
|
||||
# Save data, but don't complain when destination folder is missing
|
||||
# because this flush may come after completion of the NZO.
|
||||
sabnzbd.save_data(data, art_id, nzo.workpath, do_pickle=False, silent=True)
|
||||
@@ -179,14 +172,12 @@ class ArticleCache(object):
|
||||
|
||||
def __add_to_cache(self, article, data):
|
||||
if article in self.__article_table:
|
||||
self.__cache_size -= len(self.__article_table[article])
|
||||
self.decrease_cache_size(len(self.__article_table[article]))
|
||||
else:
|
||||
self.__article_list.append(article)
|
||||
|
||||
self.__article_table[article] = data
|
||||
self.__cache_size += len(data)
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug("Added %s to cache", article)
|
||||
self.increase_cache_size(len(data))
|
||||
|
||||
|
||||
# Create the instance
|
||||
|
||||
@@ -30,14 +30,14 @@ 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
|
||||
import sabnzbd.downloader
|
||||
import sabnzbd.utils.rarfile as rarfile
|
||||
from sabnzbd.encoding import unicoder, deunicode, is_utf8
|
||||
from sabnzbd.encoding import unicoder, is_utf8
|
||||
from sabnzbd.rating import Rating
|
||||
|
||||
|
||||
@@ -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,56 +132,104 @@ class Assembler(Thread):
|
||||
|
||||
filter, reason = nzo_filtered_by_rating(nzo)
|
||||
if filter == 1:
|
||||
logging.warning(Ta('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(Ta('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)
|
||||
|
||||
nzf.completed = True
|
||||
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):
|
||||
@@ -201,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
|
||||
@@ -273,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
|
||||
|
||||
@@ -431,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
|
||||
|
||||
@@ -73,18 +73,6 @@ def last_month_day(tm):
|
||||
return day
|
||||
|
||||
|
||||
def this_month_day(t=None):
|
||||
""" Return current day of the week, month 1..31 """
|
||||
t = t or time.localtime(t)
|
||||
return time.localtime(t).tm_mday
|
||||
|
||||
|
||||
def this_week_day(t=None):
|
||||
""" Return current day of the week 1..7 """
|
||||
t = t or time.localtime(t)
|
||||
return time.localtime(t).tm_wday + 1
|
||||
|
||||
|
||||
def next_month(t):
|
||||
""" Return timestamp for start of next month """
|
||||
now = time.localtime(t)
|
||||
|
||||
@@ -22,7 +22,7 @@ import re
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.constants import DEF_HOST, DEF_PORT, DEF_STDINTF, DEF_ADMIN_DIR, \
|
||||
DEF_DOWNLOAD_DIR, DEF_NZBBACK_DIR, DEF_SCANRATE, DEF_COMPLETE_DIR
|
||||
DEF_DOWNLOAD_DIR, DEF_NZBBACK_DIR, DEF_SCANRATE, DEF_COMPLETE_DIR, QUEUE_VERSION
|
||||
|
||||
from sabnzbd.config import OptionBool, OptionNumber, OptionPassword, \
|
||||
OptionDir, OptionStr, OptionList, no_nonsense, \
|
||||
@@ -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)
|
||||
@@ -91,6 +92,7 @@ enable_7zip = OptionBool('misc', 'enable_7zip', True)
|
||||
enable_recursive = OptionBool('misc', 'enable_recursive', True)
|
||||
enable_filejoin = OptionBool('misc', 'enable_filejoin', True)
|
||||
enable_tsjoin = OptionBool('misc', 'enable_tsjoin', True)
|
||||
enable_par_cleanup = OptionBool('misc', 'enable_par_cleanup', True)
|
||||
enable_all_par = OptionBool('misc', 'enable_all_par', False)
|
||||
ignore_unrar_dates = OptionBool('misc', 'ignore_unrar_dates', False)
|
||||
overwrite_files = OptionBool('misc', 'overwrite_files', False)
|
||||
@@ -100,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)
|
||||
@@ -141,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)
|
||||
@@ -171,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)
|
||||
|
||||
@@ -208,8 +209,9 @@ cache_limit = OptionStr('misc', 'cache_limit')
|
||||
web_dir = OptionStr('misc', 'web_dir', DEF_STDINTF)
|
||||
web_color = OptionStr('misc', 'web_color', '')
|
||||
cleanup_list = OptionList('misc', 'cleanup_list')
|
||||
warned_old_queue = OptionBool('misc', 'warned_old_queue9', False)
|
||||
warned_old_queue = OptionNumber('misc', 'warned_old_queue', QUEUE_VERSION)
|
||||
notified_new_skin = OptionNumber('misc', 'notified_new_skin', 0)
|
||||
converted_nzo_pickles = OptionBool('misc', 'converted_nzo_pickles', False)
|
||||
|
||||
unwanted_extensions = OptionList('misc', 'unwanted_extensions')
|
||||
action_on_unwanted_extensions = OptionNumber('misc', 'action_on_unwanted_extensions', 0)
|
||||
@@ -243,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)
|
||||
@@ -257,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)
|
||||
@@ -271,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)
|
||||
@@ -285,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)
|
||||
@@ -301,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)
|
||||
@@ -319,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)
|
||||
@@ -333,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)
|
||||
@@ -349,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'):
|
||||
@@ -791,7 +792,6 @@ def _read_config(path, try_backup=False):
|
||||
def save_config(force=False):
|
||||
""" Update Setup file with current option values """
|
||||
global CFG, database, modified
|
||||
if 0: assert isinstance(CFG, configobj.ConfigObj)
|
||||
|
||||
if not (modified or force):
|
||||
return True
|
||||
@@ -978,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 {}
|
||||
@@ -1065,15 +1080,6 @@ def validate_safedir(root, value, default):
|
||||
return T('Error: Queue not empty, cannot change folder.'), None
|
||||
|
||||
|
||||
def validate_dir_exists(root, value, default):
|
||||
""" Check if directory exists """
|
||||
p = sabnzbd.misc.real_path(root, value)
|
||||
if os.path.exists(p):
|
||||
return None, value
|
||||
else:
|
||||
return T('Folder "%s" does not exist') % p, None
|
||||
|
||||
|
||||
def validate_notempty(root, value, default):
|
||||
""" If value is empty, return default """
|
||||
if value:
|
||||
@@ -1084,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())
|
||||
@@ -1100,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'
|
||||
@@ -55,19 +54,14 @@ REPAIR_REQUEST = 'repair-all.sab'
|
||||
SABYENC_VERSION_REQUIRED = '3.0.2'
|
||||
|
||||
DB_HISTORY_VERSION = 1
|
||||
DB_QUEUE_VERSION = 1
|
||||
|
||||
DB_HISTORY_NAME = 'history%s.db' % DB_HISTORY_VERSION
|
||||
DB_QUEUE_NAME = 'queue%s.db' % DB_QUEUE_VERSION
|
||||
|
||||
DEF_DOWNLOAD_DIR = 'Downloads/incomplete'
|
||||
DEF_COMPLETE_DIR = 'Downloads/complete'
|
||||
DEF_ADMIN_DIR = 'admin'
|
||||
DEF_LOG_DIR = 'logs'
|
||||
DEF_NZBBACK_DIR = ''
|
||||
DEF_LANGUAGE = 'locale'
|
||||
DEF_INTERFACES = 'interfaces'
|
||||
DEF_INT_LANGUAGE = 'locale'
|
||||
DEF_EMAIL_TMPL = 'email'
|
||||
DEF_STDCONFIG = 'Config'
|
||||
DEF_STDINTF = 'Glitter'
|
||||
@@ -82,11 +76,7 @@ DEF_LOG_ERRFILE = 'sabnzbd.error.log'
|
||||
DEF_LOG_CHERRY = 'cherrypy.log'
|
||||
DEF_CACHE_LIMIT = '450M'
|
||||
DEF_TIMEOUT = 60
|
||||
MIN_TIMEOUT = 10
|
||||
MAX_TIMEOUT = 200
|
||||
DEF_LOGLEVEL = 1
|
||||
DEF_SCANRATE = 5
|
||||
DEF_QRATE = 0
|
||||
MAX_DECODE_QUEUE = 10
|
||||
LIMIT_DECODE_QUEUE = 100
|
||||
MAX_WARNINGS = 20
|
||||
|
||||
@@ -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 """
|
||||
@@ -531,7 +535,7 @@ def unpack_history_info(item):
|
||||
if item['script_log']:
|
||||
item['script_log'] = ''
|
||||
# The action line is only available for items in the postproc queue
|
||||
if not item.has_key('action_line'):
|
||||
if 'action_line' not in item:
|
||||
item['action_line'] = ''
|
||||
return item
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
sabnzbd.decoder - article decoder
|
||||
"""
|
||||
|
||||
import Queue
|
||||
import binascii
|
||||
import logging
|
||||
import re
|
||||
import hashlib
|
||||
from time import sleep
|
||||
from threading import Thread
|
||||
|
||||
@@ -31,9 +31,8 @@ from sabnzbd.constants import Status, MAX_DECODE_QUEUE, LIMIT_DECODE_QUEUE, SABY
|
||||
import sabnzbd.articlecache
|
||||
import sabnzbd.downloader
|
||||
import sabnzbd.nzbqueue
|
||||
import sabnzbd.cfg as cfg
|
||||
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:
|
||||
@@ -70,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):
|
||||
@@ -115,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
|
||||
|
||||
@@ -178,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
|
||||
@@ -187,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
|
||||
@@ -199,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:
|
||||
@@ -211,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:
|
||||
@@ -224,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):
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
##############################################################################
|
||||
from threading import RLock, Condition
|
||||
|
||||
NZBQUEUE_LOCK = RLock()
|
||||
CV = Condition(NZBQUEUE_LOCK)
|
||||
IO_LOCK = RLock()
|
||||
DOWNLOADER_CV = Condition(RLock())
|
||||
|
||||
def synchronized(lock):
|
||||
def wrap(f):
|
||||
@@ -36,13 +34,13 @@ def synchronized(lock):
|
||||
return wrap
|
||||
|
||||
|
||||
def synchronized_CV(func):
|
||||
global CV
|
||||
def notify_downloader(func):
|
||||
global DOWNLOADER_CV
|
||||
def call_func(*params, **kparams):
|
||||
CV.acquire()
|
||||
DOWNLOADER_CV.acquire()
|
||||
try:
|
||||
return func(*params, **kparams)
|
||||
finally:
|
||||
CV.notifyAll()
|
||||
CV.release()
|
||||
DOWNLOADER_CV.notify_all()
|
||||
DOWNLOADER_CV.release()
|
||||
return call_func
|
||||
|
||||
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)
|
||||
@@ -99,7 +99,6 @@ def ProcessArchiveFile(filename, path, pp=None, script=None, cat=None, catdir=No
|
||||
returns (status, nzo_ids)
|
||||
status: -1==Error/Retry, 0==OK, 1==Ignore
|
||||
"""
|
||||
from sabnzbd.nzbqueue import add_nzo
|
||||
nzo_ids = []
|
||||
if catdir is None:
|
||||
catdir = cat
|
||||
@@ -145,7 +144,7 @@ def ProcessArchiveFile(filename, path, pp=None, script=None, cat=None, catdir=No
|
||||
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo_id, add_to_history=False)
|
||||
nzo.nzo_id = nzo_id
|
||||
nzo_id = None
|
||||
nzo_ids.append(add_nzo(nzo))
|
||||
nzo_ids.append(sabnzbd.nzbqueue.NzbQueue.do.add(nzo))
|
||||
nzo.update_rating()
|
||||
zf.close()
|
||||
try:
|
||||
@@ -170,7 +169,6 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
|
||||
returns (status, nzo_ids)
|
||||
status: -2==Error/retry, -1==Error, 0==OK, 1==OK-but-ignorecannot-delete
|
||||
"""
|
||||
from sabnzbd.nzbqueue import add_nzo
|
||||
nzo_ids = []
|
||||
if catdir is None:
|
||||
catdir = cat
|
||||
@@ -231,7 +229,7 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
|
||||
# Re-use existing nzo_id, when a "future" job gets it payload
|
||||
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo_id, add_to_history=False)
|
||||
nzo.nzo_id = nzo_id
|
||||
nzo_ids.append(add_nzo(nzo, quiet=reuse))
|
||||
nzo_ids.append(sabnzbd.nzbqueue.NzbQueue.do.add(nzo, quiet=reuse))
|
||||
nzo.update_rating()
|
||||
try:
|
||||
if not keep:
|
||||
|
||||
@@ -30,7 +30,7 @@ import sys
|
||||
import Queue
|
||||
|
||||
import sabnzbd
|
||||
from sabnzbd.decorators import synchronized, synchronized_CV, CV
|
||||
from sabnzbd.decorators import synchronized, notify_downloader, DOWNLOADER_CV
|
||||
from sabnzbd.constants import MAX_DECODE_QUEUE, LIMIT_DECODE_QUEUE
|
||||
from sabnzbd.decoder import Decoder
|
||||
from sabnzbd.newswrapper import NewsWrapper, request_server_info
|
||||
@@ -252,12 +252,12 @@ class Downloader(Thread):
|
||||
|
||||
return
|
||||
|
||||
@synchronized_CV
|
||||
@notify_downloader
|
||||
def set_paused_state(self, state):
|
||||
""" Set downloader to specified paused state """
|
||||
self.paused = state
|
||||
|
||||
@synchronized_CV
|
||||
@notify_downloader
|
||||
def resume(self):
|
||||
# Do not notify when SABnzbd is still starting
|
||||
if self.paused and sabnzbd.WEB_DIR:
|
||||
@@ -265,7 +265,7 @@ class Downloader(Thread):
|
||||
notifier.send_notification("SABnzbd", T('Resuming'), 'download')
|
||||
self.paused = False
|
||||
|
||||
@synchronized_CV
|
||||
@notify_downloader
|
||||
def pause(self, save=True):
|
||||
""" Pause the downloader, optionally saving admin """
|
||||
if not self.paused:
|
||||
@@ -279,22 +279,20 @@ class Downloader(Thread):
|
||||
if save:
|
||||
ArticleCache.do.flush_articles()
|
||||
|
||||
@synchronized_CV
|
||||
def delay(self):
|
||||
logging.debug("Delaying")
|
||||
self.delayed = True
|
||||
|
||||
@synchronized_CV
|
||||
@notify_downloader
|
||||
def undelay(self):
|
||||
logging.debug("Undelaying")
|
||||
self.delayed = False
|
||||
|
||||
@synchronized_CV
|
||||
def wait_for_postproc(self):
|
||||
logging.info("Waiting for post-processing to finish")
|
||||
self.postproc = True
|
||||
|
||||
@synchronized_CV
|
||||
@notify_downloader
|
||||
def resume_from_postproc(self):
|
||||
logging.info("Post-processing finished, resuming download")
|
||||
self.postproc = False
|
||||
@@ -302,7 +300,6 @@ class Downloader(Thread):
|
||||
def disconnect(self):
|
||||
self.force_disconnect = True
|
||||
|
||||
@synchronized_CV
|
||||
def limit_speed(self, value):
|
||||
''' Set the actual download speed in Bytes/sec
|
||||
When 'value' ends with a '%' sign or is within 1-100, it is interpreted as a pecentage of the maximum bandwidth
|
||||
@@ -356,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
|
||||
@@ -421,7 +417,6 @@ class Downloader(Thread):
|
||||
|
||||
while 1:
|
||||
for server in self.servers:
|
||||
if 0: assert isinstance(server, Server) # Assert only for debug purposes
|
||||
for nw in server.busy_threads[:]:
|
||||
if (nw.nntp and nw.nntp.error_msg) or (nw.timeout and time.time() > nw.timeout):
|
||||
if nw.nntp and nw.nntp.error_msg:
|
||||
@@ -445,7 +440,6 @@ class Downloader(Thread):
|
||||
# Restart pending, don't add new articles
|
||||
continue
|
||||
|
||||
if 0: assert isinstance(server, Server) # Assert only for debug purposes
|
||||
if not server.idle_threads or server.restart or self.is_paused() or self.shutdown or self.delayed or self.postproc:
|
||||
continue
|
||||
|
||||
@@ -453,7 +447,6 @@ class Downloader(Thread):
|
||||
continue
|
||||
|
||||
for nw in server.idle_threads[:]:
|
||||
if 0: assert isinstance(nw, NewsWrapper) # Assert only for debug purposes
|
||||
if nw.timeout:
|
||||
if time.time() < nw.timeout:
|
||||
continue
|
||||
@@ -471,10 +464,11 @@ class Downloader(Thread):
|
||||
break
|
||||
|
||||
if server.retention and article.nzf.nzo.avg_stamp < time.time() - server.retention:
|
||||
# Article too old for the server, treat as missing
|
||||
if sabnzbd.LOG_ALL:
|
||||
logging.debug('Article %s too old for %s', article.article, server.id)
|
||||
self.decode(article, None, None)
|
||||
# Let's get rid of all the articles for this server at once
|
||||
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)
|
||||
break
|
||||
|
||||
server.idle_threads.remove(nw)
|
||||
@@ -553,11 +547,11 @@ class Downloader(Thread):
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
CV.acquire()
|
||||
DOWNLOADER_CV.acquire()
|
||||
while (sabnzbd.nzbqueue.NzbQueue.do.is_empty() or self.is_paused() or self.delayed or self.postproc) and not \
|
||||
self.shutdown and not self.__restart:
|
||||
CV.wait()
|
||||
CV.release()
|
||||
DOWNLOADER_CV.wait()
|
||||
DOWNLOADER_CV.release()
|
||||
|
||||
self.force_disconnect = False
|
||||
|
||||
@@ -714,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()
|
||||
|
||||
@@ -866,7 +860,7 @@ class Downloader(Thread):
|
||||
del self._timers[server_id]
|
||||
self.init_server(server_id, server_id)
|
||||
|
||||
@synchronized_CV
|
||||
@notify_downloader
|
||||
@synchronized(TIMER_LOCK)
|
||||
def unblock(self, server_id):
|
||||
# Remove timer
|
||||
@@ -885,7 +879,7 @@ class Downloader(Thread):
|
||||
for server_id in self._timers.keys():
|
||||
self.unblock(server_id)
|
||||
|
||||
@synchronized_CV
|
||||
@notify_downloader
|
||||
@synchronized(TIMER_LOCK)
|
||||
def check_timers(self):
|
||||
""" Make sure every server without a non-expired timer is active """
|
||||
@@ -905,11 +899,10 @@ class Downloader(Thread):
|
||||
logging.debug('Forcing activation of server %s', server.id)
|
||||
self.init_server(server.id, server.id)
|
||||
|
||||
@synchronized_CV
|
||||
def update_server(self, oldserver, newserver):
|
||||
self.init_server(oldserver, newserver)
|
||||
|
||||
@synchronized_CV
|
||||
@notify_downloader
|
||||
def wakeup(self):
|
||||
""" Just rattle the semaphore """
|
||||
pass
|
||||
@@ -920,12 +913,12 @@ class Downloader(Thread):
|
||||
|
||||
|
||||
def stop():
|
||||
CV.acquire()
|
||||
DOWNLOADER_CV.acquire()
|
||||
try:
|
||||
Downloader.do.stop()
|
||||
finally:
|
||||
CV.notifyAll()
|
||||
CV.release()
|
||||
DOWNLOADER_CV.notify_all()
|
||||
DOWNLOADER_CV.release()
|
||||
try:
|
||||
Downloader.do.join()
|
||||
except:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user