Compare commits

..

110 Commits

Author SHA1 Message Date
Safihre
01dfb7538d Correct FileList Move to Top/Bottom CSS for Firefox 2017-07-16 14:28:04 +02:00
Safihre
3f0d4675b6 Fix CSS for Direct Unpack and Move to Top/Bottom 2017-07-16 14:18:50 +02:00
Safihre
f23c5caf80 Fix typo in DirectRenamer for non-Windows 2017-07-16 13:55:36 +02:00
Safihre
bd22430b26 Update text files for 2.2.0Alpha3 2017-07-16 11:04:17 +02:00
Safihre
1189a7fdbc Use tuple in endswith for Direct Unpack
Thanks @hellowlol
2017-07-16 10:59:00 +02:00
Safihre
f3aa4f84fc Remove waiting-time between URLGrab's
Other newsreaders grab multiple URL's at once, so no need for us to wait.
2017-07-16 10:40:39 +02:00
Safihre
ea26ce4700 Remove non-seperator RSS-url commas by detecting if they are valid URLs
Closes #965
2017-07-16 10:30:09 +02:00
Safihre
a1e649b7e2 Correct error in PAR_Verify with renames 2017-07-15 23:43:32 +02:00
Safihre
3b9f2b2cf0 Remove par2classic/cmdline for Windows and macOS 2017-07-15 23:33:20 +02:00
Safihre
7333d19e1c Notifications selection based on Categories
Closes #716
2017-07-15 22:22:20 +02:00
Safihre
232d537d23 Correct Direct Unpack locking behavior for multisets 2017-07-15 17:02:20 +02:00
Safihre
c6e17e7bcb Duplicate par2-16k values need force-remove 2017-07-15 17:02:20 +02:00
Safihre
54c6fd55dd Detection of forbidden-Windows names altered
Now we already sanatize the name during Assembler and when we have to make decisions for Unrar/Par2 we need to know if they might create something unsafe.
2017-07-15 17:02:20 +02:00
Safihre
0625aa1ca8 Make sure all Par2-16k signatures are unique, also in multisets 2017-07-15 17:02:20 +02:00
Safihre
83643f3298 Remove allow_streaming
Bit redundant now we have DirectUnpack
2017-07-15 17:02:20 +02:00
Safihre
ff3c46fe1f Remove enable_meta 2017-07-15 17:02:20 +02:00
Safihre
0930f0dcee Test disk-speed first time DirectUnpack is called 2017-07-15 17:02:20 +02:00
Safihre
3221257310 UnRar's ERROR is also an error
And add starting file to log.
2017-07-15 17:02:20 +02:00
Safihre
8048a73156 Handle active DirectUnpacker in postproc better 2017-07-15 17:02:20 +02:00
Safihre
ea552cd402 Cancel DirectUnpack when the final name changes 2017-07-15 17:02:20 +02:00
Safihre
dcb925f621 Case insensitive matching for DirectUnpack sets 2017-07-15 17:02:20 +02:00
Safihre
cce91e1985 DirectUnpacker should stay to listen to new sets 2017-07-15 17:02:20 +02:00
Safihre
e17d417c2e Re-introduce locks for TryList
After studying everything, it really needs it. Closes #738
2017-07-15 17:02:20 +02:00
Safihre
a69f5bd2df Prevent DirectUnpack locking the PostProcessing 2017-07-15 17:02:20 +02:00
Safihre
97e53eb4d3 Better DirectUnpack percentage counter 2017-07-15 17:02:20 +02:00
Safihre
a6da2b7bee Prevent possible crash in par2_repair 2017-07-15 17:02:20 +02:00
Safihre
4a21e7c217 Show percentage of DirectUnpack, when available 2017-07-15 17:02:20 +02:00
Safihre
9bd3c7be44 Increase maximum number of unpackers
Unrar takes almost no memory anyway
2017-07-15 17:02:20 +02:00
Safihre
434f5c4b2d Remove Audio/Video quality rating icons from Queue 2017-07-15 17:02:20 +02:00
Safihre
d3cc4f9f07 Direct Unpack indicator for Queue 2017-07-15 17:02:20 +02:00
Safihre
a16aa17c17 Don't start when not set to +Unpack and abort if Category changed 2017-07-15 17:02:20 +02:00
Safihre
68445d0409 Full working implementation of DirectUnpack with multi-sets 2017-07-15 17:02:20 +02:00
Safihre
32b68a45cc Integrate with PostProc 2017-07-15 17:02:20 +02:00
Safihre
345f8359cc Unpack to the right directory (with Sorter support) 2017-07-15 17:02:20 +02:00
Safihre
81f9886584 Add Direct Unpack to Config 2017-07-15 17:02:20 +02:00
Safihre
adbc618808 Improvements to detection of volumes 2017-07-15 17:02:20 +02:00
Safihre
41eafc6b4b Become set-specific 2017-07-15 17:02:20 +02:00
Safihre
9f18d8e8c1 Basic working Direct Unpack
Lots to do
2017-07-15 17:02:20 +02:00
Safihre
8c2c853166 Make sure to always have lowest part number 2017-07-15 17:02:20 +02:00
Safihre
97914906a0 Also handle GNTP errors during sending 2017-07-14 14:43:39 +02:00
Safihre
f1ce4ed19b Correctly handle new GNTP errors 2017-07-14 14:41:03 +02:00
Safihre
99185d8151 Update GNTP to 1.0.3
Closes #334
2017-07-14 14:25:07 +02:00
Safihre
385b6b7ade Remove QCHECK_FILE again 2017-07-14 14:25:07 +02:00
gwyden
81ea513f8c Added buttons and logic to move to top and bottom of download queue (#962)
* added buttons and logic to move to top and bottom of queue
* allowed for a larger control box for the new buttons
* Cleanup of unnecessary code
* Simple top and bottom of queue using existing queue data
2017-07-13 23:52:43 +02:00
Safihre
336b1ddba3 Always remove forbidden Win-devices from filenames
This breaks support for par2cmdline on Windows with forbidden names. Assuming no users that have disabled both Multipar *and* par2_multicore
2017-07-12 18:38:19 +02:00
Safihre
7274973322 Shorten par_cleanup code 2017-07-12 18:38:19 +02:00
Safihre
af132965de Revert "Remove QCHECK_FILE, not needed"
This reverts commit 4f8cc3f697.
2017-07-12 18:38:19 +02:00
Safihre
5586742886 Use RarFile.volumelist to get list of used rar-volumes 2017-07-12 18:38:19 +02:00
Safihre
5868b51490 Use fix to allow unicode arguments to POpen on Windows 2017-07-12 18:38:19 +02:00
Travis
7f17a38b9b Automatic translation update 2017-07-12 15:10:14 +00:00
Safihre
415e843ebb Remove 'WARNING:' label from Assembler warnings
It was inconsistent with other messages
2017-07-11 13:33:50 +02:00
Safihre
7ffc1192bb Only par2-rename when actually different 2017-07-11 12:00:36 +02:00
Safihre
945e769a03 Also performe prospective-par2 on renamed files 2017-07-10 23:06:05 +02:00
Safihre
86c7fb86cc Ignore first-16k par2 info if it's not unique 2017-07-10 22:51:17 +02:00
Safihre
ff20f3f620 Fix possible unicode error in tvsort and typo in newsunpack
Closes #950
2017-07-10 21:56:07 +02:00
Safihre
e8bef94706 Correctly handle renames on (multiple) retries 2017-07-10 21:03:37 +02:00
Safihre
d05fe2d680 More uniform handeling of renames 2017-07-10 20:53:31 +02:00
Safihre
4f8cc3f697 Remove QCHECK_FILE, not needed 2017-07-10 19:54:59 +02:00
Safihre
6fa619fa37 More robust renaming based on par2 first-16k info
Also when the correct name is
2017-07-10 17:40:39 +02:00
Safihre
a43f5369ea Do not rename .par2 filenames from NZB
They are usually correct, if mentioned at all
2017-07-10 17:29:34 +02:00
Safihre
2040173dc2 Rename parts of Assembler to be more coherent 2017-07-10 17:20:03 +02:00
Safihre
a15b7ec7ac Remove Windows utf8 detection using par2
Obsolute now we have Multipar
2017-07-10 17:17:12 +02:00
Safihre
6adcf2ce10 Stylistic changes from previous commits 2017-07-10 17:11:32 +02:00
Safihre
e756b9b5c1 Correct filenames while downloading using first-16kb par2 info
Maybe we can also do DirectUnpack!
2017-07-10 17:07:16 +02:00
Safihre
b3de745849 Do not use article-filename if it looks obfuscated 2017-07-10 15:54:17 +02:00
Safihre
77f3dc18b5 Corrections of Move To Top for filelists 2017-07-09 19:51:18 +03:00
gwyden
6b2f15f82e Move To Top/Move To Bottom buttons for filelists (#959)
* Control creation

* JQuery to make the buttons work

* minor text fixes

* tab to spaces cleanup

* style additions and removed hard text from code

* Moved button control to modal finish render event, gave file details a little more room

* Moved control to replace age and size on mouseover

* Added margins and color corrected for the night theme

* resolved night theme readability

* move to working top and bottom

* controls would lose event bindings after the append.  Detach first then insert

* Move to Top and Bottom buttons for files in each NZB
2017-07-09 18:34:33 +02:00
Safihre
570e58611d Repair would fail if extrapars were deleted by previous run
Closes #961
2017-07-06 18:30:31 +03:00
Safihre
6b69010aec Add logging for missing NZF database to debug #952 2017-06-28 11:35:52 +02:00
Travis
e3e2fb7057 Automatic translation update 2017-06-27 09:23:17 +00:00
Safihre
ece04909e7 Add latest changes to changelog 2017-06-27 00:13:24 +02:00
Safihre
963920eb88 Semi-correct missing MB counter for Pre-check
It's still off (for Precheck only), but not sure why
2017-06-26 22:03:50 +02:00
Safihre
cf5fa542b6 Don't show import errors when NZO is gone 2017-06-26 21:36:49 +02:00
Safihre
1be7e99754 Remove last hashlib workaround 2017-06-26 21:00:17 +02:00
Safihre
14e3334682 Correctly handle disk-space calculations
No more glitches in the interface during downloading.
2017-06-26 16:25:03 +02:00
Safihre
b1e033dd55 Update text files for 2.2.0Alpa2 2017-06-26 14:28:22 +02:00
Safihre
111feb1b57 Show missing articles as MB instead of number of articles 2017-06-26 13:46:54 +02:00
Safihre
886b23d034 Update translatable texts 2017-06-26 10:40:02 +02:00
Safihre
f2590792b3 Download all par2 always when enable_par_cleanup is disabled
https://forums.sabnzbd.org/viewtopic.php?f=2&t=22744
2017-06-26 09:19:05 +02:00
Safihre
02a497ed74 Only set Post-processing Completed/Failed at the very end
To prevent race-issues
2017-06-25 20:32:45 +02:00
Safihre
48df0eed84 Add logging for user-actions
We were missing way too many things
2017-06-24 23:18:14 +02:00
Travis
0f58cbb671 Automatic translation update 2017-06-24 18:51:32 +00:00
Safihre
9d71670f59 Full hearted 2017-06-24 20:33:16 +02:00
Safihre
7f838ebb38 Move Donate-link in Glitter 2017-06-24 13:51:19 +02:00
Safihre
ef1cb05bc8 Store result of MultiPar verification
Just in case prospective par2 didn't catch them all
2017-06-24 10:47:33 +02:00
Safihre
c14b3ed82a Prospective Par2 correct TryList reset
I think
2017-06-24 00:34:57 +02:00
Safihre
792e337936 Rename increase_last_history_update to history_updated 2017-06-23 22:58:29 +02:00
Safihre
6cd2e66052 Use actual counter for LAST_HISTORY_UPDATE
Would otherwise miss some updates
2017-06-23 12:49:40 +02:00
Safihre
728022b86d Show Verifying Repair stage for MultiPar 2017-06-23 11:10:47 +02:00
Safihre
7718446313 Convert HTML to text in warning messages
Related: #952
2017-06-23 08:30:54 +02:00
Travis
66dea54053 Automatic translation update 2017-06-22 20:34:50 +00:00
Safihre
f19b60bd41 Also don't list line numbers for NSIS pot file 2017-06-22 21:46:55 +02:00
Safihre
09f1c92856 Move enable_multipar to Specials
Moving forward to make MultiPar the only used par2-solution on Windows.
2017-06-22 11:03:53 +02:00
Safihre
589715901d Correct counting during Checking/Verification in MultiPar 2017-06-22 10:50:00 +02:00
Safihre
3f1a5ff5e0 Fix typo in extract_pot.py 2017-06-22 09:32:59 +02:00
Safihre
49cd956d4c Do not list line-number for POT files
To avoid commit-overhead when updating texts
2017-06-21 22:17:49 +02:00
Safihre
f9acde862f Correct counting in MultiPar Checking 2017-06-21 21:40:24 +02:00
Safihre
503e1dd899 Re-work NZO_LOCK to actually lock when saving 2017-06-21 20:54:00 +02:00
Safihre
c8e12b948d Mixed up application of use_pickle 2017-06-21 17:18:16 +02:00
Safihre
18949d68c0 Fix wrong addition to en.po
Thx @thezoggy!
2017-06-21 17:12:19 +02:00
Safihre
0c51b6c016 Add Donate links to main Config page and Glitter help modal 2017-06-21 09:57:56 +02:00
Safihre
63a5c22c1f Don't continue when fetching failed
Possibly: #914
2017-06-21 09:19:25 +02:00
Safihre
f76e2a7b56 All links to sabnzbd.org should be HTTPS 2017-06-20 23:15:15 +02:00
Safihre
bab151d6f5 Properly fix redirect after enabeling/disabeling HTTPS 2017-06-20 22:46:48 +02:00
Safihre
d43fec088b Fix typo in Correct redirect when enabeling HTTPS 2017-06-20 19:48:18 +02:00
Safihre
a8ca1cbcd7 Correct redirect when enabeling HTTPS 2017-06-20 19:47:45 +02:00
Safihre
ada3494483 Fix typo in Config JavaScript 2017-06-20 19:04:38 +02:00
Safihre
43c238b7f1 Update translations 2017-06-17 11:23:50 +02:00
Safihre
128d10c51e Restart-text was always shown in English 2017-06-17 11:19:59 +02:00
Safihre
1a1e01f9f6 Correct upgrade-notice 2017-06-17 11:13:53 +02:00
111 changed files with 23251 additions and 23944 deletions

View File

@@ -1,8 +1,8 @@
Metadata-Version: 1.0
Name: SABnzbd
Version: 2.2.0Alpha1
Summary: SABnzbd-2.2.0Alpha1
Home-page: http://sabnzbd.org
Version: 2.2.0Alpha3
Summary: SABnzbd-2.2.0Alpha3
Home-page: https://sabnzbd.org
Author: The SABnzbd Team
Author-email: team@sabnzbd.org
License: GNU General Public License 2 (GPL2 or later)

View File

@@ -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

View File

@@ -1,24 +1,51 @@
Release Notes - SABnzbd 2.2.0 Alpha 1
Release Notes - SABnzbd 2.2.0 Alpha 3
=========================================================
NOTE: Due to changes in this release, the queue will be converted when 2.2.0
is started for the first time. Job order, settings and data will be preserved,
but URL's that did finished fetching before the upgrade will be lost!
is started for the first time. Job order, settings and data will be
preserved, but all jobs will be unpaused and URL's that did not finish
fetching before the upgrade will be lost!
## Changes in 2.2.0
## Changes since 2.1.0
- Direct Unpack: Jobs will start unpacking during the download, reduces
post-processing time but requires capable hard drive. Only works for jobs that
do not need repair. Will be enabled if your incomplete folder-speed > 60MB/s
- Reduced memory usage, especially with larger queues
- Slight improvement in download performance by removing internal locks
- Removed 5 second delay between fetching URLs
- Notifications can now be limited to certain Categories
- Each item in the Queue and Filelist now has Move to Top/Bottom buttons
- Smoother animations in Firefox (disabled previously due to FF high-CPU usage)
- If enabled, replace dots in filenames also when there are spaces already
- Jobs outside server retention are processed faster
- Show missing articles in MB instead of number of articles
- Obfuscated filenames are renamed during downloading, if possible
- If enable_par_cleanup is disabled all par2 files be downloaded
- If enabled, replace dots in filenames also when there are spaces already
- Update GNTP bindings to 1.0.3
- max_art_opt and replace_illegal moved from Switches to Specials
- Removed Specials enable_meta, par2_multicore and allow_streaming
- Windows: Full unicode support when calling repair and unpack
- Windows: Move enable_multipar to Specials
- Windows: Better indication of verification process before and after repair
- Windows: MultiPar verification of a job is skipped after blocks are fetched
- Windows & macOS: removed par2cmdline in favor of par2tbb/Multipar
## Bugfixes in 2.2.0
## Bugfixes since 2.1.0
- Shutdown/suspend did not work on some Linux systems
- Deleting a job could result in write errors
- Display warning if custom par2 parameters are wrong
- macOS: Catch 'Protocol wrong type for socket' errors
- RSS URLs with commas were broken
- Fixed some "Saving failed" errors
- Fixed crashing URLGrabber
- Jobs with renamed files are now correctly handled when using Retry
- Disk-space readings could be updated incorrectly
- Correct redirect after enabling HTTPS in the Config
- Fix race-condition in Post-processing
- History would not always show latest changes
- Convert HTML in error messages
- Fixed unicode error during Sorting
- Not all texts were shown in the selected Language
- Windows: Fix error in MultiPar-code when first par2-file was damaged
- macOS: Catch 'Protocol wrong type for socket' errors
## Translations
- Added Hebrew translation by ION IL, many other languages updated.

View File

@@ -428,9 +428,6 @@ def print_modules():
else:
logging.error(T('par2 binary... NOT found!'))
if sabnzbd.newsunpack.PAR2C_COMMAND:
logging.info("par2cmdline binary... found (%s)", sabnzbd.newsunpack.PAR2C_COMMAND)
if sabnzbd.newsunpack.MULTIPAR_COMMAND:
logging.info("MultiPar binary... found (%s)", sabnzbd.newsunpack.MULTIPAR_COMMAND)

View File

@@ -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
View 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
View 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
View 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
View 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"

View File

@@ -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
View 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
View 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'

View File

@@ -109,7 +109,7 @@
<tbody>
<tr>
<th scope="row">$T('homePage') </th>
<td><a href="http://sabnzbd.org/" target="_blank">http://sabnzbd.org/</a></td>
<td><a href="https://sabnzbd.org/" target="_blank">https://sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('menu-wiki') </th>
@@ -117,7 +117,7 @@
</tr>
<tr>
<th scope="row">$T('menu-forums') </th>
<td><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td>
<td><a href="https://forums.sabnzbd.org/" target="_blank">https://forums.sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('source') </th>
@@ -131,6 +131,10 @@
<th scope="row">$T('menu-issues') </th>
<td><a href="https://sabnzbd.org/wiki/introduction/known-issues" target="_blank">https://sabnzbd.org/wiki/introduction/known-issues</a></td>
</tr>
<tr>
<th scope="row">$T('menu-donate') </th>
<td><a href="https://sabnzbd.org/donate" target="_blank">https://sabnzbd.org/donate</a></td>
</tr>
</tbody>
</table>
</div>

View File

@@ -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">

View File

@@ -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

View File

@@ -130,6 +130,11 @@
<input type="checkbox" name="auto_sort" id="auto_sort" value="1" <!--#if int($auto_sort) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-auto_sort')</span>
</div>
<div class="field-pair">
<label class="config" for="direct_unpack">$T('opt-direct_unpack')</label>
<input type="checkbox" name="direct_unpack" id="direct_unpack" value="1" <!--#if int($direct_unpack) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-direct_unpack')</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
@@ -153,13 +158,6 @@
<input type="checkbox" name="enable_all_par" id="enable_all_par" value="1" <!--#if int($enable_all_par) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_all_par').replace('. ', '.<br/>')</span>
</div>
<!--#if $nt#-->
<div class="field-pair">
<label class="config" for="multipar">$T('opt-par2_multipar')</label>
<input type="checkbox" name="multipar" id="multipar" value="1" <!--#if int($multipar) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-par2_multipar')</span>
</div>
<!--#end if#-->
<div class="field-pair">
<label class="config" for="par_option">$T('opt-par_option')</label>
<input type="text" name="par_option" id="par_option" value="$par_option" />

View File

@@ -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;

View File

@@ -228,16 +228,13 @@ function do_restart() {
// What template
var arrPath = window.location.pathname.split('/');
var urlPath = (arrPath[1] == "m" || arrPath[2] == "m") ? '/sabnzbd/m/' : '/sabnzbd/';
var switchedHTTPS = !$('#enable_https').is(':checked') && window.location.protocol == 'https:'
var switchedHTTPS = ($('#enable_https').is(':checked') == ($('#enable_https').data('original') === undefined))
var portsUnchanged = ($('#port').val() == $('#port').data('original')) && ($('#https_port').val() == $('#https_port').data('original'))
// Are we on settings page?
if(!$('body').hasClass('General')) {
// Same as before, with fall-back in case location.origin is not supported (<IE9)
var urlTotal = window.location.origin ? (window.location.origin + urlPath) : window.location;
} else if (!switchedHTTPS && ($('#port').val() == $('#port').data('original')) && ($('#https_port').val() == $('#https_port').data('original'))) {
// If the http/https port config didn't change, don't try and guess the URL/port to redirect to
// This solves some incorrect behavior if running behind a reverse proxy
var urlTotal = window.location.origin ? (window.location.origin + urlPath) : window.location;
// Are we on settings page or did nothing change?
if(!$('body').hasClass('General') || (!switchedHTTPS && !portsUnchanged)) {
// Same as before
var urlTotal = window.location.origin + urlPath
} else {
// Protocol and port depend on http(s) setting
if($('#enable_https').is(':checked') && (window.location.protocol == 'https:' || !$('#https_port').val())) {
@@ -401,7 +398,7 @@ $(document).ready(function () {
$(checkDisabled).on('change', function() {
$(this).parent().nextAll().toggleClass('disabled')
})
if($(checkDisabled).is(':checked')) {
if(!$(checkDisabled).is(':checked')) {
$(checkDisabled).parent().nextAll().addClass('disabled')
}

View File

@@ -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>

View File

@@ -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#-->

View File

@@ -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&amp;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&amp;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>

View File

@@ -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>

View File

@@ -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(/&quot;/g,'"');
glitterTranslate.removeDown = "$T('Glitter-confirmClearDownloads')";
glitterTranslate.removeDow1 = "$T('Glitter-confirmClear1Download')";

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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" />-->

View File

Binary file not shown.

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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
View 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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 ""

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -15,27 +15,21 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: sabnzbd/skintext.py:67 [Config->Scheduler]
msgid "Pause low prioirty jobs"
msgstr "Pause low priority jobs"
#: sabnzbd/skintext.py:68 [Config->Scheduler]
msgid "Pause normal prioirty jobs"
msgstr "Pause normal priority jobs"
#: sabnzbd/skintext.py:69 [Config->Scheduler]
msgid "Pause high prioirty jobs"
msgstr "Pause high priority jobs"
#: sabnzbd/skintext.py:70 [Config->Scheduler]
msgid "Resume low prioirty jobs"
msgstr "Resume low priority jobs"
#: sabnzbd/skintext.py:71 [Config->Scheduler]
msgid "Resume normal prioirty jobs"
msgstr "Resume normal priority jobs"
#: sabnzbd/skintext.py:72 [Config->Scheduler]
msgid "Resume high prioirty jobs"
msgstr "Resume high priority jobs"
@@ -48,88 +42,66 @@ msgstr ""
"click the button below.<br /><br /><strong><a "
"href=\"..\">Refresh</a></strong><br />"
#: sabnzbd/skintext.py:251 [Retry all failed jobs in History]
msgid "Retry all failed"
msgstr "Retry All Failed"
#: sabnzbd/skintext.py:54 [#: Config->Scheduler]
msgid "disable server"
msgstr "Disable server:"
#: sabnzbd/skintext.py:55 [#: Config->Scheduler]
msgid "enable server"
msgstr "Enable server:"
#: sabnzbd/emailer.py:117
msgid "The server didn't reply properly to the helo greeting"
msgstr "The server didn't reply properly to the hello greeting"
#: sabnzbd/interface.py:226
msgid "API Key missing, please enter the api key from Config->General into your 3rd party program:"
msgstr "API key missing, please enter the API key from Config->General into your 3rd party program:"
#: sabnzbd/interface.py:233
msgid "API Key incorrect, Use the api key from Config->General in your 3rd party program:"
msgstr "API key incorrect, Use the API key from Config->General in your 3rd party program:"
#: sabnzbd/skintext.py:330
msgid "HTTPS Chain Certifcates"
msgstr "HTTPS Chain Certificates"
#: sabnzbd/skintext.py:465
msgid "Replace Spaces in Foldername"
msgstr "Replace spaces in folder name"
#: sabnzbd/skintext.py:467
msgid "Replace dots in Foldername"
msgstr "Replace dots in folder name"
#: sabnzbd/skintext.py:693
msgid "Original Foldername"
msgstr "Original folder name"
#: sabnzbd/skintext.py:808
msgid "How long or untill when do you want to pause? (in English!)"
msgstr "How long or until when do you want to pause? (in English!)"
#: sabnzbd/skintext.py:917
msgid "Timeleft"
msgstr "Time left"
#: sabnzbd/skintext.py:791 # sabnzbd/skintext.py:871
msgid "Optionally specify a filename"
msgstr "Optionally specify a name"
#: sabnzbd/skintext.py:819 # sabnzbd/skintext.py:880
msgid "Confirm Queue Deletions"
msgstr "Confirm queue deletions"
#: sabnzbd/skintext.py:820 # sabnzbd/skintext.py:881
msgid "Confirm History Deletions"
msgstr "Confirm history deletions"
#: sabnzbd/skintext.py:288 [Do not translate Pystone]
msgid "System Performance (Pystone)"
msgstr "System performance (Pystone)"
#: sabnzbd/skintext.py:317 # sabnzbd/skintext.py:779
msgid "Web Interface"
msgstr "Web interface"
#: sabnzbd/notifier.py
msgid "Script returned exit code %s and output \"%s\""
msgstr "Notification script returned exit code %s and output \"%s\""
#: sabnzbd/skintext.py:333
msgid "If empty, the standard port will only listen to HTTPS."
msgstr "If empty, the SABnzbd Port set above will listen to HTTPS."
#: sabnzbd/skintext.py:466
msgid "Posts will be paused untill they are at least this age. Setting job priority to Force will skip the delay."
msgstr "Posts will be paused until they are at least this age. Setting job priority to Force will skip the delay."
#: sabnzbd/skintext.py:841
msgid "How long or until when do you want to pause? (in English!)"
msgstr ""
msgid "Support the project, Donate!"
msgstr "Support the project, donate!"

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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 ""

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -7,33 +7,33 @@ msgid ""
msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2017-03-18 21:42+0000\n"
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
"PO-Revision-Date: 2017-05-06 09:07+0000\n"
"Last-Translator: ION IL <Unknown>\n"
"Language-Team: Hebrew <he@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2017-05-07 05:39+0000\n"
"X-Generator: Launchpad (build 18366)\n"
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
"X-Generator: Launchpad (build 18416)\n"
#: NSIS_Installer.nsi:473
#: NSIS_Installer.nsi
msgid "Show Release Notes"
msgstr "הראה הערות שחרור"
#: NSIS_Installer.nsi:475
#: NSIS_Installer.nsi
msgid "Start SABnzbd"
msgstr "התחל את SABnzbd"
#: NSIS_Installer.nsi:477
#: NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "תמוך במיזם, תרום!"
#: NSIS_Installer.nsi:479
#: NSIS_Installer.nsi
msgid "Please close \"SABnzbd.exe\" first"
msgstr "אנא סגור את \"SABnzbd.exe\" תחילה"
#: NSIS_Installer.nsi:481
#: NSIS_Installer.nsi
msgid ""
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
"run SABnzbd as a service, you need to update the service settings."
@@ -41,31 +41,31 @@ msgstr ""
"ספרית ההתקנה השתנתה (עכשיו ב-\"Program Files\"). \\nאם תריץ את SABnzbd בתור "
"שירות, אתה צריך לעדכן את הגדרות השירות."
#: NSIS_Installer.nsi:483
#: NSIS_Installer.nsi
msgid "This will uninstall SABnzbd from your system"
msgstr "זה יסיר את SABnzbd ממערכתך"
#: NSIS_Installer.nsi:485
#: NSIS_Installer.nsi
msgid "Run at startup"
msgstr "הפעל באתחול"
#: NSIS_Installer.nsi:487
#: NSIS_Installer.nsi
msgid "Desktop Icon"
msgstr "צלמית שולחן עבודה"
#: NSIS_Installer.nsi:489
#: NSIS_Installer.nsi
msgid "NZB File association"
msgstr "שיוך קבצי NZB"
#: NSIS_Installer.nsi:491
#: NSIS_Installer.nsi
msgid "Delete Program"
msgstr "מחק תכנית"
#: NSIS_Installer.nsi:493
#: NSIS_Installer.nsi
msgid "Delete Settings"
msgstr "מחק הגדרות"
#: NSIS_Installer.nsi:495
#: NSIS_Installer.nsi
msgid ""
"This system requires the Microsoft runtime library VC90 to be installed "
"first. Do you want to do that now?"
@@ -73,19 +73,19 @@ msgstr ""
"מערכת זו דורשת את ספרית זמן-אמת VC90 של Microsoft שתהיה מותקנת תחילה. האם "
"ברצונך להתקין אותה כעת?"
#: NSIS_Installer.nsi:497
#: NSIS_Installer.nsi
msgid "Downloading Microsoft runtime installer..."
msgstr "מוריד מתקין זמן-אמת של Microsoft..."
#: NSIS_Installer.nsi:499
#: NSIS_Installer.nsi
msgid "Download error, retry?"
msgstr "שגיאת הורדה, לנסות שוב?"
#: NSIS_Installer.nsi:501
#: NSIS_Installer.nsi
msgid "Cannot install without runtime library, retry?"
msgstr "לא ניתן להתקין ללא ספרית זמן-אמת, לנסות שוב?"
#: NSIS_Installer.nsi:503
#: NSIS_Installer.nsi
msgid ""
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
"the previous version or `Cancel` to cancel this upgrade."
@@ -93,6 +93,6 @@ msgstr ""
"אינך יכול לדרוס התקנה קיימת.\\n\\nלחץ על `אישור` כדי להסיר את הגרסה הקודמת "
"או על `ביטול` כדי לבטל שדרוג זה."
#: NSIS_Installer.nsi:505
#: NSIS_Installer.nsi
msgid "Your settings and data will be preserved."
msgstr "ההגדרות והנתונים שלך יישמרו."

View File

@@ -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å."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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 "Ваши параметры и данные будут сохранены."

View File

@@ -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 "Ваша подешавања и подаци биће сачувани."

View File

@@ -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."

View File

@@ -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 "您的设置及数据将会保留。"

View File

@@ -106,6 +106,7 @@ import sabnzbd.cfg as cfg
import sabnzbd.database
import sabnzbd.lang as lang
import sabnzbd.api
import sabnzbd.directunpacker as directunpacker
from sabnzbd.decorators import synchronized, notify_downloader
from sabnzbd.constants import NORMAL_PRIORITY, VALID_ARCHIVES, GIGI, \
REPAIR_REQUEST, QUEUE_FILE_NAME, QUEUE_VERSION, QUEUE_FILE_TMPL
@@ -157,7 +158,7 @@ WEBUI_READY = False
LAST_WARNING = None
LAST_ERROR = None
EXTERNAL_IPV6 = False
LAST_HISTORY_UPDATE = time.time()
LAST_HISTORY_UPDATE = 1
# Performance measure for dashboard
PYSTONE_SCORE = 0
@@ -383,6 +384,8 @@ def halt():
sabnzbd.zconfig.remove_server()
sabnzbd.directunpacker.abort_all()
rss.stop()
logging.debug('Stopping URLGrabber')
@@ -842,7 +845,7 @@ def keep_awake():
def CheckFreeSpace():
""" Check if enough disk space is free, if not pause downloader and send email """
if cfg.download_free() and not sabnzbd.downloader.Downloader.do.paused:
if misc.diskspace(cfg.download_dir.get_path(), force=True)[1] < cfg.download_free.get_float() / GIGI:
if misc.diskspace(force=True)['download_dir'][1] < cfg.download_free.get_float() / GIGI:
logging.warning(T('Too little diskspace forcing PAUSE'))
# Pause downloader, but don't save, since the disk is almost full!
Downloader.do.pause(save=False)
@@ -885,9 +888,9 @@ def save_data(data, _id, path, do_pickle=True, silent=False):
with open(path, 'wb') as data_file:
if do_pickle:
if cfg.use_pickle():
cPickle.dump(data, data_file)
else:
pickle.dump(data, data_file)
else:
cPickle.dump(data, data_file)
else:
data_file.write(data)
break
@@ -1180,3 +1183,11 @@ def test_ipv6():
except:
logging.debug('Test IPv6: Problem during IPv6 connect. Disabling IPv6. Reason: %s', sys.exc_info()[0])
return False
def history_updated():
""" To make sure we always have a fresh history """
sabnzbd.LAST_HISTORY_UPDATE += 1
# Never go over the limit
if sabnzbd.LAST_HISTORY_UPDATE+1 >= sys.maxint:
sabnzbd.LAST_HISTORY_UPDATE = 1

View File

@@ -484,13 +484,14 @@ def _api_history(name, output, kwargs):
value = kwargs.get('value', '')
start = int_conv(kwargs.get('start'))
limit = int_conv(kwargs.get('limit'))
last_history_update = int_conv(kwargs.get('last_history_update', 0))
search = kwargs.get('search')
failed_only = kwargs.get('failed_only')
categories = kwargs.get('category')
last_history_update = kwargs.get('last_history_update', 0)
# Do we need to send anything?
if int(last_history_update) == int(sabnzbd.LAST_HISTORY_UPDATE):
if last_history_update == sabnzbd.LAST_HISTORY_UPDATE:
return report(output, keyword='history', data=False)
if categories and not isinstance(categories, list):
@@ -510,15 +511,13 @@ def _api_history(name, output, kwargs):
history_db.remove_failed(search)
if special in ('all', 'completed'):
history_db.remove_completed(search)
# Update the last check time
sabnzbd.LAST_HISTORY_UPDATE = time.time()
sabnzbd.history_updated()
return report(output)
elif value:
jobs = value.split(',')
for job in jobs:
del_hist_job(job, del_files)
# Update the last check time
sabnzbd.LAST_HISTORY_UPDATE = time.time()
sabnzbd.history_updated()
return report(output)
else:
return report(output, _MSG_NO_VALUE)
@@ -532,7 +531,7 @@ def _api_history(name, output, kwargs):
search=search, failed_only=failed_only,
categories=categories,
output=output)
history['last_history_update'] = int(sabnzbd.LAST_HISTORY_UPDATE)
history['last_history_update'] = sabnzbd.LAST_HISTORY_UPDATE
history['version'] = sabnzbd.__version__
return report(output, keyword='history', data=remove_callable(history))
else:
@@ -1347,6 +1346,8 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
slot['sizeleft'] = format_bytes(bytesleft)
slot['percentage'] = "%s" % (int(((mb - mbleft) / mb) * 100)) if mb != mbleft else '0'
slot['missing'] = pnfo.missing
slot['mbmissing'] = "%.2f" % (pnfo.bytes_missing / MEBI)
slot['direct_unpack'] = pnfo.direct_unpack
if not output:
slot['mb_fmt'] = locale.format('%d', int(mb), True)
slot['mbdone_fmt'] = locale.format('%d', int(mb - mbleft), True)
@@ -1517,7 +1518,6 @@ def options_list(output):
return report(output, keyword='options', data={
'yenc': sabnzbd.decoder.HAVE_YENC,
'par2': sabnzbd.newsunpack.PAR2_COMMAND,
'par2c': sabnzbd.newsunpack.PAR2C_COMMAND,
'multipar': sabnzbd.newsunpack.MULTIPAR_COMMAND,
'rar': sabnzbd.newsunpack.RAR_COMMAND,
'zip': sabnzbd.newsunpack.ZIP_COMMAND,
@@ -1622,8 +1622,7 @@ def build_header(webdir='', output=None):
if speed_limit_abs <= 0:
speed_limit_abs = ''
disk_total1, disk_free1 = diskspace(cfg.download_dir.get_path())
disk_total2, disk_free2 = diskspace(cfg.complete_dir.get_path())
diskspace_info = diskspace()
header = {}
@@ -1660,12 +1659,12 @@ def build_header(webdir='', output=None):
header['pause_int'] = scheduler.pause_int()
header['paused_all'] = sabnzbd.PAUSED_ALL
header['diskspace1'] = "%.2f" % disk_free1
header['diskspace2'] = "%.2f" % disk_free2
header['diskspace1_norm'] = to_units(disk_free1 * GIGI)
header['diskspace2_norm'] = to_units(disk_free2 * GIGI)
header['diskspacetotal1'] = "%.2f" % disk_total1
header['diskspacetotal2'] = "%.2f" % disk_total2
header['diskspace1'] = "%.2f" % diskspace_info['download_dir'][1]
header['diskspace2'] = "%.2f" % diskspace_info['complete_dir'][1]
header['diskspace1_norm'] = to_units(diskspace_info['download_dir'][1] * GIGI)
header['diskspace2_norm'] = to_units(diskspace_info['complete_dir'][1] * GIGI)
header['diskspacetotal1'] = "%.2f" % diskspace_info['download_dir'][0]
header['diskspacetotal2'] = "%.2f" % diskspace_info['complete_dir'][0]
header['loadavg'] = loadavg()
header['speedlimit'] = "{1:0.{0}f}".format(int(speed_limit % 1 > 0), speed_limit)
header['speedlimit_abs'] = "%s" % speed_limit_abs

View File

@@ -30,8 +30,8 @@ import hashlib
import sabnzbd
from sabnzbd.misc import get_filepath, sanitize_filename, get_unique_filename, renamer, \
set_permissions, flag_file, long_path, clip_path, has_win_device, get_all_passwords
from sabnzbd.constants import QCHECK_FILE, Status
set_permissions, long_path, clip_path, has_win_device, get_all_passwords
from sabnzbd.constants import Status
import sabnzbd.cfg as cfg
from sabnzbd.articlecache import ArticleCache
from sabnzbd.postproc import PostProcessor
@@ -70,18 +70,16 @@ class Assembler(Thread):
if nzf:
sabnzbd.CheckFreeSpace()
# We allow win_devices because otherwise par2cmdline fails to repair
filename = sanitize_filename(nzf.filename, allow_win_devices=True)
filename = sanitize_filename(nzf.filename)
nzf.filename = filename
dupe = nzo.check_for_dupe(nzf)
filepath = get_filepath(long_path(cfg.download_dir.get_path()), nzo, filename)
if filepath:
logging.info('Decoding %s %s', filepath, nzf.type)
try:
filepath = _assemble(nzf, filepath, dupe)
filepath = self.assemble(nzf, filepath, dupe)
except IOError, (errno, strerror):
# If job was deleted, ignore error
if not nzo.is_gone():
@@ -100,7 +98,7 @@ class Assembler(Thread):
nzf.remove_admin()
setname = nzf.setname
if nzf.is_par2 and (nzo.md5packs.get(setname) is None):
pack = GetMD5Hashes(filepath)[0]
pack = self.parse_par2_file(filepath, nzo.md5of16k)
if pack:
nzo.md5packs[setname] = pack
logging.debug('Got md5pack for set %s', setname)
@@ -113,15 +111,15 @@ class Assembler(Thread):
rar_encrypted, unwanted_file = check_encrypted_and_unwanted_files(nzo, filepath)
if rar_encrypted:
if cfg.pause_on_pwrar() == 1:
logging.warning(T('WARNING: Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'), nzo.final_name)
logging.warning(remove_warning_label(T('WARNING: Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)')), nzo.final_name)
nzo.pause()
else:
logging.warning(T('WARNING: Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)'), nzo.final_name)
logging.warning(remove_warning_label(T('WARNING: Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)')), nzo.final_name)
nzo.fail_msg = T('Aborted, encryption detected')
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
if unwanted_file:
logging.warning(T('WARNING: In "%s" unwanted extension in RAR file. Unwanted file is %s '), nzo.final_name, unwanted_file)
logging.warning(remove_warning_label(T('WARNING: In "%s" unwanted extension in RAR file. Unwanted file is %s ')), nzo.final_name, unwanted_file)
logging.debug(T('Unwanted extension is in rar file %s'), filepath)
if cfg.action_on_unwanted_extensions() == 1 and nzo.unwanted_ext == 0:
logging.debug('Unwanted extension ... pausing')
@@ -134,54 +132,104 @@ class Assembler(Thread):
filter, reason = nzo_filtered_by_rating(nzo)
if filter == 1:
logging.warning(T('WARNING: Paused job "%s" because of rating (%s)'), nzo.final_name, reason)
logging.warning(remove_warning_label(T('WARNING: Paused job "%s" because of rating (%s)')), nzo.final_name, reason)
nzo.pause()
elif filter == 2:
logging.warning(T('WARNING: Aborted job "%s" because of rating (%s)'), nzo.final_name, reason)
logging.warning(remove_warning_label(T('WARNING: Aborted job "%s" because of rating (%s)')), nzo.final_name, reason)
nzo.fail_msg = T('Aborted, rating filter matched (%s)') % reason
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
if rarfile.is_rarfile(filepath):
nzo.add_to_direct_unpacker(nzf)
else:
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo.nzo_id, add_to_history=False, cleanup=False)
PostProcessor.do.process(nzo)
def assemble(self, nzf, path, dupe):
""" Assemble a NZF from its table of articles """
if os.path.exists(path):
unique_path = get_unique_filename(path)
if dupe:
path = unique_path
else:
renamer(path, unique_path)
def _assemble(nzf, path, dupe):
if os.path.exists(path):
unique_path = get_unique_filename(path)
if dupe:
path = unique_path
else:
renamer(path, unique_path)
md5 = hashlib.md5()
fout = open(path, 'ab')
decodetable = nzf.decodetable
md5 = hashlib.md5()
fout = open(path, 'ab')
decodetable = nzf.decodetable
for articlenum in decodetable:
# Break if deleted during writing
if nzf.nzo.status is Status.DELETED:
break
for articlenum in decodetable:
# Break if deleted during writing
if nzf.nzo.status is Status.DELETED:
break
# Sleep to allow decoder/assembler switching
sleep(0.0001)
article = decodetable[articlenum]
# Sleep to allow decoder/assembler switching
sleep(0.0001)
article = decodetable[articlenum]
data = ArticleCache.do.load_article(article)
data = ArticleCache.do.load_article(article)
if not data:
logging.info(T('%s missing'), article)
else:
# yenc data already decoded, flush it out
fout.write(data)
md5.update(data)
if not data:
logging.info(T('%s missing'), article)
else:
# yenc data already decoded, flush it out
fout.write(data)
md5.update(data)
fout.flush()
fout.close()
set_permissions(path)
nzf.md5sum = md5.digest()
del md5
fout.flush()
fout.close()
set_permissions(path)
nzf.md5sum = md5.digest()
del md5
return path
return path
def parse_par2_file(self, fname, table16k):
""" Get the hash table and the first-16k hash table from a PAR2 file
Return as dictionary, indexed on names or hashes for the first-16 table
For a full description of the par2 specification, visit:
http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html
"""
table = {}
duplicates16k = []
try:
f = open(fname, 'rb')
except:
return table
try:
header = f.read(8)
while header:
name, hash, hash16k = parse_par2_file_packet(f, header)
if name:
table[name] = hash
if hash16k not in table16k:
table16k[hash16k] = name
else:
# Not unique, remove to avoid false-renames
duplicates16k.append(hash16k)
header = f.read(8)
except (struct.error, IndexError):
logging.info('Cannot use corrupt par2 file for QuickCheck, "%s"', fname)
table = {}
except:
logging.debug('QuickCheck parser crashed in file %s', fname)
logging.info('Traceback: ', exc_info=True)
table = {}
f.close()
# Have to remove duplicates at the end to make sure
# no trace is left in case of multi-duplicates
for hash16k in duplicates16k:
if hash16k in table16k:
old_name = table16k.pop(hash16k)
logging.debug('Par2-16k signature of %s not unique, discarding', old_name)
return table
def file_has_articles(nzf):
@@ -199,47 +247,10 @@ def file_has_articles(nzf):
return has
# For a full description of the par2 specification, visit:
# http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html
def GetMD5Hashes(fname, force=False):
""" Get the hash table from a PAR2 file
Return as dictionary, indexed on names and True for utf8-encoded names
"""
new_encoding = True
table = {}
if force or not flag_file(os.path.split(fname)[0], QCHECK_FILE):
try:
f = open(fname, 'rb')
except:
return table, new_encoding
new_encoding = False
try:
header = f.read(8)
while header:
name, hash = ParseFilePacket(f, header)
new_encoding |= is_utf8(name)
if name:
table[name] = hash
header = f.read(8)
except (struct.error, IndexError):
logging.info('Cannot use corrupt par2 file for QuickCheck, "%s"', fname)
table = {}
except:
logging.debug('QuickCheck parser crashed in file %s', fname)
logging.info('Traceback: ', exc_info=True)
table = {}
f.close()
return table, new_encoding
def ParseFilePacket(f, header):
def parse_par2_file_packet(f, header):
""" Look up and analyze a FileDesc package """
nothing = None, None
nothing = None, None, None
if header != 'PAR2\0PKT':
return nothing
@@ -271,8 +282,9 @@ def ParseFilePacket(f, header):
for offset in range(0, len, 8):
if data[offset:offset + 16] == "PAR 2.0\0FileDesc":
hash = data[offset + 32:offset + 48]
hash16k = data[offset + 48:offset + 64]
filename = data[offset + 72:].strip('\0')
return filename, hash
return filename, hash, hash16k
return nothing
@@ -429,3 +441,11 @@ def rating_filtered(rating, filename, abort):
if any(check_keyword(k) for k in keywords.split(',')):
return T('keywords')
return None
def remove_warning_label(msg):
""" Standardize errors by removing obsolete
"WARNING:" part in all languages """
if ':' in msg:
return msg.split(':')[1]
return msg

View File

@@ -74,6 +74,7 @@ email_endjob = OptionNumber('misc', 'email_endjob', 0, 0, 2)
email_full = OptionBool('misc', 'email_full', False)
email_dir = OptionDir('misc', 'email_dir', create=True)
email_rss = OptionBool('misc', 'email_rss', False)
email_cats = OptionList('misc', 'email_cats', ['*'])
version_check = OptionNumber('misc', 'check_new_rel', 1)
autobrowser = OptionBool('misc', 'auto_browser', True)
@@ -101,9 +102,7 @@ par_option = OptionStr('misc', 'par_option', '', validation=no_nonsense)
nice = OptionStr('misc', 'nice', '', validation=no_nonsense)
ionice = OptionStr('misc', 'ionice', '', validation=no_nonsense)
ignore_wrong_unrar = OptionBool('misc', 'ignore_wrong_unrar', False)
par2_multicore = OptionBool('misc', 'par2_multicore', True)
multipar = OptionBool('misc', 'multipar', sabnzbd.WIN32)
allow_streaming = OptionBool('misc', 'allow_streaming', False)
pre_check = OptionBool('misc', 'pre_check', False)
fail_hopeless_jobs = OptionBool('misc', 'fail_hopeless_jobs', True)
req_completion_rate = OptionNumber('misc', 'req_completion_rate', 100.2, 100, 200)
@@ -142,11 +141,12 @@ backup_for_duplicates = OptionBool('misc', 'backup_for_duplicates', True)
ignore_samples = OptionBool('misc', 'ignore_samples', False)
auto_sort = OptionBool('misc', 'auto_sort', False)
direct_unpack = OptionBool('misc', 'direct_unpack', False)
direct_unpack_tested = OptionBool('misc', 'direct_unpack_tested', False)
propagation_delay = OptionNumber('misc', 'propagation_delay', 0)
folder_rename = OptionBool('misc', 'folder_rename', True)
folder_max_length = OptionNumber('misc', 'folder_max_length', DEF_FOLDER_MAX, 20, 65000)
pause_on_pwrar = OptionNumber('misc', 'pause_on_pwrar', 1)
enable_meta = OptionBool('misc', 'enable_meta', True)
safe_postproc = OptionBool('misc', 'safe_postproc', True)
empty_postproc = OptionBool('misc', 'empty_postproc', False)
@@ -172,7 +172,7 @@ movie_categories = OptionList('misc', 'movie_categories', ['movies'])
enable_date_sorting = OptionBool('misc', 'enable_date_sorting', False)
date_sort_string = OptionStr('misc', 'date_sort_string')
date_categories = OptionStr('misc', 'date_categories', ['tv'])
date_categories = OptionList('misc', 'date_categories', ['tv'])
configlock = OptionBool('misc', 'config_lock', 0)
@@ -245,6 +245,7 @@ fixed_ports = OptionBool('misc', 'fixed_ports', False)
# [ncenter]
ncenter_enable = OptionBool('ncenter', 'ncenter_enable', sabnzbd.DARWIN)
ncenter_cats = OptionList('ncenter', 'ncenter_cats', ['*'])
ncenter_prio_startup = OptionBool('ncenter', 'ncenter_prio_startup', True)
ncenter_prio_download = OptionBool('ncenter', 'ncenter_prio_download', False)
ncenter_prio_pp = OptionBool('ncenter', 'ncenter_prio_pp', False)
@@ -259,6 +260,7 @@ ncenter_prio_other = OptionBool('ncenter', 'ncenter_prio_other', False)
# [acenter]
acenter_enable = OptionBool('acenter', 'acenter_enable', sabnzbd.WIN32)
acenter_cats = OptionList('acenter', 'acenter_cats', ['*'])
acenter_prio_startup = OptionBool('acenter', 'acenter_prio_startup', False)
acenter_prio_download = OptionBool('acenter', 'acenter_prio_download', False)
acenter_prio_pp = OptionBool('acenter', 'acenter_prio_pp', False)
@@ -273,6 +275,7 @@ acenter_prio_other = OptionBool('acenter', 'acenter_prio_other', False)
# [ntfosd]
ntfosd_enable = OptionBool('ntfosd', 'ntfosd_enable', not sabnzbd.WIN32 and not sabnzbd.DARWIN)
ntfosd_cats = OptionList('ntfosd', 'ntfosd_cats', ['*'])
ntfosd_prio_startup = OptionBool('ntfosd', 'ntfosd_prio_startup', True)
ntfosd_prio_download = OptionBool('ntfosd', 'ntfosd_prio_download', False)
ntfosd_prio_pp = OptionBool('ntfosd', 'ntfosd_prio_pp', False)
@@ -287,6 +290,7 @@ ntfosd_prio_other = OptionBool('ntfosd', 'ntfosd_prio_other', False)
# [growl]
growl_enable = OptionBool('growl', 'growl_enable', False)
growl_cats = OptionList('growl', 'growl_cats', ['*'])
growl_server = OptionStr('growl', 'growl_server')
growl_password = OptionPassword('growl', 'growl_password')
growl_prio_startup = OptionBool('growl', 'growl_prio_startup', True)
@@ -303,6 +307,7 @@ growl_prio_other = OptionBool('growl', 'growl_prio_other', False)
# [prowl]
prowl_enable = OptionBool('prowl', 'prowl_enable', False)
prowl_cats = OptionList('prowl', 'prowl_cats', ['*'])
prowl_apikey = OptionStr('prowl', 'prowl_apikey')
prowl_prio_startup = OptionNumber('prowl', 'prowl_prio_startup', -3)
prowl_prio_download = OptionNumber('prowl', 'prowl_prio_download', -3)
@@ -321,6 +326,7 @@ pushover_token = OptionStr('pushover', 'pushover_token')
pushover_userkey = OptionStr('pushover', 'pushover_userkey')
pushover_device = OptionStr('pushover', 'pushover_device')
pushover_enable = OptionBool('pushover', 'pushover_enable')
pushover_cats = OptionList('pushover', 'pushover_cats', ['*'])
pushover_prio_startup = OptionNumber('pushover', 'pushover_prio_startup', -3)
pushover_prio_download = OptionNumber('pushover', 'pushover_prio_download', -2)
pushover_prio_pp = OptionNumber('pushover', 'pushover_prio_pp', -3)
@@ -335,6 +341,7 @@ pushover_prio_other = OptionNumber('pushover', 'pushover_prio_other', -3)
# [pushbullet]
pushbullet_enable = OptionBool('pushbullet', 'pushbullet_enable')
pushbullet_cats = OptionList('pushbullet', 'pushbullet_cats', ['*'])
pushbullet_apikey = OptionStr('pushbullet', 'pushbullet_apikey')
pushbullet_device = OptionStr('pushbullet', 'pushbullet_device')
pushbullet_prio_startup = OptionNumber('pushbullet', 'pushbullet_prio_startup', 0)
@@ -351,6 +358,7 @@ pushbullet_prio_other = OptionNumber('pushbullet', 'pushbullet_prio_other', 0)
# [nscript]
nscript_enable = OptionBool('nscript', 'nscript_enable')
nscript_cats = OptionList('nscript', 'nscript_cats', ['*'])
nscript_script = OptionStr('nscript', 'nscript_script')
nscript_parameters = OptionStr('nscript', 'nscript_parameters')
nscript_prio_startup = OptionBool('nscript', 'nscript_prio_startup', True)

View File

@@ -24,6 +24,10 @@ import re
import logging
import threading
import shutil
import time
import random
from hashlib import md5
from urlparse import urlparse
import sabnzbd.misc
from sabnzbd.constants import CONFIG_VERSION, NORMAL_PRIORITY, DEFAULT_PRIORITY, MAX_WIN_DFOLDER
from sabnzbd.utils import configobj
@@ -765,9 +769,6 @@ def _read_config(path, try_backup=False):
CFG['__encoding__'] = u'utf-8'
CFG['__version__'] = unicode(CONFIG_VERSION)
if 'misc' in CFG:
compatibility_fix(CFG['misc'])
# Use CFG data to set values for all static options
for section in database:
if section not in ('servers', 'categories', 'rss'):
@@ -977,6 +978,21 @@ def define_rss():
def get_rss():
global database
try:
# We have to remove non-seperator commas by detecting if they are valid URL's
for feed_key in database['rss']:
feed = database['rss'][feed_key]
# Create a new corrected list
new_feed_uris = []
for feed_uri in feed.uri():
if new_feed_uris and not urlparse(feed_uri).scheme and urlparse(new_feed_uris[-1]).scheme:
# Current one has no scheme but previous one does, append to previous
new_feed_uris[-1] += '%2C' + feed_uri
continue
# Add full working URL
new_feed_uris.append(feed_uri)
# Set new list
feed.uri.set(new_feed_uris)
return database['rss']
except KeyError:
return {}
@@ -1074,12 +1090,6 @@ def validate_notempty(root, value, default):
def create_api_key():
""" Return a new randomized API_KEY """
import time
try:
from hashlib import md5
except ImportError:
from md5 import md5
import random
# Create some values to seed md5
t = str(time.time())
r = str(random.random())
@@ -1090,22 +1100,3 @@ def create_api_key():
# Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b
return m.hexdigest()
_FIXES = (
('enable_par_multicore', 'par2_multicore'),
)
def compatibility_fix(cf):
""" Convert obsolete INI entries """
for item in _FIXES:
old, new = item
try:
cf[new]
except KeyError:
try:
cf[new] = cf[old]
del cf[old]
except KeyError:
pass

View File

@@ -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'

View File

@@ -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 """

View File

@@ -22,6 +22,7 @@ sabnzbd.decoder - article decoder
import binascii
import logging
import re
import hashlib
from time import sleep
from threading import Thread
@@ -30,8 +31,8 @@ from sabnzbd.constants import Status, MAX_DECODE_QUEUE, LIMIT_DECODE_QUEUE, SABY
import sabnzbd.articlecache
import sabnzbd.downloader
import sabnzbd.nzbqueue
from sabnzbd.encoding import yenc_name_fixer
from sabnzbd.misc import match_str
from sabnzbd.encoding import yenc_name_fixer, platform_encode
from sabnzbd.misc import match_str, is_obfuscated_filename
# Check for basic-yEnc
try:
@@ -68,6 +69,9 @@ class BadYenc(Exception):
Exception.__init__(self)
YDEC_TRANS = ''.join([chr((i + 256 - 42) % 256) for i in xrange(256)])
class Decoder(Thread):
def __init__(self, servers, queue):
@@ -113,7 +117,7 @@ class Decoder(Thread):
register = True
logging.debug("Decoding %s", art_id)
data = decode(article, lines, raw_data)
data = self.decode(article, lines, raw_data)
nzf.article_count += 1
found = True
@@ -176,7 +180,7 @@ class Decoder(Thread):
logging.info(logme)
if not found or killed:
new_server_found = self.__search_new_server(article)
new_server_found = self.search_new_server(article)
if new_server_found:
register = False
logme = None
@@ -185,7 +189,7 @@ class Decoder(Thread):
logme = T('Unknown Error while decoding %s') % art_id
logging.info(logme)
logging.info("Traceback: ", exc_info=True)
new_server_found = self.__search_new_server(article)
new_server_found = self.search_new_server(article)
if new_server_found:
register = False
logme = None
@@ -197,7 +201,7 @@ class Decoder(Thread):
nzo.inc_log('bad_art_log', art_id)
else:
new_server_found = self.__search_new_server(article)
new_server_found = self.search_new_server(article)
if new_server_found:
register = False
elif nzo.precheck:
@@ -209,7 +213,100 @@ class Decoder(Thread):
if register:
sabnzbd.nzbqueue.NzbQueue.do.register_article(article, found)
def __search_new_server(self, article):
def decode(self, article, data, raw_data):
# Do we have SABYenc? Let it do all the work
if sabnzbd.decoder.SABYENC_ENABLED:
decoded_data, output_filename, crc, crc_expected, crc_correct = sabyenc.decode_usenet_chunks(raw_data, article.bytes)
# Assume it is yenc
article.nzf.type = 'yenc'
# Only set the name if it was found and not obfuscated
self.verify_filename(article, decoded_data, output_filename)
# CRC check
if not crc_correct:
raise CrcError(crc_expected, crc, decoded_data)
return decoded_data
# Continue for _yenc or Python-yEnc
# Filter out empty ones
data = filter(None, data)
# No point in continuing if we don't have any data left
if data:
nzf = article.nzf
yenc, data = yCheck(data)
ybegin, ypart, yend = yenc
decoded_data = None
# Deal with non-yencoded posts
if not ybegin:
found = False
try:
for i in xrange(min(40, len(data))):
if data[i].startswith('begin '):
nzf.type = 'uu'
found = True
# Pause the job and show warning
if nzf.nzo.status != Status.PAUSED:
nzf.nzo.pause()
msg = T('UUencode detected, only yEnc encoding is supported [%s]') % nzf.nzo.final_name
logging.warning(msg)
break
except IndexError:
raise BadYenc()
if found:
decoded_data = ''
else:
raise BadYenc()
# Deal with yenc encoded posts
elif ybegin and yend:
if 'name' in ybegin:
output_filename = yenc_name_fixer(ybegin['name'])
else:
output_filename = None
logging.debug("Possible corrupt header detected => ybegin: %s", ybegin)
nzf.type = 'yenc'
# Decode data
if HAVE_YENC:
decoded_data, crc = _yenc.decode_string(''.join(data))[:2]
partcrc = '%08X' % ((crc ^ -1) & 2 ** 32L - 1)
else:
data = ''.join(data)
for i in (0, 9, 10, 13, 27, 32, 46, 61):
j = '=%c' % (i + 64)
data = data.replace(j, chr(i))
decoded_data = data.translate(YDEC_TRANS)
crc = binascii.crc32(decoded_data)
partcrc = '%08X' % (crc & 2 ** 32L - 1)
if ypart:
crcname = 'pcrc32'
else:
crcname = 'crc32'
if crcname in yend:
_partcrc = yenc_name_fixer('0' * (8 - len(yend[crcname])) + yend[crcname].upper())
else:
_partcrc = None
logging.debug("Corrupt header detected => yend: %s", yend)
if not _partcrc == partcrc:
raise CrcError(_partcrc, partcrc, decoded_data)
else:
raise BadYenc()
# Parse filename if there was data
if decoded_data:
# Only set the name if it was found and not obfuscated
self.verify_filename(article, decoded_data, output_filename)
return decoded_data
def search_new_server(self, article):
# Search new server
article.add_to_try_list(article.fetcher)
for server in self.servers:
@@ -222,99 +319,43 @@ class Decoder(Thread):
return True
msg = T('%s => missing from all servers, discarding') % article
logging.debug(msg)
logging.info(msg)
article.nzf.nzo.inc_log('missing_art_log', msg)
return False
YDEC_TRANS = ''.join([chr((i + 256 - 42) % 256) for i in xrange(256)])
def decode(article, data, raw_data):
# Do we have SABYenc? Let it do all the work
if sabnzbd.decoder.SABYENC_ENABLED:
decoded_data, output_filename, crc, crc_expected, crc_correct = sabyenc.decode_usenet_chunks(raw_data, article.bytes)
# Assume it is yenc
article.nzf.type = 'yenc'
# Only set the name if it was found
if output_filename:
article.nzf.filename = output_filename
# CRC check
if not crc_correct:
raise CrcError(crc_expected, crc, decoded_data)
return decoded_data
# Continue for _yenc or Python-yEnc
# Filter out empty ones
data = filter(None, data)
# No point in continuing if we don't have any data left
if data:
def verify_filename(self, article, decoded_data, yenc_filename):
""" Verify the filename provided by yenc by using
par2 information and otherwise fall back to NZB name
"""
nzf = article.nzf
yenc, data = yCheck(data)
ybegin, ypart, yend = yenc
decoded_data = None
# Was this file already verified and did we get a name?
if nzf.filename_checked or not yenc_filename:
return
# Deal with non-yencoded posts
if not ybegin:
found = False
try:
for i in xrange(min(40, len(data))):
if data[i].startswith('begin '):
nzf.type = 'uu'
found = True
# Pause the job and show warning
if nzf.nzo.status != Status.PAUSED:
nzf.nzo.pause()
msg = T('UUencode detected, only yEnc encoding is supported [%s]') % nzf.nzo.final_name
logging.warning(msg)
break
except IndexError:
raise BadYenc()
# Set the md5-of-16k if this is the first article
if article.partnum == nzf.lowest_partnum:
nzf.md5of16k = hashlib.md5(decoded_data[:16384]).digest()
if found:
decoded_data = ''
else:
raise BadYenc()
# If we have the md5, use it to rename
if nzf.md5of16k:
# Don't check again, even if no match
nzf.filename_checked = True
# Find the match and rename
if nzf.md5of16k in nzf.nzo.md5of16k:
new_filename = platform_encode(nzf.nzo.md5of16k[nzf.md5of16k])
# Was it even new?
if new_filename != nzf.filename:
logging.info('Detected filename based on par2: %s -> %s', nzf.filename, new_filename)
nzf.nzo.renamed_file(new_filename, nzf.filename)
nzf.filename = new_filename
return
# Deal with yenc encoded posts
elif ybegin and yend:
if 'name' in ybegin:
nzf.filename = yenc_name_fixer(ybegin['name'])
else:
logging.debug("Possible corrupt header detected => ybegin: %s", ybegin)
nzf.type = 'yenc'
# Decode data
if HAVE_YENC:
decoded_data, crc = _yenc.decode_string(''.join(data))[:2]
partcrc = '%08X' % ((crc ^ -1) & 2 ** 32L - 1)
else:
data = ''.join(data)
for i in (0, 9, 10, 13, 27, 32, 46, 61):
j = '=%c' % (i + 64)
data = data.replace(j, chr(i))
decoded_data = data.translate(YDEC_TRANS)
crc = binascii.crc32(decoded_data)
partcrc = '%08X' % (crc & 2 ** 32L - 1)
if ypart:
crcname = 'pcrc32'
else:
crcname = 'crc32'
if crcname in yend:
_partcrc = yenc_name_fixer('0' * (8 - len(yend[crcname])) + yend[crcname].upper())
else:
_partcrc = None
logging.debug("Corrupt header detected => yend: %s", yend)
if not _partcrc == partcrc:
raise CrcError(_partcrc, partcrc, decoded_data)
else:
raise BadYenc()
return decoded_data
# Fallback to yenc/nzb name (also when there is no partnum=1)
# We also keep the NZB name in case it ends with ".par2" (usually correct)
if yenc_filename != nzf.filename and not is_obfuscated_filename(yenc_filename) and not nzf.filename.endswith('.par2'):
logging.info('Detected filename from yenc: %s -> %s', nzf.filename, yenc_filename)
nzf.nzo.renamed_file(yenc_filename, nzf.filename)
nzf.filename = yenc_filename
def yCheck(data):

379
sabnzbd/directunpacker.py Normal file
View 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)

View File

@@ -353,7 +353,6 @@ class Downloader(Thread):
""" Return True when this server has the highest priority of the active ones
0 is the highest priority
"""
for server in self.servers:
if server is not me and server.active and server.priority < me.priority:
return False
@@ -466,7 +465,7 @@ class Downloader(Thread):
if server.retention and article.nzf.nzo.avg_stamp < time.time() - server.retention:
# Let's get rid of all the articles for this server at once
logging.debug('Job %s too old for %s, moving on', article.nzf.nzo.work_name, server.id)
logging.info('Job %s too old for %s, moving on', article.nzf.nzo.work_name, server.id)
while article:
self.decode(article, None, None)
article = article.nzf.nzo.get_article(server, self.servers)
@@ -709,7 +708,7 @@ class Downloader(Thread):
elif nw.status_code in ('411', '423', '430'):
done = True
logging.info('Thread %s@%s: Article %s missing (error=%s)',
logging.debug('Thread %s@%s: Article %s missing (error=%s)',
nw.thrdnum, nw.server.id, article.article, nw.status_code)
nw.clear_data()

View File

@@ -29,6 +29,7 @@ from sabnzbd.constants import *
import sabnzbd
from sabnzbd.misc import to_units, split_host, time_format
from sabnzbd.encoding import EmailFilter
from sabnzbd.notifier import check_cat
import sabnzbd.cfg as cfg
@@ -216,6 +217,9 @@ def send_with_template(prefix, parm, test=None):
def endjob(filename, cat, status, path, bytes, fail_msg, stages, script, script_output, script_ret, test=None):
""" Send end-of-job email """
# Is it allowed?
if not check_cat('email', cat):
return None
# Translate the stage names
tr = sabnzbd.api.Ttemplate

View File

@@ -1216,6 +1216,7 @@ def orphan_delete(kwargs):
if path:
path = platform_encode(path)
path = os.path.join(long_path(cfg.download_dir.get_path()), path)
logging.info('Removing orphaned job %s', path)
remove_all(path, recursive=True)
def orphan_delete_all():
@@ -1229,6 +1230,7 @@ def orphan_add(kwargs):
if path:
path = platform_encode(path)
path = os.path.join(long_path(cfg.download_dir.get_path()), path)
logging.info('Re-adding orphaned job %s', path)
NzbQueue.do.repair_job(path, None, None)
def orphan_add_all():
@@ -1298,7 +1300,7 @@ class ConfigFolders(object):
##############################################################################
SWITCH_LIST = \
('par2_multicore', 'multipar', 'par_option', 'top_only', 'ssl_ciphers',
('par_option', 'top_only', 'ssl_ciphers', 'direct_unpack',
'auto_sort', 'propagation_delay', 'auto_disconnect', 'flat_unpack',
'safe_postproc', 'no_dupes', 'replace_spaces', 'replace_dots',
'ignore_samples', 'pause_on_post_processing', 'nice', 'ionice',
@@ -1374,11 +1376,11 @@ class ConfigSwitches(object):
SPECIAL_BOOL_LIST = \
('start_paused', 'no_penalties', 'ignore_wrong_unrar', 'overwrite_files', 'enable_par_cleanup',
'queue_complete_pers', 'api_warnings', 'ampm', 'enable_unrar', 'enable_unzip', 'enable_7zip',
'enable_filejoin', 'enable_tsjoin', 'allow_streaming', 'ignore_unrar_dates', 'par2_multicore',
'osx_menu', 'osx_speed', 'win_menu', 'use_pickle', 'allow_incomplete_nzb', 'rss_filenames',
'ipv6_hosting', 'keep_awake', 'empty_postproc', 'html_login', 'wait_for_dfolder', 'max_art_opt',
'warn_empty_nzb', 'enable_bonjour','allow_duplicate_files', 'warn_dupl_jobs', 'replace_illegal',
'backup_for_duplicates', 'disable_api_key', 'api_logging', 'enable_meta',
'enable_filejoin', 'enable_tsjoin', 'ignore_unrar_dates',
'multipar', 'osx_menu', 'osx_speed', 'win_menu', 'use_pickle', 'allow_incomplete_nzb',
'rss_filenames', 'ipv6_hosting', 'keep_awake', 'empty_postproc', 'html_login', 'wait_for_dfolder',
'max_art_opt', 'warn_empty_nzb', 'enable_bonjour','allow_duplicate_files', 'warn_dupl_jobs',
'replace_illegal', 'backup_for_duplicates', 'disable_api_key', 'api_logging',
)
SPECIAL_VALUE_LIST = \
('size_limit', 'folder_max_length', 'fsys_type', 'movie_rename_limit', 'nomedia_marker',
@@ -2693,39 +2695,39 @@ def GetRssLog(feed):
##############################################################################
LIST_EMAIL = (
'email_endjob', 'email_full',
'email_endjob', 'email_cats', 'email_full',
'email_server', 'email_to', 'email_from',
'email_account', 'email_pwd', 'email_dir', 'email_rss'
)
LIST_GROWL = ('growl_enable', 'growl_server', 'growl_password',
LIST_GROWL = ('growl_enable', 'growl_cats', 'growl_server', 'growl_password',
'growl_prio_startup', 'growl_prio_download', 'growl_prio_pp', 'growl_prio_complete', 'growl_prio_failed',
'growl_prio_disk_full', 'growl_prio_warning', 'growl_prio_error', 'growl_prio_queue_done', 'growl_prio_other',
'growl_prio_new_login')
LIST_NCENTER = ('ncenter_enable',
LIST_NCENTER = ('ncenter_enable', 'ncenter_cats',
'ncenter_prio_startup', 'ncenter_prio_download', 'ncenter_prio_pp', 'ncenter_prio_complete', 'ncenter_prio_failed',
'ncenter_prio_disk_full', 'ncenter_prio_warning', 'ncenter_prio_error', 'ncenter_prio_queue_done', 'ncenter_prio_other',
'ncenter_prio_new_login')
LIST_ACENTER = ('acenter_enable',
LIST_ACENTER = ('acenter_enable', 'acenter_cats',
'acenter_prio_startup', 'acenter_prio_download', 'acenter_prio_pp', 'acenter_prio_complete', 'acenter_prio_failed',
'acenter_prio_disk_full', 'acenter_prio_warning', 'acenter_prio_error', 'acenter_prio_queue_done', 'acenter_prio_other',
'acenter_prio_new_login')
LIST_NTFOSD = ('ntfosd_enable',
LIST_NTFOSD = ('ntfosd_enable', 'ntfosd_cats',
'ntfosd_prio_startup', 'ntfosd_prio_download', 'ntfosd_prio_pp', 'ntfosd_prio_complete', 'ntfosd_prio_failed',
'ntfosd_prio_disk_full', 'ntfosd_prio_warning', 'ntfosd_prio_error', 'ntfosd_prio_queue_done', 'ntfosd_prio_other',
'ntfosd_prio_new_login')
LIST_PROWL = ('prowl_enable', 'prowl_apikey',
LIST_PROWL = ('prowl_enable', 'prowl_cats', 'prowl_apikey',
'prowl_prio_startup', 'prowl_prio_download', 'prowl_prio_pp', 'prowl_prio_complete', 'prowl_prio_failed',
'prowl_prio_disk_full', 'prowl_prio_warning', 'prowl_prio_error', 'prowl_prio_queue_done', 'prowl_prio_other',
'prowl_prio_new_login')
LIST_PUSHOVER = ('pushover_enable', 'pushover_token', 'pushover_userkey', 'pushover_device',
LIST_PUSHOVER = ('pushover_enable', 'pushover_cats', 'pushover_token', 'pushover_userkey', 'pushover_device',
'pushover_prio_startup', 'pushover_prio_download', 'pushover_prio_pp', 'pushover_prio_complete', 'pushover_prio_failed',
'pushover_prio_disk_full', 'pushover_prio_warning', 'pushover_prio_error', 'pushover_prio_queue_done', 'pushover_prio_other',
'pushover_prio_new_login')
LIST_PUSHBULLET = ('pushbullet_enable', 'pushbullet_apikey', 'pushbullet_device',
LIST_PUSHBULLET = ('pushbullet_enable', 'pushbullet_cats', 'pushbullet_apikey', 'pushbullet_device',
'pushbullet_prio_startup', 'pushbullet_prio_download', 'pushbullet_prio_pp', 'pushbullet_prio_complete', 'pushbullet_prio_failed',
'pushbullet_prio_disk_full', 'pushbullet_prio_warning', 'pushbullet_prio_error', 'pushbullet_prio_queue_done', 'pushbullet_prio_other',
'pushbullet_prio_new_login')
LIST_NSCRIPT = ('nscript_enable', 'nscript_script', 'nscript_parameters',
LIST_NSCRIPT = ('nscript_enable', 'nscript_cats', 'nscript_script', 'nscript_parameters',
'nscript_prio_startup', 'nscript_prio_download', 'nscript_prio_pp', 'nscript_prio_complete', 'nscript_prio_failed',
'nscript_prio_disk_full', 'nscript_prio_warning', 'nscript_prio_error', 'nscript_prio_queue_done', 'nscript_prio_other',
'nscript_prio_new_login')
@@ -2747,6 +2749,7 @@ class ConfigNotify(object):
conf = build_header(sabnzbd.WEB_DIR_CONFIG)
conf['my_home'] = sabnzbd.DIR_HOME
conf['categories'] = list_cats(False)
conf['lastmail'] = self.__lastmail
conf['have_growl'] = True
conf['have_ntfosd'] = sabnzbd.notifier.have_ntfosd()
@@ -2857,7 +2860,7 @@ def rss_history(url, limit=50, search=None):
rss = RSS()
rss.channel.title = "SABnzbd History"
rss.channel.description = "Overview of completed downloads"
rss.channel.link = "http://sabnzbd.org/"
rss.channel.link = "https://sabnzbd.org/"
rss.channel.language = "en"
items, _fetched_items, _max_items = build_history(limit=limit, search=search)
@@ -2904,7 +2907,7 @@ def rss_warnings():
rss = RSS()
rss.channel.title = "SABnzbd Warnings"
rss.channel.description = "Overview of warnings/errors"
rss.channel.link = "http://sabnzbd.org/"
rss.channel.link = "https://sabnzbd.org/"
rss.channel.language = "en"
for warn in sabnzbd.GUIHANDLER.content():

View File

@@ -247,10 +247,11 @@ def replace_win_devices(name):
def has_win_device(p):
""" Return True if filename part contains forbidden name
Before and after sanitizing
"""
p = os.path.split(p)[1].lower()
for dev in _DEVICES:
if p == dev or p.startswith(dev + '.'):
if p == dev or p.startswith(dev + '.') or p.startswith('_' + dev + '.'):
return True
return False
@@ -264,7 +265,7 @@ else:
CH_LEGAL = '+'
def sanitize_filename(name, allow_win_devices=False):
def sanitize_filename(name):
""" Return filename with illegal chars converted to legal ones
and with the par2 extension always in lowercase
"""
@@ -281,7 +282,7 @@ def sanitize_filename(name, allow_win_devices=False):
# Compensate for the foolish way par2 on OSX handles a colon character
name = name[name.rfind(':') + 1:]
if sabnzbd.WIN32 and not allow_win_devices:
if sabnzbd.WIN32 or cfg.sanitize_safe():
name = replace_win_devices(name)
lst = []
@@ -397,20 +398,11 @@ def sanitize_files_in_folder(folder):
return lst
def flag_file(path, flag, create=False):
""" Create verify flag file or return True if it already exists """
path = os.path.join(path, JOB_ADMIN)
path = os.path.join(path, flag)
if create:
try:
f = open(path, 'w')
f.write('ok\n')
f.close()
return True
except IOError:
return False
else:
return os.path.exists(path)
def is_obfuscated_filename(filename):
""" Check if this file has an extension, if not, it's
probably obfuscated and we don't use it
"""
return (os.path.splitext(filename)[1] == '')
##############################################################################
@@ -911,8 +903,7 @@ def move_to_path(path, new_path):
new_path = get_unique_filename(new_path)
if new_path:
logging.debug("Moving. Old path: %s New path: %s Overwrite: %s",
path, new_path, overwrite)
logging.debug("Moving (overwrite: %s) %s => %s", overwrite, path, new_path)
try:
# First try cheap rename
renamer(path, new_path)
@@ -1183,24 +1174,40 @@ else:
return 20.0, 10.0
__LAST_DISK_RESULT = {}
__LAST_DISK_CALL = {}
def diskspace(_dir, force=False):
# Store all results to speed things up
__DIRS_CHECKED = []
__DISKS_SAME = None
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []}
__LAST_DISK_CALL = 0
def diskspace(force=False):
""" Wrapper to cache results """
if _dir not in __LAST_DISK_RESULT:
__LAST_DISK_RESULT[_dir] = [0.0, 0.0]
__LAST_DISK_CALL[_dir] = 0.0
global __DIRS_CHECKED, __DISKS_SAME, __LAST_DISK_RESULT, __LAST_DISK_CALL
# Reset everything when folders changed
dirs_to_check = [cfg.download_dir.get_path(), cfg.complete_dir.get_path()]
if __DIRS_CHECKED != dirs_to_check:
__DIRS_CHECKED = dirs_to_check
__DISKS_SAME = None
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []}
__LAST_DISK_CALL = 0
# When forced, ignore any cache to avoid problems in UI
if force:
return diskspace_base(_dir)
__LAST_DISK_CALL = 0
# Check against cache
if time.time() > __LAST_DISK_CALL[_dir] + 10.0:
__LAST_DISK_RESULT[_dir] = diskspace_base(_dir)
__LAST_DISK_CALL[_dir] = time.time()
if time.time() > __LAST_DISK_CALL + 10.0:
# Same disk? Then copy-paste
__LAST_DISK_RESULT['download_dir'] = diskspace_base(cfg.download_dir.get_path())
__LAST_DISK_RESULT['complete_dir'] = __LAST_DISK_RESULT['download_dir'] if __DISKS_SAME else diskspace_base(cfg.complete_dir.get_path())
__LAST_DISK_CALL = time.time()
return __LAST_DISK_RESULT[_dir]
# Do we know if it's same disk?
if __DISKS_SAME is None:
__DISKS_SAME = (__LAST_DISK_RESULT['download_dir'] == __LAST_DISK_RESULT['complete_dir'])
return __LAST_DISK_RESULT
##############################################################################
@@ -1365,6 +1372,7 @@ def renamer(old, new):
def remove_dir(path):
""" Remove directory with retries for Win32 """
logging.debug('Removing dir %s', path)
if sabnzbd.WIN32:
retries = 15
while retries > 0:
@@ -1393,6 +1401,7 @@ def remove_all(path, pattern='*', keep_folder=False, recursive=False):
for f in files:
if os.path.isfile(f):
try:
logging.debug('Removing file %s', f)
os.remove(f)
except:
logging.info('Cannot remove file %s', f)
@@ -1400,6 +1409,7 @@ def remove_all(path, pattern='*', keep_folder=False, recursive=False):
remove_all(f, pattern, False, True)
if not keep_folder:
try:
logging.debug('Removing dir %s', path)
os.rmdir(path)
except:
logging.info('Cannot remove folder %s', path)
@@ -1451,6 +1461,7 @@ def starts_with_path(path, prefix):
def set_chmod(path, permissions, report):
""" Set 'permissions' on 'path', report any errors when 'report' is True """
try:
logging.debug('Applying %s to %s', permissions, path)
os.chmod(path, permissions)
except:
lpath = path.lower()

View File

@@ -24,7 +24,7 @@ import sys
import re
import subprocess
import logging
from time import time
import time
import binascii
import shutil
@@ -32,11 +32,11 @@ import sabnzbd
from sabnzbd.encoding import TRANS, UNTRANS, unicoder, platform_encode, deunicode
import sabnzbd.utils.rarfile as rarfile
from sabnzbd.misc import format_time_string, find_on_path, make_script_path, int_conv, \
flag_file, real_path, globber, globber_full, get_all_passwords, renamer, clip_path, \
real_path, globber, globber_full, get_all_passwords, renamer, clip_path, \
has_win_device, calc_age
from sabnzbd.tvsort import SeriesSorter
import sabnzbd.cfg as cfg
from sabnzbd.constants import Status, QCHECK_FILE, RENAMES_FILE
from sabnzbd.constants import Status
if sabnzbd.WIN32:
try:
@@ -45,15 +45,18 @@ if sabnzbd.WIN32:
from win32process import STARTF_USESHOWWINDOW, IDLE_PRIORITY_CLASS
except ImportError:
pass
# Load the POpen from the fixed unicode-subprocess
from sabnzbd.utils.subprocess_fix import Popen
else:
# Define dummy WindowsError for non-Windows
class WindowsError(Exception):
def __init__(self, value):
self.parameter = value
def __str__(self):
return repr(self.parameter)
# Load the regular POpen
from subprocess import Popen
# Regex globals
RAR_RE = re.compile(r'\.(?P<ext>part\d*\.rar|rar|r\d\d|s\d\d|t\d\d|u\d\d|v\d\d|\d\d\d)$', re.I)
@@ -70,7 +73,6 @@ FULLVOLPAR2_RE = re.compile(r'(.*[^.])(\.*vol[0-9]+\+[0-9]+\.par2)', re.I)
TS_RE = re.compile(r'\.(\d+)\.(ts$)', re.I)
PAR2_COMMAND = None
PAR2C_COMMAND = None
MULTIPAR_COMMAND = None
RAR_COMMAND = None
NICE_COMMAND = None
@@ -92,7 +94,6 @@ def find_programs(curdir):
return None
if sabnzbd.DARWIN:
sabnzbd.newsunpack.PAR2C_COMMAND = check(curdir, 'osx/par2/par2-classic')
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'osx/par2/par2-sl64')
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'osx/unrar/unrar')
sabnzbd.newsunpack.SEVEN_COMMAND = check(curdir, 'osx/7zip/7za')
@@ -100,15 +101,13 @@ def find_programs(curdir):
if sabnzbd.WIN32:
if sabnzbd.WIN64:
# 64 bit versions
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'win/par2/x64/par2.exe')
sabnzbd.newsunpack.MULTIPAR_COMMAND = check(curdir, 'win/par2/multipar/par2j64.exe')
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'win/unrar/x64/UnRAR.exe')
else:
# 32 bit versions
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'win/par2/par2.exe')
sabnzbd.newsunpack.MULTIPAR_COMMAND = check(curdir, 'win/par2/multipar/par2j.exe')
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'win/unrar/UnRAR.exe')
sabnzbd.newsunpack.PAR2C_COMMAND = check(curdir, 'win/par2/par2cmdline.exe')
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'win/par2/par2.exe')
sabnzbd.newsunpack.ZIP_COMMAND = check(curdir, 'win/unzip/unzip.exe')
sabnzbd.newsunpack.SEVEN_COMMAND = check(curdir, 'win/7zip/7za.exe')
else:
@@ -125,9 +124,6 @@ def find_programs(curdir):
if not sabnzbd.newsunpack.SEVEN_COMMAND:
sabnzbd.newsunpack.SEVEN_COMMAND = find_on_path('7z')
if not sabnzbd.newsunpack.PAR2C_COMMAND:
sabnzbd.newsunpack.PAR2C_COMMAND = sabnzbd.newsunpack.PAR2_COMMAND
if not (sabnzbd.WIN32 or sabnzbd.DARWIN):
# Run check on rar version
version, original = unrar_check(sabnzbd.newsunpack.RAR_COMMAND)
@@ -166,7 +162,7 @@ def external_processing(extern_proc, nzo, complete_dir, nicename, status):
logging.info('Running external script %s(%s, %s, %s, %s, %s, %s, %s, %s)',
extern_proc, complete_dir, nzo.filename, nicename, '', nzo.cat, nzo.group, status, failure_url)
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, env=env, creationflags=creationflags)
@@ -207,7 +203,7 @@ def external_script(script, p1, p2, p3=None, p4=None):
stup, need_shell, command, creationflags = build_command(command)
env = create_env()
logging.info('Running user script %s(%s, %s)', script, p1, p2)
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, env=env, creationflags=creationflags)
except:
@@ -475,27 +471,45 @@ def rar_unpack(nzo, workdir, workdir_complete, delete, one_folder, rars):
else:
extraction_path = os.path.split(rarpath)[0]
logging.info("Extracting rarfile %s (belonging to %s) to %s",
rarpath, rar_set, extraction_path)
# Is the direct-unpacker still running? We wait for it
if nzo.direct_unpacker:
while nzo.direct_unpacker.is_alive():
logging.debug('DirectUnpacker still alive for %s', nzo)
time.sleep(2)
try:
fail, newfiles, rars = rar_extract(rarpath, len(rar_sets[rar_set]),
one_folder, nzo, rar_set, extraction_path)
# Was it aborted?
if not nzo.pp_active:
fail = 1
break
success = not fail
except:
success = False
fail = True
msg = sys.exc_info()[1]
nzo.fail_msg = T('Unpacking failed, %s') % msg
setname = nzo.final_name
nzo.set_unpack_info('Unpack', T('[%s] Error "%s" while unpacking RAR files') % (unicoder(setname), msg))
# Bump the file-lock in case it's stuck
with nzo.direct_unpacker.next_file_lock:
nzo.direct_unpacker.next_file_lock.notify()
logging.error(T('Error "%s" while running rar_unpack on %s'), msg, setname)
logging.debug("Traceback: ", exc_info=True)
# Did we already direct-unpack it? Not when recursive-unpacking
if nzo.direct_unpacker and rar_set in nzo.direct_unpacker.success_sets:
logging.info("Set %s completed by DirectUnpack", rar_set)
fail = 0
success = 1
rars = rar_sets[rar_set]
newfiles = globber(extraction_path)
nzo.direct_unpacker.success_sets.remove(rar_set)
else:
logging.info("Extracting rarfile %s (belonging to %s) to %s",
rarpath, rar_set, extraction_path)
try:
fail, newfiles, rars = rar_extract(rarpath, len(rar_sets[rar_set]),
one_folder, nzo, rar_set, extraction_path)
# Was it aborted?
if not nzo.pp_active:
fail = 1
break
success = not fail
except:
success = False
fail = True
msg = sys.exc_info()[1]
nzo.fail_msg = T('Unpacking failed, %s') % msg
setname = nzo.final_name
nzo.set_unpack_info('Unpack', T('[%s] Error "%s" while unpacking RAR files') % (unicoder(setname), msg))
logging.error(T('Error "%s" while running rar_unpack on %s'), msg, setname)
logging.debug("Traceback: ", exc_info=True)
if success:
logging.debug('rar_unpack(): Rars: %s', rars)
@@ -530,7 +544,6 @@ def rar_extract(rarfile_path, numrars, one_folder, nzo, setname, extraction_path
with password tries
Return fail==0(ok)/fail==1(error)/fail==2(wrong password), new_files, rars
"""
fail = 0
new_files = None
rars = []
@@ -555,7 +568,7 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
""" Unpack single rar set 'rarfile_path' to 'extraction_path'
Return fail==0(ok)/fail==1(error)/fail==2(wrong password)/fail==3(crc-error), new_files, rars
"""
start = time()
start = time.time()
logging.debug("rar_extract(): Extractionpath: %s", extraction_path)
@@ -579,13 +592,14 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
if sabnzbd.WIN32:
# Use all flags
# See: https://github.com/sabnzbd/sabnzbd/pull/771
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, rename, '-ai', password_command,
'%s' % clip_path(rarfile_path), '%s\\' % extraction_path]
if not has_win_device(rarfile_path):
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, rename, '-ai', password_command,
'%s' % clip_path(rarfile_path), clip_path(extraction_path)]
else:
# Need long-path notation in case of forbidden-names
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, rename, '-ai', password_command,
'%s' % clip_path(rarfile_path), '%s\\' % extraction_path]
# If this is the retry without leading \\.\, we need to remove the \ at the end (yes..)
if not extraction_path.startswith('\\\\?\\'):
command[-1] = command[-1][:-1]
elif RAR_PROBLEM:
# Use only oldest options (specifically no "-or")
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, password_command,
@@ -600,9 +614,12 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
stup, need_shell, command, creationflags = build_command(command)
# Get list of all the volumes part of this set
logging.debug("Analyzing rar file ... %s found", rarfile.is_rarfile(rarfile_path))
rarfiles = rarfile.RarFile(rarfile_path).volumelist()
logging.debug("Running unrar %s", command)
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, creationflags=creationflags)
@@ -615,7 +632,6 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
# Loop over the output from rar!
curr = 0
extracted = []
rarfiles = []
fail = 0
inrecovery = False
lines = []
@@ -638,9 +654,6 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
lines.append(line)
if line.startswith('Extracting from'):
filename = TRANS((re.search(EXTRACTFROM_RE, line).group(1)))
if filename not in rarfiles:
rarfiles.append(filename)
curr += 1
nzo.set_action_line(T('Unpacking'), '%02d/%02d' % (curr, numrars))
@@ -683,12 +696,6 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
logging.error(T('ERROR: write error (%s)'), line[11:])
fail = 1
elif line.startswith('Cannot create') and sabnzbd.WIN32 and extraction_path.startswith('\\\\?\\'):
# Can be due to Unicode problems on Windows, let's retry
fail = 4
# Kill the process (can stay in endless loop on Windows Server)
p.kill()
elif line.startswith('Cannot create'):
line2 = proc.readline()
if 'must not exceed 260' in line2:
@@ -701,6 +708,8 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
logging.error(T('ERROR: write error (%s)'), unicoder(line[13:]))
nzo.set_unpack_info('Unpack', unicoder(msg))
fail = 1
# Kill the process (can stay in endless loop on Windows Server)
p.kill()
elif line.startswith('ERROR: '):
nzo.fail_msg = T('Unpacking failed, see log')
@@ -763,13 +772,7 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
proc.close()
p.wait()
logging.debug('UNRAR output %s', '\n'.join(lines))
# Unicode problems, lets start again but now we try without \\?\
# This will only fail if the download contains forbidden-Windows-names
if fail == 4:
return rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, clip_path(extraction_path), password)
else:
return fail, (), ()
return fail, (), ()
if proc:
proc.close()
@@ -777,7 +780,7 @@ def rar_extract_core(rarfile_path, numrars, one_folder, nzo, setname, extraction
logging.debug('UNRAR output %s', '\n'.join(lines))
nzo.fail_msg = ''
msg = T('Unpacked %s files/folders in %s') % (str(len(extracted)), format_time_string(time() - start))
msg = T('Unpacked %s files/folders in %s') % (str(len(extracted)), format_time_string(time.time() - start))
nzo.set_unpack_info('Unpack', '[%s] %s' % (unicoder(setname), msg))
logging.info('%s', msg)
@@ -795,7 +798,7 @@ def unzip(nzo, workdir, workdir_complete, delete, one_folder, zips):
try:
i = 0
unzip_failed = False
tms = time()
tms = time.time()
for _zip in zips:
logging.info("Starting extract on zipfile: %s ", _zip)
@@ -811,7 +814,7 @@ def unzip(nzo, workdir, workdir_complete, delete, one_folder, zips):
else:
i += 1
msg = T('%s files in %s') % (str(i), format_time_string(time() - tms))
msg = T('%s files in %s') % (str(i), format_time_string(time.time() - tms))
nzo.set_unpack_info('Unpack', msg)
# Delete the old files if we have to
@@ -854,7 +857,7 @@ def ZIP_Extract(zipfile, extraction_path, one_folder):
stup, need_shell, command, creationflags = build_command(command)
logging.debug('Starting unzip: %s', command)
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, creationflags=creationflags)
@@ -875,7 +878,7 @@ def unseven(nzo, workdir, workdir_complete, delete, one_folder, sevens):
"""
i = 0
unseven_failed = False
tms = time()
tms = time.time()
# Find multi-volume sets, because 7zip will not provide actual set members
sets = {}
@@ -909,7 +912,7 @@ def unseven(nzo, workdir, workdir_complete, delete, one_folder, sevens):
i += 1
if not unseven_failed:
msg = T('%s files in %s') % (str(i), format_time_string(time() - tms))
msg = T('%s files in %s') % (str(i), format_time_string(time.time() - tms))
nzo.set_unpack_info('Unpack', msg)
return unseven_failed
@@ -976,7 +979,7 @@ def seven_extract_core(sevenset, extensions, extraction_path, one_folder, delete
stup, need_shell, command, creationflags = build_command(command)
logging.debug('Starting 7za: %s', command)
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, creationflags=creationflags)
@@ -1020,6 +1023,9 @@ def par2_repair(parfile_nzf, nzo, workdir, setname, single):
if os.path.exists(test_parfile):
parfile_nzf = new_par
break
else:
# No file was found, we assume this set already finished
return False, True
# Shorten just the workdir on Windows
parfile = os.path.join(workdir, parfile_nzf.filename)
@@ -1053,7 +1059,6 @@ def par2_repair(parfile_nzf, nzo, workdir, setname, single):
return readd, result
if not result:
flag_file(workdir, QCHECK_FILE, True)
nzo.status = Status.REPAIRING
result = False
readd = False
@@ -1073,8 +1078,7 @@ def par2_repair(parfile_nzf, nzo, workdir, setname, single):
if finished:
result = True
logging.info('Par verify finished ok on %s!',
parfile)
logging.info('Par verify finished ok on %s!', parfile)
# Remove this set so we don't try to check it again
nzo.remove_parset(parfile_nzf.setname)
@@ -1097,45 +1101,20 @@ def par2_repair(parfile_nzf, nzo, workdir, setname, single):
try:
if cfg.enable_par_cleanup():
deletables = []
new_dir_content = os.listdir(workdir)
# Remove extra files created during repair and par2 base files
for path in new_dir_content:
if os.path.splitext(path)[1] == '.1' and path not in old_dir_content:
try:
path = os.path.join(workdir, path)
deletables.append(os.path.join(workdir, path))
deletables.append(os.path.join(workdir, setname + '.par2'))
deletables.append(os.path.join(workdir, setname + '.PAR2'))
deletables.append(parfile)
logging.info("Deleting %s", path)
os.remove(path)
except:
logging.warning(T('Deleting %s failed!'), path)
path = os.path.join(workdir, setname + '.par2')
path2 = os.path.join(workdir, setname + '.PAR2')
if os.path.exists(path):
try:
logging.info("Deleting %s", path)
os.remove(path)
except:
logging.warning(T('Deleting %s failed!'), path)
if os.path.exists(path2):
try:
logging.info("Deleting %s", path2)
os.remove(path2)
except:
logging.warning(T('Deleting %s failed!'), path2)
if os.path.exists(parfile):
try:
logging.info("Deleting %s", parfile)
os.remove(parfile)
except OSError:
logging.warning(T('Deleting %s failed!'), parfile)
deletables = []
# Add output of par2-repair to remove
deletables.extend(used_joinables)
deletables.extend(used_for_repair)
deletables.extend([os.path.join(workdir, f) for f in used_for_repair])
# Delete pars of the set and maybe extra ones that par2 found
deletables.extend([os.path.join(workdir, f) for f in setpars])
@@ -1164,36 +1143,17 @@ _RE_LOADING_PAR2 = re.compile(r'Loading "([^"]+)"\.')
_RE_LOADED_PAR2 = re.compile(r'Loaded (\d+) new packets')
def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, single=False):
def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, single=False):
""" Run par2 on par-set """
retry_classic = False
used_joinables = []
used_for_repair = []
extra_par2_name = None
# set the current nzo status to "Verifying...". Used in History
nzo.status = Status.VERIFYING
start = time()
start = time.time()
options = cfg.par_option().strip()
classic = classic or not cfg.par2_multicore()
if sabnzbd.WIN32:
# If filenames are UTF-8 then we must use par2-tbb, unless this is a retry with classic
tbb = (sabnzbd.assembler.GetMD5Hashes(parfile, True)[1] and not classic) or not PAR2C_COMMAND
else:
tbb = False
if tbb and options:
command = [str(PAR2_COMMAND), 'r', options, parfile]
else:
if classic:
command = [str(PAR2C_COMMAND), 'r', parfile]
else:
command = [str(PAR2_COMMAND), 'r', parfile]
# Allow options if not classic or when classic and non-classic are the same
if (not classic or (PAR2_COMMAND == PAR2C_COMMAND)):
command.insert(2, options)
logging.debug('Par2-classic/cmdline = %s', classic)
command = [str(PAR2_COMMAND), 'r', options, parfile]
# Append the wildcard for this set
parfolder = os.path.split(parfile)[0]
@@ -1214,7 +1174,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
# We need to check for the bad par2cmdline that skips blocks
# Or the one that complains about basepath
# Only if we're not doing multicore
if not tbb:
if not sabnzbd.WIN32 and not sabnzbd.DARWIN:
par2text = run_simple([command[0], '-h'])
if 'No data skipping' in par2text:
logging.info('Detected par2cmdline version that skips blocks, adding -N parameter')
@@ -1227,19 +1187,15 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
stup, need_shell, command, creationflags = build_command(command)
# par2multicore wants to see \\.\ paths on Windows
# But par2cmdline doesn't support that notation, or \\?\ notation
# See: https://github.com/sabnzbd/sabnzbd/pull/771
if sabnzbd.WIN32 and (tbb or has_win_device(parfile)):
if sabnzbd.WIN32:
command = [x.replace('\\\\?\\', '\\\\.\\', 1) if x.startswith('\\\\?\\') else x for x in command]
elif sabnzbd.WIN32:
# For par2cmdline on Windows we need clipped paths
command = [clip_path(x) if x.startswith('\\\\?\\') else x for x in command]
# Run the external command
logging.debug('Starting par2: %s', command)
lines = []
try:
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, creationflags=creationflags)
@@ -1298,7 +1254,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
if extra_par2_name and line.startswith('Loaded '):
m = _RE_LOADED_PAR2.search(line)
if m and int(m.group(1)) > 0:
used_for_repair.append(os.path.join(nzo.downpath, extra_par2_name))
used_for_repair.append(extra_par2_name)
extra_par2_name = None
continue
extra_par2_name = None
@@ -1310,18 +1266,18 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
logging.error(msg)
elif line.startswith('All files are correct'):
msg = T('[%s] Verified in %s, all files correct') % (unicoder(setname), format_time_string(time() - start))
msg = T('[%s] Verified in %s, all files correct') % (unicoder(setname), format_time_string(time.time() - start))
nzo.set_unpack_info('Repair', msg)
logging.info('Verified in %s, all files correct',
format_time_string(time() - start))
format_time_string(time.time() - start))
finished = 1
elif line.startswith('Repair is required'):
msg = T('[%s] Verified in %s, repair is required') % (unicoder(setname), format_time_string(time() - start))
msg = T('[%s] Verified in %s, repair is required') % (unicoder(setname), format_time_string(time.time() - start))
nzo.set_unpack_info('Repair', msg)
logging.info('Verified in %s, repair is required',
format_time_string(time() - start))
start = time()
format_time_string(time.time() - start))
start = time.time()
verified = 1
elif line.startswith('Loading "'):
@@ -1362,22 +1318,6 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
nzo.status = Status.FAILED
elif line.startswith('You need'):
# Because par2cmdline doesn't handle split files correctly
# if there are joinables, let's join them first and try again
# Only when in the par2-detection also only 1 output-file was mentioned
if joinables and len(datafiles) == 1:
error, newf = file_join(nzo, parfolder, parfolder, True, joinables)
# Only do it again if we had a good join
if newf:
retry_classic = True
# Save the renames in case of retry
for jn in joinables:
renames[datafiles[0]] = os.path.split(jn)[1]
joinables = []
# Need to set it to 1 so the renames get saved
finished = 1
break
chunks = line.split()
needed_blocks = int(chunks[2])
avail_blocks = 0
@@ -1447,7 +1387,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
nzo.status = Status.FAILED
elif line.startswith('Repair is possible'):
start = time()
start = time.time()
nzo.set_action_line(T('Repairing'), '%2d%%' % (0))
elif line.startswith('Repairing:'):
@@ -1457,9 +1397,9 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
nzo.status = Status.REPAIRING
elif line.startswith('Repair complete'):
msg = T('[%s] Repaired in %s') % (unicoder(setname), format_time_string(time() - start))
msg = T('[%s] Repaired in %s') % (unicoder(setname), format_time_string(time.time() - start))
nzo.set_unpack_info('Repair', msg)
logging.info('Repaired in %s', format_time_string(time() - start))
logging.info('Repaired in %s', format_time_string(time.time() - start))
finished = 1
elif line.startswith('File:') and line.find('data blocks from') > 0:
@@ -1483,27 +1423,20 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
logging.debug('PAR2 will reconstruct "%s" from "%s"', new_name, old_name)
reconstructed.append(os.path.join(workdir, old_name))
elif 'Could not write' in line and 'at offset 0:' in line and not classic:
elif 'Could not write' in line and 'at offset 0:' in line:
# If there are joinables, this error will only happen in case of 100% complete files
# We can just skip the retry, because par2cmdline will fail in those cases
# becauses it refuses to scan the ".001" file
if joinables:
finished = 1
used_joinables = []
else:
# Hit a bug in par2-tbb, retry with par2-classic
retry_classic = sabnzbd.WIN32
elif ' cannot be renamed to ' in line:
if not classic and sabnzbd.WIN32:
# Hit a bug in par2-tbb, retry with par2-classic
retry_classic = True
else:
msg = unicoder(line.strip())
nzo.fail_msg = msg
msg = u'[%s] %s' % (unicoder(setname), msg)
nzo.set_unpack_info('Repair', msg)
nzo.status = Status.FAILED
msg = unicoder(line.strip())
nzo.fail_msg = msg
msg = u'[%s] %s' % (unicoder(setname), msg)
nzo.set_unpack_info('Repair', msg)
nzo.status = Status.FAILED
elif 'There is not enough space on the disk' in line:
# Oops, disk is full!
@@ -1570,47 +1503,36 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, sin
p.wait()
except WindowsError, err:
if err[0] == '87' and not classic:
# Hit a bug in par2-tbb, retry with par2-classic
retry_classic = True
else:
raise WindowsError(err)
raise WindowsError(err)
logging.debug('PAR2 output was\n%s', '\n'.join(lines))
# If successful, add renamed files to the collection
if finished and renames:
previous = sabnzbd.load_data(RENAMES_FILE, nzo.workpath, remove=False)
for name in previous or {}:
renames[name] = previous[name]
sabnzbd.save_data(renames, RENAMES_FILE, nzo.workpath)
nzo.renamed_file(renames)
# If successful and files were reconstructed, remove incomplete original files
if finished and reconstructed:
# Use 'used_joinables' as a vehicle to get rid of the files
used_joinables.extend(reconstructed)
if retry_classic:
logging.debug('Retry PAR2-joining with par2-classic/cmdline')
return PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=True, single=single)
else:
return finished, readd, pars, datafiles, used_joinables, used_for_repair
return finished, readd, pars, datafiles, used_joinables, used_for_repair
_RE_FILENAME = re.compile(r'"([^"]+)"')
def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, single=False):
def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, single=False):
""" Run par2 on par-set """
parfolder = os.path.split(parfile)[0]
used_joinables = []
used_for_repair = []
# set the current nzo status to "Verifying...". Used in History
nzo.status = Status.VERIFYING
start = time()
start = time.time()
# Can implement caching of verification by adding:
# '-vs2', '-vd%s' % parfolder
# Caching of verification implemented by adding:
# But not really required due to prospective-par2
command = [str(MULTIPAR_COMMAND), 'r', parfile]
command = [str(MULTIPAR_COMMAND), 'r', '-vs2', '-vd%s' % parfolder, parfile]
# Only add user-options if supplied
options = cfg.par_option().strip()
@@ -1618,7 +1540,6 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
command.insert(2, options)
# Append the wildcard for this set
parfolder = os.path.split(parfile)[0]
if single or len(globber(parfolder, setname + '*')) < 2:
# Support bizarre naming conventions
wildcard = '*'
@@ -1632,7 +1553,7 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
logging.info('Starting MultiPar: %s', command)
lines = []
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, creationflags=creationflags)
@@ -1658,6 +1579,7 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
in_check = False
in_verify = False
in_repair = False
in_verify_repaired = False
misnamed_files = False
old_name = None
@@ -1687,8 +1609,6 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
# Skip empty lines
if line == '':
# Empty lines also end every section
in_repair = False
continue
# Save it all
@@ -1740,7 +1660,7 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
nzo.set_unpack_info('Repair', msg)
nzo.status = Status.FAILED
# ----------------- Verify stage
# ----------------- Start check/verify stage
# List of Par2 files we will use today
if line.startswith('PAR File list'):
in_parlist = True
@@ -1750,9 +1670,20 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
elif in_parlist:
m = _RE_FILENAME.search(line)
if m:
used_for_repair.append(os.path.join(nzo.downpath, TRANS(m.group(1))))
used_for_repair.append(TRANS(m.group(1)))
pars.append(TRANS(m.group(1)))
elif line.startswith('Recovery Set ID'):
# Remove files were MultiPar stores verification result when repaired succesfull
recovery_id = line.split()[-1]
used_for_repair.append('2_%s.bin' % recovery_id)
used_for_repair.append('2_%s.ini' % recovery_id)
elif line.startswith('Input File total count'):
# How many files will it try to find?
verifytotal = int(line.split()[-1])
# ----------------- Misnamed-detection stage
# Misnamed files
elif line.startswith('Searching misnamed file'):
# We are in the misnamed files block
@@ -1776,22 +1707,31 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
datafiles.append(new_name)
reconstructed.append(old_name)
# How many files will it try to find?
elif line.startswith('Input File total count'):
verifytotal = int(line.split()[-1])
# ----------------- Checking stage
# Checking input files
elif line.startswith('Complete file count'):
in_check = False
verifynum = 0
old_name = None
elif line.startswith('Verifying Input File'):
in_check = True
nzo.status = Status.VERIFYING
elif in_check:
m = _RE_FILENAME.search(line)
if m:
verifynum += 1
# Only increase counter if it was really the detection line
if line.startswith('= ') or '%' not in line:
verifynum += 1
nzo.set_action_line(T('Checking'), '%02d/%02d' % (verifynum, verifytotal))
old_name = TRANS(m.group(1))
# ----------------- Verify stage
# Which files need extra verification?
elif line.startswith('Damaged file count'):
verifytotal = int(line.split()[-1])
elif line.startswith('Missing file count'):
verifytotal += int(line.split()[-1])
# Actual verification
elif line.startswith('Input File Slice found'):
@@ -1833,7 +1773,7 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
datafiles.pop()
datafiles.append(new_name)
# Need to remove the old file after repair (Multipar keeps it)
used_for_repair.append(os.path.join(parfolder, old_name))
used_for_repair.append(old_name)
# Need to reset it to avoid collision
old_name = None
@@ -1912,19 +1852,20 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
# Result of verification
elif line.startswith('All Files Complete'):
# Completed without damage!
msg = T('[%s] Verified in %s, all files correct') % (unicoder(setname), format_time_string(time() - start))
msg = T('[%s] Verified in %s, all files correct') % (unicoder(setname), format_time_string(time.time() - start))
nzo.set_unpack_info('Repair', msg)
logging.info('Verified in %s, all files correct',
format_time_string(time() - start))
format_time_string(time.time() - start))
finished = 1
elif line.startswith(('Ready to repair', 'Ready to rejoin')):
# Ready to repair!
# Or we are re-joining a split file when there's no damage but takes time
msg = T('[%s] Verified in %s, repair is required') % (unicoder(setname), format_time_string(time() - start))
msg = T('[%s] Verified in %s, repair is required') % (unicoder(setname), format_time_string(time.time() - start))
nzo.set_unpack_info('Repair', msg)
logging.info('Verified in %s, repair is required',
format_time_string(time() - start))
start = time()
format_time_string(time.time() - start))
start = time.time()
# Set message for user in case of joining
if line.startswith('Ready to rejoin'):
@@ -1933,9 +1874,17 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
# ----------------- Repair stage
elif 'Recovering slice' in line:
# Before this it will calculate matrix, here is where it starts
start = time()
start = time.time()
in_repair = True
nzo.set_action_line(T('Repairing'), '%2d%%' % (0))
elif in_repair and line.startswith('Verifying repair'):
in_repair = False
in_verify_repaired = True
# How many will be checked?
verifytotal = int(line.split()[-1])
verifynum = 0
elif in_repair:
# Line with percentage of repair (nothing else)
per = float(line[:-1])
@@ -1943,11 +1892,17 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
nzo.status = Status.REPAIRING
elif line.startswith('Repaired successfully'):
msg = T('[%s] Repaired in %s') % (unicoder(setname), format_time_string(time() - start))
msg = T('[%s] Repaired in %s') % (unicoder(setname), format_time_string(time.time() - start))
nzo.set_unpack_info('Repair', msg)
logging.info('Repaired in %s', format_time_string(time() - start))
logging.info('Repaired in %s', format_time_string(time.time() - start))
finished = 1
elif in_verify_repaired and line.startswith('Repaired :'):
# Track verification of repaired files (can sometimes take a while)
verifynum += 1
nzo.set_action_line(T('Verifying repair'), '%02d/%02d' % (verifynum, verifytotal))
p.wait()
logging.debug('MultiPar output was\n%s', '\n'.join(lines))
@@ -1957,15 +1912,12 @@ def MultiPar_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False
# Even if the repair did not complete fully it will rename those!
# But the ones in 'Finding available slices'-section will only be renamed after succesfull repair
if renames:
# Adding to the collection
previous = sabnzbd.load_data(RENAMES_FILE, nzo.workpath, remove=False)
for name in previous or {}:
renames[name] = previous[name]
sabnzbd.save_data(renames, RENAMES_FILE, nzo.workpath)
# If succes, we also remove the possibly previously renamed ones
if finished and previous:
reconstructed.extend(previous.values())
if finished:
reconstructed.extend(nzo.renames)
# Adding to the collection
nzo.renamed_file(renames)
# Remove renamed original files
workdir = os.path.split(parfile)[0]
@@ -2029,10 +1981,6 @@ def userxbit(filename):
def build_command(command):
""" Prepare list from running an external program """
for n in xrange(len(command)):
if isinstance(command[n], unicode):
command[n] = deunicode(command[n])
if not sabnzbd.WIN32:
if command[0].endswith('.py'):
with open(command[0], 'r') as script_file:
@@ -2222,10 +2170,8 @@ def QuickCheck(set, nzo):
# Save renames
if renames:
previous = sabnzbd.load_data(RENAMES_FILE, nzo.workpath, remove=False)
for name in previous or {}:
renames[name] = previous[name]
sabnzbd.save_data(renames, RENAMES_FILE, nzo.workpath)
nzo.renamed_file(renames)
return result
@@ -2356,7 +2302,7 @@ def pre_queue(name, pp, cat, script, priority, size, groups):
stup, need_shell, command, creationflags = build_command(command)
env = create_env()
logging.info('Running pre-queue script %s', command)
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, env=env, creationflags=creationflags)
except:
@@ -2426,7 +2372,7 @@ class SevenZip(object):
command = [SEVEN_COMMAND, 'l', '-p', '-y', '-slt', self.path]
stup, need_shell, command, creationflags = build_command(command)
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
startupinfo=stup, creationflags=creationflags)
@@ -2453,7 +2399,7 @@ class SevenZip(object):
else:
stderr = open('/dev/null', 'w')
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
p = Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=stderr,
startupinfo=stup, creationflags=creationflags)
@@ -2469,7 +2415,7 @@ class SevenZip(object):
def run_simple(cmd):
""" Run simple external command and return output """
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
txt = p.stdout.read()
p.wait()
return txt

View File

@@ -39,8 +39,9 @@ from sabnzbd.constants import NOTIFY_KEYS
from sabnzbd.misc import split_host, make_script_path
from sabnzbd.newsunpack import external_script
from gntp import GNTPRegister
from gntp.core import GNTPRegister
from gntp.notifier import GrowlNotifier
import gntp.errors
try:
import Growl
# Detect classic Growl (older than 1.3)
@@ -58,6 +59,7 @@ try:
except:
_HAVE_NTFOSD = False
##############################################################################
# Define translatable message table
##############################################################################
@@ -89,13 +91,9 @@ def get_icon():
if not os.path.isfile(icon):
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico')
if os.path.isfile(icon):
if sabnzbd.WIN32 or sabnzbd.DARWIN:
fp = open(icon, 'rb')
icon = fp.read()
fp.close()
else:
# Due to a bug in GNTP, need this work-around for Linux/Unix
icon = 'http://sabnzbdplus.sourceforge.net/version/sabnzbd.ico'
fp = open(icon, 'rb')
icon = fp.read()
fp.close()
else:
icon = None
return icon
@@ -122,7 +120,7 @@ def check_classes(gtype, section):
def get_prio(gtype, section):
""" Check if `gtype` is enabled in `section` """
""" Check prio of `gtype` in `section` """
try:
return sabnzbd.config.get_config(section, '%s_prio_%s' % (section, gtype))()
except TypeError:
@@ -130,20 +128,32 @@ def get_prio(gtype, section):
return -1000
def send_notification(title, msg, gtype):
def check_cat(section, job_cat):
""" Check if `job_cat` is enabled in `section`. * = All """
if not job_cat:
return True
try:
section_cats = sabnzbd.config.get_config(section, '%s_cats' % section)()
return ('*' in section_cats or job_cat in section_cats)
except TypeError:
logging.debug('Incorrect Notify option %s:%s_prio_%s', section, section, gtype)
return False
def send_notification(title, msg, gtype, job_cat=None):
""" Send Notification message """
# Notification Center
if sabnzbd.DARWIN and sabnzbd.cfg.ncenter_enable():
if check_classes(gtype, 'ncenter'):
if check_classes(gtype, 'ncenter') and check_cat('ncenter', job_cat):
send_notification_center(title, msg, gtype)
# Windows
if sabnzbd.WIN32 and sabnzbd.cfg.acenter_enable():
if check_classes(gtype, 'acenter'):
if check_classes(gtype, 'acenter') and check_cat('acenter', job_cat):
send_windows(title, msg, gtype)
# Growl
if sabnzbd.cfg.growl_enable() and check_classes(gtype, 'growl'):
if sabnzbd.cfg.growl_enable() and check_classes(gtype, 'growl') and check_cat('growl', job_cat):
if _HAVE_CLASSIC_GROWL and not sabnzbd.cfg.growl_server():
return send_local_growl(title, msg, gtype)
else:
@@ -151,32 +161,33 @@ def send_notification(title, msg, gtype):
time.sleep(0.5)
# Prowl
if sabnzbd.cfg.prowl_enable():
if sabnzbd.cfg.prowl_enable() and check_cat('prowl', job_cat):
if sabnzbd.cfg.prowl_apikey():
Thread(target=send_prowl, args=(title, msg, gtype)).start()
time.sleep(0.5)
# Pushover
if sabnzbd.cfg.pushover_enable():
if sabnzbd.cfg.pushover_enable() and check_cat('pushover', job_cat):
if sabnzbd.cfg.pushover_token():
Thread(target=send_pushover, args=(title, msg, gtype)).start()
time.sleep(0.5)
# Pushbullet
if sabnzbd.cfg.pushbullet_enable():
if sabnzbd.cfg.pushbullet_enable() and check_cat('pushbullet', job_cat):
if sabnzbd.cfg.pushbullet_apikey() and check_classes(gtype, 'pushbullet'):
Thread(target=send_pushbullet, args=(title, msg, gtype)).start()
time.sleep(0.5)
# Notification script.
if sabnzbd.cfg.nscript_enable():
if sabnzbd.cfg.nscript_enable() and check_cat('nscript', job_cat):
if sabnzbd.cfg.nscript_script():
Thread(target=send_nscript, args=(title, msg, gtype)).start()
time.sleep(0.5)
# NTFOSD
if have_ntfosd() and sabnzbd.cfg.ntfosd_enable() and check_classes(gtype, 'ntfosd'):
send_notify_osd(title, msg)
if have_ntfosd() and sabnzbd.cfg.ntfosd_enable():
if check_classes(gtype, 'ntfosd') and check_cat('ntfosd', job_cat):
send_notify_osd(title, msg)
def reset_growl():
@@ -193,6 +204,9 @@ def register_growl(growl_server, growl_password):
sys_name = hostname(host)
# Reduce logging of Growl in Debug/Info mode
logging.getLogger('gntp').setLevel(logging.WARNING)
# Clean up persistent data in GNTP to make re-registration work
GNTPRegister.notifications = []
GNTPRegister.headers = {}
@@ -216,7 +230,7 @@ def register_growl(growl_server, growl_password):
logging.debug(error)
del growler
ret = None
except socket.error, err:
except (gntp.errors.NetworkError, gntp.errors.AuthError) as err:
error = 'Cannot register with Growl %s' % str(err)
logging.debug(error)
del growler
@@ -270,7 +284,7 @@ def send_growl(title, msg, gtype, test=None):
else:
logging.debug('Growl error %s', ret)
return 'Growl error %s', ret
except socket.error, err:
except (gntp.errors.NetworkError, gntp.errors.AuthError) as err:
error = 'Growl error %s' % err
logging.debug(error)
return error

View File

@@ -259,6 +259,7 @@ class NzbQueue(object):
del self.__nzo_table[nzo_id]
# And attach the new nzo to the old nzo_id
self.__nzo_table[old_id] = new_nzo
logging.info('Replacing in queue %s by %s', nzo.final_name, new_nzo.final_name)
del nzo
return new_nzo
except:
@@ -289,6 +290,7 @@ class NzbQueue(object):
def generate_future(self, msg, pp=None, script=None, cat=None, url=None, priority=NORMAL_PRIORITY, nzbname=None):
""" Create and return a placeholder nzo object """
logging.debug('Creating placeholder NZO')
future_nzo = NzbObject(msg, pp, script, None, True, cat=cat, url=url, priority=priority, nzbname=nzbname, status=Status.GRABBING)
self.add(future_nzo)
return future_nzo
@@ -306,6 +308,7 @@ class NzbQueue(object):
for nzo_id in [item.strip() for item in nzo_ids.split(',')]:
if nzo_id in self.__nzo_table:
self.__nzo_table[nzo_id].script = script
logging.info('Set script=%s for job %s', script, self.__nzo_table[nzo_id].final_name)
result += 1
return result
@@ -315,15 +318,21 @@ class NzbQueue(object):
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]
nzo.cat, pp, nzo.script, prio = cat_to_opts(cat)
logging.info('Set cat=%s for job %s', cat, nzo.final_name)
nzo.set_pp(pp)
if explicit_priority is None:
self.set_priority(nzo_id, prio)
# Abort any ongoing unpacking if the category changed
nzo.abort_direct_unpacker()
result += 1
return result
def change_name(self, nzo_id, name, password=None):
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]
logging.info('Renaming %s to %s', nzo.final_name, name)
# Abort any ongoing unpacking if the name changed (dirs change)
nzo.abort_direct_unpacker()
if not nzo.futuretype:
nzo.set_final_name_pw(name, password)
else:
@@ -389,7 +398,7 @@ class NzbQueue(object):
self.save(nzo)
if not (quiet or nzo.status in ('Fetching',)):
notifier.send_notification(T('NZB added to queue'), nzo.filename, 'download')
notifier.send_notification(T('NZB added to queue'), nzo.filename, 'download', nzo.cat)
if not quiet and cfg.auto_sort():
self.sort_by_avg_age()
@@ -410,20 +419,17 @@ class NzbQueue(object):
# Other information is obtained from the nzo
history_db.add_history_db(nzo, '', '', 0, '', '')
history_db.close()
sabnzbd.history_updated()
elif cleanup:
self.cleanup_nzo(nzo, keep_basic, del_files)
sabnzbd.remove_data(nzo_id, nzo.workpath)
logging.info('Removed job %s', nzo.final_name)
if save:
self.save(nzo)
else:
nzo_id = None
# Update the last check time, since history was updated
sabnzbd.LAST_HISTORY_UPDATE = time.time()
return nzo_id
def remove_multiple(self, nzo_ids, del_files=False):
@@ -458,6 +464,7 @@ class NzbQueue(object):
if nzf:
removed.append(nzf_id)
nzo.abort_direct_unpacker()
post_done = nzo.remove_nzf(nzf)
if post_done:
if nzo.finished_files:
@@ -470,7 +477,7 @@ class NzbQueue(object):
nzo.bytes_tried -= (nzf.bytes - nzf.bytes_left)
del nzo.files_table[nzf_id]
nzo.finished_files.remove(nzf)
logging.info('Removed NZFs %s from job %s', removed, nzo.final_name)
return removed
def pause_multiple_nzo(self, nzo_ids):
@@ -485,7 +492,7 @@ class NzbQueue(object):
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]
nzo.pause()
logging.debug("Paused nzo: %s", nzo_id)
logging.info("Paused nzo: %s", nzo_id)
handled.append(nzo_id)
return handled
@@ -503,7 +510,7 @@ class NzbQueue(object):
nzo = self.__nzo_table[nzo_id]
nzo.resume()
nzo.reset_all_try_lists()
logging.debug("Resumed nzo: %s", nzo_id)
logging.info("Resumed nzo: %s", nzo_id)
handled.append(nzo_id)
return handled
@@ -548,6 +555,7 @@ class NzbQueue(object):
item_id_pos2 = i
if (item_id_pos1 > -1) and (item_id_pos2 > -1):
item = self.__nzo_list[item_id_pos1]
logging.info('Switching job [%s] %s => [%s] %s', item_id_pos1, item.final_name, item_id_pos2, self.__nzo_list[item_id_pos2].final_name)
del self.__nzo_list[item_id_pos1]
self.__nzo_list.insert(item_id_pos2, item)
return (item_id_pos2, nzo1.priority)
@@ -573,15 +581,15 @@ class NzbQueue(object):
self.__nzo_table[nzo_id].move_bottom_bulk(nzf_ids)
def sort_by_avg_age(self, reverse=False):
logging.info("Sorting by average date...(reversed:%s)", reverse)
logging.info("Sorting by average date... (reversed:%s)", reverse)
self.__nzo_list = sort_queue_function(self.__nzo_list, _nzo_date_cmp, reverse)
def sort_by_name(self, reverse=False):
logging.info("Sorting by name...(reversed:%s)", reverse)
logging.info("Sorting by name... (reversed:%s)", reverse)
self.__nzo_list = sort_queue_function(self.__nzo_list, _nzo_name_cmp, reverse)
def sort_by_size(self, reverse=False):
logging.info("Sorting by size...(reversed:%s)", reverse)
logging.info("Sorting by size... (reversed:%s)", reverse)
self.__nzo_list = sort_queue_function(self.__nzo_list, _nzo_size_cmp, reverse)
def sort_queue(self, field, reverse=None):
@@ -666,6 +674,8 @@ class NzbQueue(object):
# if the queue is empty then simple append the item to the bottom
self.__nzo_list.append(nzo)
pos = 0
logging.info('Set priority=%s for job %s => position=%s ', priority, self.__nzo_table[nzo_id].final_name, pos)
return pos
except:
@@ -733,7 +743,7 @@ class NzbQueue(object):
else:
if file_done:
if nzo.next_save is None or time.time() > nzo.next_save:
sabnzbd.save_data(nzo, nzo.nzo_id, nzo.workpath)
nzo.save_to_disk()
BPSMeter.do.save()
if nzo.save_timeout is None:
nzo.next_save = None
@@ -755,6 +765,7 @@ class NzbQueue(object):
def end_job(self, nzo):
""" Send NZO to the post-processing queue """
logging.info('Ending job %s', nzo.final_name)
if self.actives(grabs=False) < 2 and cfg.autodisconnect():
# This was the last job, close server connections
if sabnzbd.downloader.Downloader.do:
@@ -837,6 +848,8 @@ class NzbQueue(object):
return empty
def cleanup_nzo(self, nzo, keep_basic=False, del_files=False):
# Abort DirectUnpack and let it remove files
nzo.abort_direct_unpacker()
nzo.purge_data(keep_basic, del_files)
ArticleCache.do.purge_articles(nzo.saved_articles)

View File

@@ -66,9 +66,10 @@ RE_NORMAL_NAME = re.compile(r'\.\w{2,5}$') # Test reasonably sized extension at
# Trylist
##############################################################################
TRYLIST_LOCK = threading.Lock()
class TryList(object):
""" TryList keeps track of which servers have been tried for a specific article
This used to have a Lock, but it's not needed (all atomic) and faster without
"""
# Pre-define attributes to save memory
__slots__ = ('__try_list', 'fetcher_priority')
@@ -79,16 +80,19 @@ class TryList(object):
def server_in_try_list(self, server):
""" Return whether specified server has been tried """
return server in self.__try_list
with TRYLIST_LOCK:
return server in self.__try_list
def add_to_try_list(self, server):
""" Register server as having been tried already """
self.__try_list.append(server)
with TRYLIST_LOCK:
if server not in self.__try_list:
self.__try_list.append(server)
def reset_try_list(self):
""" Clean the list """
self.__try_list = []
self.fetcher_priority = 0
with TRYLIST_LOCK:
self.__try_list = []
##############################################################################
@@ -209,10 +213,10 @@ class Article(TryList):
# NzbFile
##############################################################################
NzbFileSaver = (
'date', 'subject', 'filename', 'type', 'is_par2', 'vol', 'blocks', 'setname',
'extrapars', 'articles', 'decodetable', 'bytes', 'bytes_left',
'date', 'subject', 'filename', 'filename_checked', 'type', 'is_par2', 'vol',
'blocks', 'setname','extrapars', 'articles', 'decodetable', 'bytes', 'bytes_left',
'article_count', 'nzo', 'nzf_id', 'deleted', 'valid', 'import_finished',
'md5sum'
'md5sum', 'md5of16k'
)
@@ -229,6 +233,7 @@ class NzbFile(TryList):
self.subject = subject
self.type = None
self.filename = name_extractor(subject)
self.filename_checked = False
self.is_par2 = False
self.vol = None
@@ -250,6 +255,7 @@ class NzbFile(TryList):
self.import_finished = False
self.md5sum = None
self.md5of16k = None
self.valid = bool(article_db)
@@ -271,6 +277,13 @@ class NzbFile(TryList):
self.decodetable[partnum] = article
self.import_finished = True
else:
# TEMPORARY ERRORS
if not os.path.exists(os.path.join(self.nzf_id, self.nzo.workpath)):
logging.warning('Article DB file not found %s', self)
else:
# It was there, but empty
logging.warning('Article DB empty %s', self)
def remove_article(self, article, found):
""" Handle completed article, possibly end of file """
@@ -278,8 +291,10 @@ class NzbFile(TryList):
self.articles.remove(article)
if found:
self.bytes_left -= article.bytes
# To keep counter correct for pre-check
if self.nzo.precheck:
self.nzo.bytes_downloaded += article.bytes
self.nzo.bytes_tried += article.bytes
return (not self.articles)
def set_par2(self, setname, vol, blocks):
@@ -309,6 +324,11 @@ class NzbFile(TryList):
""" Is this file completed? """
return self.import_finished and not bool(self.articles)
@property
def lowest_partnum(self):
""" Get lowest article number of this file """
return min(self.decodetable)
def remove_admin(self):
""" Remove article database from disk (sabnzbd_nzf_<id>)"""
try:
@@ -529,13 +549,13 @@ class NzbParser(xml.sax.handler.ContentHandler):
##############################################################################
NzbObjectSaver = (
'filename', 'work_name', 'final_name', 'created', 'bytes', 'bytes_downloaded', 'bytes_tried',
'repair', 'unpack', 'delete', 'script', 'cat', 'url', 'groups', 'avg_date',
'repair', 'unpack', 'delete', 'script', 'cat', 'url', 'groups', 'avg_date', 'md5of16k',
'partable', 'extrapars', 'md5packs', 'files', 'files_table', 'finished_files', 'status',
'avg_bps_freq', 'avg_bps_total', 'priority', 'dupe_table', 'saved_articles', 'nzo_id',
'futuretype', 'deleted', 'parsed', 'action_line', 'unpack_info', 'fail_msg', 'nzo_info',
'custom_name', 'password', 'next_save', 'save_timeout', 'encrypted',
'duplicate', 'oversized', 'precheck', 'incomplete', 'reuse', 'meta',
'md5sum', 'servercount', 'unwanted_ext', 'rating_filtered'
'md5sum', 'servercount', 'unwanted_ext', 'renames', 'rating_filtered'
)
# Lock to prevent errors when saving the NZO data
@@ -584,6 +604,7 @@ class NzbObject(TryList):
self.meta = {}
self.servercount = {} # Dict to keep bytes per server
self.created = False # dirprefixes + work_name created
self.direct_unpacker = None # Holds the DirectUnpacker instance
self.bytes = 0 # Original bytesize
self.bytes_downloaded = 0 # Downloaded byte
self.bytes_tried = 0 # Which bytes did we try
@@ -602,10 +623,12 @@ class NzbObject(TryList):
self.partable = {} # Holds one parfile-name for each set
self.extrapars = {} # Holds the extra parfile names for all sets
self.md5packs = {} # Holds the md5pack for each set
self.md5packs = {} # Holds the md5pack for each set (name: hash)
self.md5of16k = {} # Holds the md5s of the first-16k of all files in the NZB (hash: name)
self.files = [] # List of all NZFs
self.files_table = {} # Dictionary of NZFs indexed using NZF_ID
self.renames = {} # Dictionary of all renamed files
self.finished_files = [] # List of all finished NZFs
@@ -934,7 +957,6 @@ class NzbObject(TryList):
nzf.deleted = True
return not bool(self.files)
@synchronized(NZO_LOCK)
def reset_all_try_lists(self):
for nzf in self.files:
nzf.reset_all_try_lists()
@@ -954,8 +976,9 @@ class NzbObject(TryList):
head, vol, block = analyse_par2(name)
if head and matcher(lparset, head.lower()):
xnzf.set_par2(parset, vol, block)
self.extrapars[parset].append(xnzf)
if not self.precheck:
# Don't postpone if all par2 should be kept
if cfg.enable_par_cleanup():
self.extrapars[parset].append(xnzf)
self.files.remove(xnzf)
@synchronized(NZO_LOCK)
@@ -968,7 +991,7 @@ class NzbObject(TryList):
if not nzf.is_par2:
head, vol, block = analyse_par2(fn)
# Is a par2file and repair mode activated
if head and (self.repair or cfg.allow_streaming()):
if head and self.repair:
# Skip if mini-par2 is not complete and there are more par2 files
if not block and nzf.bytes_left and self.extrapars.get(head):
return
@@ -1023,8 +1046,6 @@ class NzbObject(TryList):
self.fail_msg = T('Aborted, cannot be completed') + ' - https://sabnzbd.org/not-complete'
self.set_unpack_info('Download', self.fail_msg, unique=False)
logging.debug('Abort job "%s", due to impossibility to complete it', self.final_name_pw_clean)
# Update the last check time
sabnzbd.LAST_HISTORY_UPDATE = time.time()
return True, True
if file_done:
@@ -1033,6 +1054,7 @@ class NzbObject(TryList):
if not found:
# Add extra parfiles when there was a damaged article and not pre-checking
if self.extrapars and not self.precheck:
self.abort_direct_unpacker()
self.prospective_add(nzf)
post_done = False
@@ -1063,6 +1085,7 @@ class NzbObject(TryList):
if name in files:
files.remove(name)
files.append(renames[name])
self.renames = renames
# Looking for the longest name first, minimizes the chance on a mismatch
files.sort(lambda x, y: len(y) - len(x))
@@ -1083,6 +1106,7 @@ class NzbObject(TryList):
nzfs.remove(nzf)
files.remove(filename)
self.bytes_tried += nzf.bytes
self.bytes_downloaded += nzf.bytes
break
try:
@@ -1112,6 +1136,10 @@ class NzbObject(TryList):
def set_pp(self, value):
self.repair, self.unpack, self.delete = sabnzbd.pp_to_opts(value)
logging.info('Set pp=%s for job %s', value, self.final_name)
# Abort unpacking if not desired anymore
if not self.unpack:
self.abort_direct_unpacker()
self.save_to_disk()
@property
@@ -1162,7 +1190,7 @@ class NzbObject(TryList):
self.status = Status.PAUSED
# Prevent loss of paused state when terminated
if self.nzo_id and not self.is_gone():
sabnzbd.save_data(self, self.nzo_id, self.workpath)
self.save_to_disk()
def resume(self):
self.status = Status.QUEUED
@@ -1190,7 +1218,8 @@ class NzbObject(TryList):
@synchronized(NZO_LOCK)
def remove_parset(self, setname):
self.partable.pop(setname)
if setname in self.partable:
self.partable.pop(setname)
@synchronized(NZO_LOCK)
def remove_extrapar(self, parfile):
@@ -1221,22 +1250,37 @@ class NzbObject(TryList):
if nzf_check.blocks:
blocks_already = blocks_already + int_conv(nzf_check.blocks)
# Make sure to also select a parset if it was in the original filename
original_filename = self.renames.get(nzf.filename, '')
# Need more?
if not nzf.is_par2 and blocks_already < total_need:
# We have to find the right par-set
for parset in self.extrapars.keys():
if parset in nzf.filename and self.extrapars[parset]:
if (parset in nzf.filename or parset in original_filename) and self.extrapars[parset]:
extrapars_sorted = sorted(self.extrapars[parset], key=lambda x: x.blocks, reverse=True)
# Loop until we have enough
while blocks_already < total_need and extrapars_sorted:
# Add the first one
new_nzf = extrapars_sorted.pop()
# Reset NZF TryList, in case something was on it before it became extrapar
new_nzf.reset_try_list()
self.add_parfile(new_nzf)
self.extrapars[parset] = extrapars_sorted
blocks_already = blocks_already + int_conv(new_nzf.blocks)
logging.info('Prospectively added %s repair blocks to %s', new_nzf.blocks, self.final_name)
# Reset all try lists
self.reset_all_try_lists()
# Reset NZO TryList
self.reset_try_list()
def add_to_direct_unpacker(self, nzf):
""" Start or add to DirectUnpacker """
if not self.direct_unpacker:
sabnzbd.directunpacker.DirectUnpacker(self)
self.direct_unpacker.add(nzf)
def abort_direct_unpacker(self):
""" Abort any running DirectUnpackers """
if self.direct_unpacker:
self.direct_unpacker.abort()
def check_quality(self, req_ratio=0):
""" Determine amount of articles present on servers
@@ -1338,7 +1382,7 @@ class NzbObject(TryList):
if sabnzbd.highest_server(server):
nzf.finish_import()
# Still not finished? Something went wrong...
if not nzf.import_finished:
if not nzf.import_finished and not self.is_gone():
logging.error(T('Error importing %s'), nzf)
nzf_remove_list.append(nzf)
continue
@@ -1430,6 +1474,17 @@ class NzbObject(TryList):
self.files[pos + 1] = nzf
self.files[pos] = tmp_nzf
@synchronized(NZO_LOCK)
def renamed_file(self, name_set, old_name=None):
""" Save renames at various stages (Download/PP)
to be used on Retry. Accepts strings and dicts.
"""
if not old_name:
# Add to dict
self.renames.update(name_set)
else:
self.renames[name_set] = old_name
# Determine if rating information (including site identifier so rating can be updated)
# is present in metadata and if so store it
@synchronized(NZO_LOCK)
@@ -1489,6 +1544,8 @@ class NzbObject(TryList):
if keep_basic:
remove_all(wpath, 'SABnzbd_nz?_*', keep_folder=True)
remove_all(wpath, 'SABnzbd_article_*', keep_folder=True)
# We save the renames file
sabnzbd.save_data(self.renames, RENAMES_FILE, self.workpath)
else:
remove_all(wpath, recursive=True)
if del_files:
@@ -1525,8 +1582,9 @@ class NzbObject(TryList):
self.files if full else [],
queued_files,
self.status, self.priority,
len(self.nzo_info.get('missing_art_log', []))
)
len(self.nzo_info.get('missing_art_log', [])),
self.bytes_tried - self.bytes_downloaded,
self.direct_unpacker.get_formatted_stats() if self.direct_unpacker else 0)
def get_nzf_by_id(self, nzf_id):
if nzf_id in self.files_table:
@@ -1545,19 +1603,19 @@ class NzbObject(TryList):
else:
self.unpack_info[key] = [msg]
@synchronized(NZO_LOCK)
def set_action_line(self, action=None, msg=None):
# Update the last check time
sabnzbd.LAST_HISTORY_UPDATE = time.time()
if action and msg:
self.action_line = '%s: %s' % (action, msg)
else:
self.action_line = ''
# Make sure it's updated in the interface
sabnzbd.history_updated()
@property
def repair_opts(self):
return self.repair, self.unpack, self.delete
@synchronized(NZO_LOCK)
def save_to_disk(self):
""" Save job's admin to disk """
self.save_attribs()
@@ -1624,7 +1682,6 @@ class NzbObject(TryList):
""" Is this job still going somehow? """
return self.status in (Status.COMPLETED, Status.DELETED, Status.FAILED)
@synchronized(NZO_LOCK)
def __getstate__(self):
""" Save to pickle file, selecting attributes """
dict_ = {}
@@ -1644,10 +1701,15 @@ class NzbObject(TryList):
self.avg_stamp = time.mktime(self.avg_date.timetuple())
self.wait = None
self.to_be_removed = False
self.direct_unpacker = None
if self.meta is None:
self.meta = {}
if self.servercount is None:
self.servercount = {}
if self.md5of16k is None:
self.md5of16k = {}
if self.renames is None:
self.renames = {}
if self.bytes_tried is None:
# Fill with old info
self.bytes_tried = 0

View File

@@ -618,8 +618,8 @@ class SABnzbdDelegate(NSObject):
def diskspaceUpdate(self):
try:
self.completefolder_menu_item.setTitle_("%s%.2f GB" % (T('Complete Folder') + '\t\t\t', diskspace(sabnzbd.cfg.complete_dir.get_path())[1]))
self.incompletefolder_menu_item.setTitle_("%s%.2f GB" % (T('Incomplete Folder') + '\t\t', diskspace(sabnzbd.cfg.download_dir.get_path())[1]))
self.completefolder_menu_item.setTitle_("%s%.2f GB" % (T('Complete Folder') + '\t\t\t', diskspace()['complete_dir'][1]))
self.incompletefolder_menu_item.setTitle_("%s%.2f GB" % (T('Incomplete Folder') + '\t\t', diskspace()['download_dir'][1]))
except:
logging.info("[osx] diskspaceUpdate Exception %s" % (sys.exc_info()[0]))

Some files were not shown because too many files have changed in this diff Show More