Compare commits

...

158 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
Safihre
8483e4ab8a Add last minor change to changelog 2017-06-17 09:01:53 +02:00
Safihre
f6c163b505 CherryPy 8.1.2 - Catch OSX "Protocol wrong type for socket" 2017-06-17 08:40:14 +02:00
Safihre
8f30173db0 Update text files for 2.2.0Alpha1 2017-06-16 15:38:17 +02:00
Brendan Ball
0372ff95bb Added support for systemd power controls
added systemd support to powersup.linux_shutdown, linux_hibernate, linux_standby
2017-06-16 15:19:32 +02:00
Safihre
6fa29c7877 Update translatable texts 2017-06-15 20:58:48 +02:00
Safihre
d4c9121593 Remove NZB_LOCK
Not required
2017-06-15 20:54:13 +02:00
Safihre
76a8df0282 Make it more clear that Hostname verification is a server problem 2017-06-15 20:51:27 +02:00
Safihre
0b6d8309a0 Format the SSL certificate messages more for humans 2017-06-15 20:51:21 +02:00
Safihre
10a9bc0817 Don't show Advanded on Config>General if HTTPS disabled 2017-06-15 16:09:01 +02:00
Safihre
2a14af4ffa Firefox doesn't suck at animations anymore 2017-06-15 14:47:04 +02:00
Safihre
d1a4a292e3 Prevent log-flooding when job is too old for server 2017-06-13 21:40:45 +02:00
Safihre
14c0efa151 Fix error in MultiPar repair when first .par2-file was broken 2017-06-13 21:40:45 +02:00
Safihre
4fc03f2581 Discard all articles at once when too old for server 2017-06-13 21:40:45 +02:00
Safihre
3205b9fda9 Don't fill anything for bandwith limit if nothing is set
Now it would just fill "M" when nothing was set.
2017-06-13 21:40:45 +02:00
Safihre
953e0d6c22 Remove DIR_LOCK
The operations it was locking were always performed from 1 thread anyway.
2017-06-13 21:40:45 +02:00
Safihre
b50ce54ca9 Reformat IO_LOCK to really only protect against NZO-saving collisions
The main intent was not to read/write to same file, but this can (as far as I can see) never happen anyway. 
Before this change 2 threads could not be writing data at the same time, even if they were writing to completly different directories.
2017-06-13 21:40:45 +02:00
Safihre
5e7558ce4a Remove locks from ArticleCache
All operations on the list are atomic or modify objects in place that can't be read at the same time.
2017-06-13 21:40:45 +02:00
Safihre
8aa6362432 Remove NZBQueue wrapper functions
Direct-access!
2017-06-13 21:40:45 +02:00
Safihre
02ebb97a8b Remove NZBQUEUE_LOCK and only use synced wrappers when needed
Less locks = Less waiting
2017-06-13 21:40:45 +02:00
Safihre
b36063403d Remove legacy asserts and work-a-rounds 2017-06-13 21:40:45 +02:00
Safihre
526ffa2afb Add new translation to Changelog 2017-06-13 17:26:37 +02:00
Safihre
5b3fd812d8 Don't break MO-creation on missing Email templates 2017-06-13 14:40:47 +02:00
Safihre
af6dac9cdc Refresh Config > General when submitting language change 2017-06-13 14:14:26 +02:00
Safihre
bc25d936bb Show Hebrew in language menu
Correction of previous commit: I intended to write that it's an experiment because Hebrew is RTL. It seems to work!
2017-06-13 14:14:01 +02:00
Safihre
b497fe1444 Add Hebrew as language
This is an experiment, since Hebrew is LTR language. But a translator translated almost all the texts, so we want to use his efforts!
2017-06-13 11:43:18 +02:00
Safihre
3f456cce05 Move max_art_opt to Specials
Only for special cases (don't know which ones, but I can imagine it could be usefull..)
Deprecate later!
2017-06-13 00:40:02 +02:00
Safihre
4dd2f089ec Move replace_illegal to Specials
Who doesn't want that
2017-06-13 00:37:20 +02:00
Safihre
b1b1bc248d Reformat startswith() to use tuples when testing multiple options 2017-06-11 22:01:45 +02:00
Safihre
d9e675469c Don't throw errors when silent-saving fails 2017-06-11 22:01:45 +02:00
Safihre
ede0ca1772 Catch new way of par2 reporting bad parameters
See: https://forums.sabnzbd.org/viewtopic.php?f=2&t=22713&p=112209#p112209
2017-06-11 22:01:45 +02:00
Safihre
2d098a1477 Defend against possible NTFS crash
Closes #930
2017-06-11 22:01:45 +02:00
Safihre
e5f014b68e Replace spaces/dots in the order written in the Config 2017-06-11 22:01:45 +02:00
Safihre
b3a9dc9eeb Fix old code throughout 2017-06-11 22:01:45 +02:00
Safihre
2a06cec27c Seperate compatibility-check logic in NZBQueue 2017-06-11 22:01:45 +02:00
Safihre
19230c889d Remove unused functions and constants (vulture) 2017-06-11 22:01:45 +02:00
Safihre
c969ce552c Remove unused constants (vulture) 2017-06-11 22:01:45 +02:00
Safihre
2def600d21 Remove unused imports and functions (pyflakes) 2017-06-11 22:01:45 +02:00
Safihre
02aa8f18c8 Trylist doesn't need locks, all atomic operations 2017-06-11 22:01:45 +02:00
Safihre
fcd9522dae Remove unused import and define NzbQueue as proper class 2017-06-11 22:01:45 +02:00
Safihre
72d3ce885e Remove un-used version definitions from __init__ 2017-06-11 22:01:45 +02:00
Safihre
b428996eb7 Convert pickles and keep queue order
Also restore future jobs


dewd
2017-06-11 22:01:45 +02:00
Safihre
2b4eb58fad Correctly show message about old Queue-version
Now it's also upgrade-proof, and not just works for 1 version.
2017-06-11 22:01:45 +02:00
Safihre
240e8dff60 Bump Queue-Version to force Queue-Repair 2017-06-11 22:01:45 +02:00
Safihre
1c286afde6 Implement __slots__ to conserve memory
Objects such as Article() get created a lot. By using the __slots__ property, python will only reserve space for the give keywords instead of a whole diectonary. 

Testing showed that (on WinX64) 1 job now takes between 2-3MB of memory when loaded, compared to 4MB before.
2017-06-11 22:01:45 +02:00
Safihre
2eeb908540 Revert "Remove enable_par_cleanup"
This reverts commit f5ab4a2253.
2017-06-11 16:10:25 +02:00
Safihre
562e6ecce9 Fix Untill typos in texts and comments
Closes #943
2017-06-11 11:51:50 +02:00
Safihre
4bd0d32508 Bump version to 2.2.0-develop 2017-06-11 11:46:36 +02:00
Safihre
6f2ccbef80 Always show Par2-Multicore status on first Config page on Linux 2017-06-09 16:04:30 +02:00
133 changed files with 27077 additions and 23480 deletions

View File

@@ -1,5 +1,5 @@
*******************************************
*** This is SABnzbd 2.1.0 ***
*** This is SABnzbd 2.2.0 ***
*******************************************
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,

View File

@@ -1,4 +1,4 @@
SABnzbd 2.1.0
SABnzbd 2.2.0
-------------------------------------------------------------------------------
0) LICENSE

View File

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

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,22 +1,54 @@
Release Notes - SABnzbd 2.1.0
Release Notes - SABnzbd 2.2.0 Alpha 3
=========================================================
## Changes since 2.0.1
- Windows-only: Will now use MultiPar for verification and repair. MultiPar uses
the latest optimizations, multiple cores and can utilize the GPU, resulting in
repairs often being twice as fast. Created by Yutaka Sawada (Windows only)
"Extra Par2 Parameters" are also passed to MultiPar
For example: Set "/lc32" to enable GPU for repair (can also be slower!)
- Minimal macOS version set to 10.9 (Mavericks)
- Human readable history date/time default setting in Glitter (x hours ago, etc)
- Post-processing script execution can be aborted
- Removed Specials settings: never_repair, nr_decoders, prio_sort_list,
create_group_folder, enable_par_cleanup, enable_quickcheck, allow_64bit_tools
NOTE: Due to changes in this release, the queue will be converted when 2.2.0
is started for the first time. Job order, settings and data will be
preserved, but all jobs will be unpaused and URL's that did not finish
fetching before the upgrade will be lost!
# Bugfixes
- File join would fail incorrectly
- Catch and show 'Disk full' messages from PAR2/MultiPar
- MemoryError occurred when opening the interface after long period of inactivity
## Changes since 2.1.0
- Direct Unpack: Jobs will start unpacking during the download, reduces
post-processing time but requires capable hard drive. Only works for jobs that
do not need repair. Will be enabled if your incomplete folder-speed > 60MB/s
- Reduced memory usage, especially with larger queues
- Removed 5 second delay between fetching URLs
- Notifications can now be limited to certain Categories
- Each item in the Queue and Filelist now has Move to Top/Bottom buttons
- Smoother animations in Firefox (disabled previously due to FF high-CPU usage)
- Jobs outside server retention are processed faster
- Show missing articles in MB instead of number of articles
- Obfuscated filenames are renamed during downloading, if possible
- If enable_par_cleanup is disabled all par2 files be downloaded
- If enabled, replace dots in filenames also when there are spaces already
- Update GNTP bindings to 1.0.3
- max_art_opt and replace_illegal moved from Switches to Specials
- Removed Specials enable_meta, par2_multicore and allow_streaming
- Windows: Full unicode support when calling repair and unpack
- Windows: Move enable_multipar to Specials
- Windows: Better indication of verification process before and after repair
- Windows: MultiPar verification of a job is skipped after blocks are fetched
- Windows & macOS: removed par2cmdline in favor of par2tbb/Multipar
## Bugfixes since 2.1.0
- Shutdown/suspend did not work on some Linux systems
- Deleting a job could result in write errors
- Display warning if custom par2 parameters are wrong
- RSS URLs with commas were broken
- Fixed some "Saving failed" errors
- Fixed crashing URLGrabber
- Jobs with renamed files are now correctly handled when using Retry
- Disk-space readings could be updated incorrectly
- Correct redirect after enabling HTTPS in the Config
- Fix race-condition in Post-processing
- History would not always show latest changes
- Convert HTML in error messages
- Fixed unicode error during Sorting
- Not all texts were shown in the selected Language
- Windows: Fix error in MultiPar-code when first par2-file was damaged
- macOS: Catch 'Protocol wrong type for socket' errors
## Translations
- Added Hebrew translation by ION IL, many other languages updated.
## Upgrading from 0.7.x and older
- Finish queue

View File

@@ -56,8 +56,6 @@ if [int(n) for n in cherrypy.__version__.split('.')] < [8, 1, 2]:
print 'Sorry, requires Python module Cherrypy 8.1.2+ (use the included version)'
sys.exit(1)
from cherrypy import _cpserver
SQLITE_DLL = True
try:
from sqlite3 import version as sqlite3_version
@@ -90,7 +88,7 @@ from sabnzbd.misc import real_path, \
check_latest_version, exit_sab, \
split_host, get_ext, create_https_certificates, \
windows_variant, ip_extract, set_serv_parms, get_serv_parms, globber_full
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, panic_fwall, \
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, \
panic_sqlite, panic, launch_a_browser
import sabnzbd.scheduler as scheduler
import sabnzbd.config as config
@@ -430,9 +428,6 @@ def print_modules():
else:
logging.error(T('par2 binary... NOT found!'))
if sabnzbd.newsunpack.PAR2C_COMMAND:
logging.info("par2cmdline binary... found (%s)", sabnzbd.newsunpack.PAR2C_COMMAND)
if sabnzbd.newsunpack.MULTIPAR_COMMAND:
logging.info("MultiPar binary... found (%s)", sabnzbd.newsunpack.MULTIPAR_COMMAND)
@@ -727,24 +722,6 @@ def evaluate_inipath(path):
return path
def cherrypy_logging(log_path, log_handler):
""" Setup CherryPy logging """
log = cherrypy.log
log.access_file = ''
log.error_file = ''
# Max size of 512KB
maxBytes = getattr(log, "rot_maxBytes", 524288)
# cherrypy.log cherrypy.log.1 cherrypy.log.2
backupCount = getattr(log, "rot_backupCount", 3)
# Make a new RotatingFileHandler for the error log.
fname = getattr(log, "rot_error_file", log_path)
h = log_handler(fname, 'a', maxBytes, backupCount)
h.setLevel(logging.DEBUG)
h.setFormatter(cherrypy._cplogging.logfmt)
log.error_log.addHandler(h)
def commandline_handler(frozen=True):
""" Split win32-service commands are true parameters
Returns:
@@ -1353,7 +1330,6 @@ def main():
'error_page.404': sabnzbd.panic.error_page_404
})
# Do we want CherryPy Logging? Cannot be done via the config
if cherrypylogging:
sabnzbd.WEBLOGFILE = os.path.join(logdir, DEF_LOG_CHERRY)

View File

@@ -196,12 +196,13 @@ socket_errors_to_ignore = plat_specific_errors(
)
socket_errors_to_ignore.append('timed out')
socket_errors_to_ignore.append('The read operation timed out')
if sys.platform == 'darwin':
socket_errors_to_ignore.append(plat_specific_errors('EPROTOTYPE'))
socket_errors_nonblocking = plat_specific_errors(
'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
if sys.platform == 'darwin':
socket_errors_to_ignore.append(plat_specific_errors('EPROTOTYPE'))
socket_errors_nonblocking.append(plat_specific_errors('EPROTOTYPE'))
comma_separated_headers = [
ntob(h) for h in
['Accept', 'Accept-Charset', 'Accept-Encoding',

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

@@ -41,12 +41,16 @@
</td>
</tr>
<!--#end if#-->
<!--#if not $have_mt_par2#-->
<!--#if not $nt and not $darwin#-->
<tr>
<th scope="row">Multicore Par2</th>
<th scope="row">$T('opt-multicore-par2')</th>
<td>
<!--#if $have_mt_par2#-->
<span class="glyphicon glyphicon-ok"></span>
<!--#else#-->
<span class="label label-warning">$T('notAvailable')</span> $T('explain-getpar2mt')
<a href="${helpuri}installation/multicore-par2" target="_blank">${helpuri}installation/multicore-par2</a>
<!--#end if#-->
</td>
</tr>
<!--#end if#-->
@@ -105,7 +109,7 @@
<tbody>
<tr>
<th scope="row">$T('homePage') </th>
<td><a href="http://sabnzbd.org/" target="_blank">http://sabnzbd.org/</a></td>
<td><a href="https://sabnzbd.org/" target="_blank">https://sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('menu-wiki') </th>
@@ -113,7 +117,7 @@
</tr>
<tr>
<th scope="row">$T('menu-forums') </th>
<td><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td>
<td><a href="https://forums.sabnzbd.org/" target="_blank">https://forums.sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('source') </th>
@@ -127,6 +131,10 @@
<th scope="row">$T('menu-issues') </th>
<td><a href="https://sabnzbd.org/wiki/introduction/known-issues" target="_blank">https://sabnzbd.org/wiki/introduction/known-issues</a></td>
</tr>
<tr>
<th scope="row">$T('menu-donate') </th>
<td><a href="https://sabnzbd.org/donate" target="_blank">https://sabnzbd.org/donate</a></td>
</tr>
</tbody>
</table>
</div>

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">
@@ -240,7 +240,17 @@
\$('.alert-translate').show()
}
}
\$('#language').on('change', hideOrShowTranslate)
\$('#language').on('change', function() {
// Show message
hideOrShowTranslate()
// Re-load page on submit
\$('.fullform').submit(function() {
// Skip the fancy stuff, just submit
this.submit()
})
// No JSON reponse
\$('#ajax').val('')
})
hideOrShowTranslate()
\$('#apikey, #nzbkey').click(function () { \$(this).select() });
@@ -314,14 +324,20 @@
if(bandwidthLimit) {
var bandwithLimitNumber = parseFloat(bandwidthLimit)
var bandwithLimitText = bandwidthLimit.replace(/[^a-zA-Z]+/g, '');
\$('#bandwidth_max_value').val(bandwithLimitNumber)
\$('#bandwidth_max_dropdown').val(bandwithLimitText)
if(bandwithLimitNumber) {
\$('#bandwidth_max_value').val(bandwithLimitNumber)
\$('#bandwidth_max_dropdown').val(bandwithLimitText)
}
}
// Update the value
\$('#bandwidth_max_value, #bandwidth_max_dropdown').on('change', function() {
\$('#bandwidth_max').val(\$('#bandwidth_max_value').val() + \$('#bandwidth_max_dropdown').val())
if(\$('#bandwidth_max_value').val()) {
\$('#bandwidth_max').val(\$('#bandwidth_max_value').val() + \$('#bandwidth_max_dropdown').val())
} else {
\$('#bandwidth_max').val('')
}
})
});

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

@@ -338,6 +338,7 @@
}).then(function(data) {
// Let's replace the link
msg = data.value.message.replace('https://sabnzbd.org/certificate-errors', '<a href="https://sabnzbd.org/certificate-errors" class="alert-link" target="_blank">https://sabnzbd.org/certificate-errors</a>')
msg = msg.replace('-', '<br>')
// Fill the box and enable the button
resultBox.removeClass('alert-success alert-danger').show()
resultBox.html(msg)

View File

@@ -31,12 +31,6 @@
<input type="number" name="max_art_tries" id="max_art_tries" value="$max_art_tries" min="2" max="2000" />
<span class="desc">$T('explain-max_art_tries')</span>
</div>
<div class="field-pair">
<label class="config" for="max_art_opt">$T('opt-max_art_opt')</label>
<input type="checkbox" name="max_art_opt" id="max_art_opt" value="1" <!--#if int($max_art_opt) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-max_art_opt')</span>
</div>
<div class="field-pair">
<label class="config" for="auto_disconnect">$T('opt-auto_disconnect')</label>
<input type="checkbox" name="auto_disconnect" id="auto_disconnect" value="1" <!--#if int($auto_disconnect) > 0 then 'checked="checked"' else ""#--> />
@@ -136,6 +130,11 @@
<input type="checkbox" name="auto_sort" id="auto_sort" value="1" <!--#if int($auto_sort) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-auto_sort')</span>
</div>
<div class="field-pair">
<label class="config" for="direct_unpack">$T('opt-direct_unpack')</label>
<input type="checkbox" name="direct_unpack" id="direct_unpack" value="1" <!--#if int($direct_unpack) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-direct_unpack')</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
@@ -159,13 +158,6 @@
<input type="checkbox" name="enable_all_par" id="enable_all_par" value="1" <!--#if int($enable_all_par) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_all_par').replace('. ', '.<br/>')</span>
</div>
<!--#if $nt#-->
<div class="field-pair">
<label class="config" for="multipar">$T('opt-par2_multipar')</label>
<input type="checkbox" name="multipar" id="multipar" value="1" <!--#if int($multipar) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-par2_multipar')</span>
</div>
<!--#end if#-->
<div class="field-pair">
<label class="config" for="par_option">$T('opt-par_option')</label>
<input type="text" name="par_option" id="par_option" value="$par_option" />
@@ -251,11 +243,6 @@
<input type="checkbox" name="replace_dots" id="replace_dots" value="1" <!--#if int($replace_dots) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-replace_dots')</span>
</div>
<div class="field-pair">
<label class="config" for="replace_illegal">$T('opt-replace_illegal')</label>
<input type="checkbox" name="replace_illegal" id="replace_illegal" value="1" <!--#if int($replace_illegal) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-replace_illegal')</span>
</div>
<!--#if not $nt#-->
<div class="field-pair">
<label class="config" for="sanitize_safe">$T('opt-sanitize_safe')</label>

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())) {
@@ -263,7 +260,7 @@ function do_restart() {
// Keep counter of failures
var failureCounter = 0;
// Now we try untill we can connect
// Now we try until we can connect
var refreshInterval = setInterval(function() {
// We skip the first one
if(failureCounter == 0) {
@@ -409,6 +406,9 @@ $(document).ready(function () {
$('#enable_https').on('change', function() {
$('.enable_https_options').toggle()
})
if(!$('#enable_https').is(':checked')) {
$('.enable_https_options').hide()
}
$('.advancedButton').click(function(event){
$('.advanced-settings').toggle()

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) {
@@ -1921,49 +1961,6 @@ input[name="nzbURL"] {
}
}
/***
SPECIAL FOR FIREFOX
It uses very high CPU for anything animated (Sep 2015)
Disable animations on progress-bar and make the History-'processing' a block animation
Can be removed if it's performance gets better in the future..
***/
@supports (-moz-transform: translate(0, 0)) {
.progress-bar {
transform: none !important;
animation: none !important;
transition: none !important;
}
@keyframes stretchdelay {
0%, 60% {
transform: scaleY(0.4);
}
61%, 100% {
transform: scaleY(1.0);
}
}
.processing-download > div {
animation: stretchdelay 2s infinite linear;
}
.processing-download .loader-bar-two {
animation-delay: 0.2s;
}
.processing-download .loader-bar-three {
animation-delay: 0.4s;
}
.processing-download .loader-bar-four {
animation-delay: 0.6s;
}
.queue-table td.name input {
margin-left: -2px;
}
}
/***
Bootstrap overwrites

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

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

4812
po/main/he.po Normal file
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."

98
po/nsis/he.po Normal file
View File

@@ -0,0 +1,98 @@
# Hebrew translation for sabnzbd
# Copyright (c) 2017 Rosetta Contributors and Canonical Ltd 2017
# This file is distributed under the same license as the sabnzbd package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
#
msgid ""
msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2017-06-22 20:42+0000\n"
"PO-Revision-Date: 2017-05-06 09:07+0000\n"
"Last-Translator: ION IL <Unknown>\n"
"Language-Team: Hebrew <he@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2017-06-23 05:56+0000\n"
"X-Generator: Launchpad (build 18416)\n"
#: NSIS_Installer.nsi
msgid "Show Release Notes"
msgstr "הראה הערות שחרור"
#: NSIS_Installer.nsi
msgid "Start SABnzbd"
msgstr "התחל את SABnzbd"
#: NSIS_Installer.nsi
msgid "Support the project, Donate!"
msgstr "תמוך במיזם, תרום!"
#: NSIS_Installer.nsi
msgid "Please close \"SABnzbd.exe\" first"
msgstr "אנא סגור את \"SABnzbd.exe\" תחילה"
#: NSIS_Installer.nsi
msgid ""
"The installation directory has changed (now in \"Program Files\"). \\nIf you "
"run SABnzbd as a service, you need to update the service settings."
msgstr ""
"ספרית ההתקנה השתנתה (עכשיו ב-\"Program Files\"). \\nאם תריץ את SABnzbd בתור "
"שירות, אתה צריך לעדכן את הגדרות השירות."
#: NSIS_Installer.nsi
msgid "This will uninstall SABnzbd from your system"
msgstr "זה יסיר את SABnzbd ממערכתך"
#: NSIS_Installer.nsi
msgid "Run at startup"
msgstr "הפעל באתחול"
#: NSIS_Installer.nsi
msgid "Desktop Icon"
msgstr "צלמית שולחן עבודה"
#: NSIS_Installer.nsi
msgid "NZB File association"
msgstr "שיוך קבצי NZB"
#: NSIS_Installer.nsi
msgid "Delete Program"
msgstr "מחק תכנית"
#: NSIS_Installer.nsi
msgid "Delete Settings"
msgstr "מחק הגדרות"
#: NSIS_Installer.nsi
msgid ""
"This system requires the Microsoft runtime library VC90 to be installed "
"first. Do you want to do that now?"
msgstr ""
"מערכת זו דורשת את ספרית זמן-אמת VC90 של Microsoft שתהיה מותקנת תחילה. האם "
"ברצונך להתקין אותה כעת?"
#: NSIS_Installer.nsi
msgid "Downloading Microsoft runtime installer..."
msgstr "מוריד מתקין זמן-אמת של Microsoft..."
#: NSIS_Installer.nsi
msgid "Download error, retry?"
msgstr "שגיאת הורדה, לנסות שוב?"
#: NSIS_Installer.nsi
msgid "Cannot install without runtime library, retry?"
msgstr "לא ניתן להתקין ללא ספרית זמן-אמת, לנסות שוב?"
#: NSIS_Installer.nsi
msgid ""
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
"the previous version or `Cancel` to cancel this upgrade."
msgstr ""
"אינך יכול לדרוס התקנה קיימת.\\n\\nלחץ על `אישור` כדי להסיר את הגרסה הקודמת "
"או על `ביטול` כדי לבטל שדרוג זה."
#: NSIS_Installer.nsi
msgid "Your settings and data will be preserved."
msgstr "ההגדרות והנתונים שלך יישמרו."

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

@@ -15,9 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Imported to be referenced from other files directly
from sabnzbd.version import __version__, __baseline__
__configversion__ = 18
__queueversion__ = 8
import os
import logging
@@ -25,8 +24,6 @@ import datetime
import tempfile
import cPickle
import pickle
import zipfile
import glob
import gzip
import subprocess
import time
@@ -34,7 +31,7 @@ import socket
import cherrypy
import sys
import re
from threading import RLock, Lock, Condition, Thread
from threading import Lock, Thread
try:
import sleepless
except ImportError:
@@ -43,7 +40,7 @@ except ImportError:
##############################################################################
# Determine platform flags
##############################################################################
WIN32 = DARWIN = POSIX = FOUNDATION = WIN64 = False
WIN32 = DARWIN = FOUNDATION = WIN64 = False
KERNEL32 = None
if os.name == 'nt':
@@ -57,7 +54,6 @@ if os.name == 'nt':
elif os.name == 'posix':
ORG_UMASK = os.umask(18)
os.umask(ORG_UMASK)
POSIX = True
import platform
if platform.system().lower() == 'darwin':
DARWIN = True
@@ -110,9 +106,10 @@ import sabnzbd.cfg as cfg
import sabnzbd.database
import sabnzbd.lang as lang
import sabnzbd.api
from sabnzbd.decorators import synchronized, synchronized_CV, IO_LOCK
import sabnzbd.directunpacker as directunpacker
from sabnzbd.decorators import synchronized, notify_downloader
from sabnzbd.constants import NORMAL_PRIORITY, VALID_ARCHIVES, GIGI, \
REPAIR_REQUEST, QUEUE_FILE_NAME, QUEUE_VERSION, QUEUE_FILE_TMPL
REPAIR_REQUEST, QUEUE_FILE_NAME, QUEUE_VERSION, QUEUE_FILE_TMPL
import sabnzbd.getipaddress as getipaddress
LINUX_POWER = powersup.HAVE_DBUS
@@ -161,7 +158,7 @@ WEBUI_READY = False
LAST_WARNING = None
LAST_ERROR = None
EXTERNAL_IPV6 = False
LAST_HISTORY_UPDATE = time.time()
LAST_HISTORY_UPDATE = 1
# Performance measure for dashboard
PYSTONE_SCORE = 0
@@ -177,10 +174,10 @@ __SHUTTING_DOWN__ = False
##############################################################################
def sig_handler(signum=None, frame=None):
global SABSTOP, WINTRAY
if sabnzbd.WIN32 and type(signum) != type(None) and DAEMON and signum == 5:
if sabnzbd.WIN32 and signum is not None and DAEMON and signum == 5:
# Ignore the "logoff" event when running as a Win32 daemon
return True
if type(signum) != type(None):
if signum is not None:
logging.warning(T('Signal %s caught, saving and exiting...'), signum)
try:
save_state()
@@ -387,6 +384,8 @@ def halt():
sabnzbd.zconfig.remove_server()
sabnzbd.directunpacker.abort_all()
rss.stop()
logging.debug('Stopping URLGrabber')
@@ -580,12 +579,9 @@ def unpause_all():
##############################################################################
# NZB_LOCK Methods
# NZB Saving Methods
##############################################################################
NZB_LOCK = Lock()
@synchronized(NZB_LOCK)
def backup_exists(filename):
""" Return True if backup exists and no_dupes is set """
path = cfg.nzb_backup_dir.get_path()
@@ -599,7 +595,6 @@ def backup_nzb(filename, data):
save_compressed(path, filename, data)
@synchronized(NZB_LOCK)
def save_compressed(folder, filename, data):
""" Save compressed NZB file in folder """
# Need to go to the save folder to
@@ -625,9 +620,9 @@ def save_compressed(folder, filename, data):
##############################################################################
# CV synchronized (notifies downloader)
# Unsynchronized methods
##############################################################################
@synchronized_CV
def add_nzbfile(nzbfile, pp=None, script=None, cat=None, priority=NORMAL_PRIORITY, nzbname=None, reuse=False, password=None):
""" Add disk-based NZB file, optional attributes,
'reuse' flag will suppress duplicate detection
@@ -697,9 +692,6 @@ def add_nzbfile(nzbfile, pp=None, script=None, cat=None, priority=NORMAL_PRIORIT
keep=keep, reuse=reuse, password=password)
##############################################################################
# Unsynchronized methods
##############################################################################
def enable_server(server):
""" Enable server (scheduler only) """
try:
@@ -809,7 +801,6 @@ def change_queue_complete_action(action, new=True):
# keep the name of the action for matching the current select in queue.tmpl
QUEUECOMPLETE = action
QUEUECOMPLETEACTION = _action
QUEUECOMPLETEARG = _argument
@@ -854,7 +845,7 @@ def keep_awake():
def CheckFreeSpace():
""" Check if enough disk space is free, if not pause downloader and send email """
if cfg.download_free() and not sabnzbd.downloader.Downloader.do.paused:
if misc.diskspace(cfg.download_dir.get_path(), force=True)[1] < cfg.download_free.get_float() / GIGI:
if misc.diskspace(force=True)['download_dir'][1] < cfg.download_free.get_float() / GIGI:
logging.warning(T('Too little diskspace forcing PAUSE'))
# Pause downloader, but don't save, since the disk is almost full!
Downloader.do.pause(save=False)
@@ -865,7 +856,6 @@ def CheckFreeSpace():
# Data IO #
################################################################################
@synchronized(IO_LOCK)
def get_new_id(prefix, folder, check_list=None):
""" Return unique prefixed admin identifier within folder
optionally making sure that id is not in the check_list.
@@ -886,7 +876,6 @@ def get_new_id(prefix, folder, check_list=None):
raise IOError
@synchronized(IO_LOCK)
def save_data(data, _id, path, do_pickle=True, silent=False):
""" Save data to a diskfile """
if not silent:
@@ -899,14 +888,17 @@ def save_data(data, _id, path, do_pickle=True, silent=False):
with open(path, 'wb') as data_file:
if do_pickle:
if cfg.use_pickle():
cPickle.dump(data, data_file)
else:
pickle.dump(data, data_file)
else:
cPickle.dump(data, data_file)
else:
data_file.write(data)
break
except:
if t == 2:
if silent:
# This can happen, probably a removed folder
pass
elif t == 2:
logging.error(T('Saving %s failed'), path)
logging.info("Traceback: ", exc_info=True)
else:
@@ -914,7 +906,6 @@ def save_data(data, _id, path, do_pickle=True, silent=False):
time.sleep(0.1)
@synchronized(IO_LOCK)
def load_data(_id, path, remove=True, do_pickle=True, silent=False):
""" Read data from disk file """
path = os.path.join(path, _id)
@@ -946,7 +937,6 @@ def load_data(_id, path, remove=True, do_pickle=True, silent=False):
return data
@synchronized(IO_LOCK)
def remove_data(_id, path):
""" Remove admin file """
path = os.path.join(path, _id)
@@ -958,7 +948,6 @@ def remove_data(_id, path):
logging.debug("Failed to remove %s", path)
@synchronized(IO_LOCK)
def save_admin(data, _id):
""" Save data in admin folder in specified format """
path = os.path.join(cfg.admin_dir.get_path(), _id)
@@ -982,7 +971,6 @@ def save_admin(data, _id):
time.sleep(0.1)
@synchronized(IO_LOCK)
def load_admin(_id, remove=False, silent=False):
""" Read data in admin folder in specified format """
path = os.path.join(cfg.admin_dir.get_path(), _id)
@@ -1170,10 +1158,6 @@ def highest_server(me):
return sabnzbd.downloader.Downloader.do.highest_server(me)
def proxy_pre_queue(name, pp, cat, script, priority, size, groups):
return sabnzbd.newsunpack.pre_queue(name, pp, cat, script, priority, size, groups)
def test_ipv6():
""" Check if external IPv6 addresses are reachable """
if not cfg.selftest_host():
@@ -1199,3 +1183,11 @@ def test_ipv6():
except:
logging.debug('Test IPv6: Problem during IPv6 connect. Disabling IPv6. Reason: %s', sys.exc_info()[0])
return False
def history_updated():
""" To make sure we always have a fresh history """
sabnzbd.LAST_HISTORY_UPDATE += 1
# Never go over the limit
if sabnzbd.LAST_HISTORY_UPDATE+1 >= sys.maxint:
sabnzbd.LAST_HISTORY_UPDATE = 1

View File

@@ -27,7 +27,7 @@ import time
import json
import cherrypy
import locale
import socket
from threading import Thread
try:
locale.setlocale(locale.LC_ALL, "")
@@ -40,15 +40,14 @@ try:
except ImportError:
pass
import sabnzbd
from sabnzbd.constants import VALID_ARCHIVES, Status, \
TOP_PRIORITY, REPAIR_PRIORITY, HIGH_PRIORITY, HIGH_PRIORITY, NORMAL_PRIORITY, LOW_PRIORITY, \
TOP_PRIORITY, REPAIR_PRIORITY, HIGH_PRIORITY, NORMAL_PRIORITY, LOW_PRIORITY, \
KIBI, MEBI, GIGI, JOB_ADMIN
import sabnzbd.config as config
import sabnzbd.cfg as cfg
from sabnzbd.downloader import Downloader
from sabnzbd.nzbqueue import NzbQueue, set_priority, sort_queue, scan_jobs, repair_job
from sabnzbd.nzbqueue import NzbQueue
import sabnzbd.scheduler as scheduler
from sabnzbd.skintext import SKIN_TEXT
from sabnzbd.utils.json import JsonWriter
@@ -165,14 +164,8 @@ def _api_del_config(name, output, kwargs):
def _api_qstatus(name, output, kwargs):
""" API: accepts output """
if output == 'json':
# Compatibility Fix:
# Old qstatus did not have a keyword, so do not use one now.
keyword = ''
else:
keyword = 'queue'
info, pnfo_list, bytespersec = build_queue()
return report(output, keyword='', data=remove_callable(info))
return report(output, data=remove_callable(info))
def _api_queue(name, output, kwargs):
@@ -253,7 +246,7 @@ def _api_queue_priority(output, value, kwargs):
priority = int(value2)
except:
return report(output, _MSG_INT_VALUE)
pos = set_priority(value, priority)
pos = NzbQueue.do.set_priority(value, priority)
# Returns the position in the queue, -1 is incorrect job-id
return report(output, keyword='position', data=pos)
except:
@@ -267,7 +260,7 @@ def _api_queue_sort(output, value, kwargs):
sort = kwargs.get('sort')
direction = kwargs.get('dir', '')
if sort:
sort_queue(sort, direction)
NzbQueue.do.sort_queue(sort, direction)
return report(output)
else:
return report(output, _MSG_NO_VALUE2)
@@ -491,13 +484,14 @@ def _api_history(name, output, kwargs):
value = kwargs.get('value', '')
start = int_conv(kwargs.get('start'))
limit = int_conv(kwargs.get('limit'))
last_history_update = int_conv(kwargs.get('last_history_update', 0))
search = kwargs.get('search')
failed_only = kwargs.get('failed_only')
categories = kwargs.get('category')
last_history_update = kwargs.get('last_history_update', 0)
# Do we need to send anything?
if int(last_history_update) == int(sabnzbd.LAST_HISTORY_UPDATE):
if last_history_update == sabnzbd.LAST_HISTORY_UPDATE:
return report(output, keyword='history', data=False)
if categories and not isinstance(categories, list):
@@ -517,15 +511,13 @@ def _api_history(name, output, kwargs):
history_db.remove_failed(search)
if special in ('all', 'completed'):
history_db.remove_completed(search)
# Update the last check time
sabnzbd.LAST_HISTORY_UPDATE = time.time()
sabnzbd.history_updated()
return report(output)
elif value:
jobs = value.split(',')
for job in jobs:
del_hist_job(job, del_files)
# Update the last check time
sabnzbd.LAST_HISTORY_UPDATE = time.time()
sabnzbd.history_updated()
return report(output)
else:
return report(output, _MSG_NO_VALUE)
@@ -539,7 +531,7 @@ def _api_history(name, output, kwargs):
search=search, failed_only=failed_only,
categories=categories,
output=output)
history['last_history_update'] = int(sabnzbd.LAST_HISTORY_UPDATE)
history['last_history_update'] = sabnzbd.LAST_HISTORY_UPDATE
history['version'] = sabnzbd.__version__
return report(output, keyword='history', data=remove_callable(history))
else:
@@ -686,7 +678,7 @@ def _api_osx_icon(name, output, kwargs):
def _api_rescan(name, output, kwargs):
""" API: accepts output """
scan_jobs(all=False, action=True)
NzbQueue.do.scan_jobs(all=False, action=True)
return report(output)
@@ -1196,7 +1188,7 @@ def build_status(skip_dashboard=False, output=None):
info['logfile'] = sabnzbd.LOGFILE
info['weblogfile'] = sabnzbd.WEBLOGFILE
info['loglevel'] = str(cfg.log_level())
info['folders'] = [xml_name(item) for item in sabnzbd.nzbqueue.scan_jobs(all=False, action=False)]
info['folders'] = [xml_name(item) for item in NzbQueue.do.scan_jobs(all=False, action=False)]
info['configfn'] = xml_name(config.get_filename())
# Dashboard: Speed of System
@@ -1340,7 +1332,6 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
priority = pnfo.priority
mbleft = (bytesleft / MEBI)
mb = (bytes / MEBI)
missing = pnfo.missing
slot = {'index': n, 'nzo_id': str(nzo_id)}
slot['unpackopts'] = str(sabnzbd.opts_to_pp(pnfo.repair, pnfo.unpack, pnfo.delete))
@@ -1355,6 +1346,8 @@ def build_queue(start=0, limit=0, trans=False, output=None, search=None):
slot['sizeleft'] = format_bytes(bytesleft)
slot['percentage'] = "%s" % (int(((mb - mbleft) / mb) * 100)) if mb != mbleft else '0'
slot['missing'] = pnfo.missing
slot['mbmissing'] = "%.2f" % (pnfo.bytes_missing / MEBI)
slot['direct_unpack'] = pnfo.direct_unpack
if not output:
slot['mb_fmt'] = locale.format('%d', int(mb), True)
slot['mbdone_fmt'] = locale.format('%d', int(mb - mbleft), True)
@@ -1525,7 +1518,6 @@ def options_list(output):
return report(output, keyword='options', data={
'yenc': sabnzbd.decoder.HAVE_YENC,
'par2': sabnzbd.newsunpack.PAR2_COMMAND,
'par2c': sabnzbd.newsunpack.PAR2C_COMMAND,
'multipar': sabnzbd.newsunpack.MULTIPAR_COMMAND,
'rar': sabnzbd.newsunpack.RAR_COMMAND,
'zip': sabnzbd.newsunpack.ZIP_COMMAND,
@@ -1548,7 +1540,7 @@ def retry_job(job, new_nzb, password):
else:
path = history_db.get_path(job)
if path:
nzo_id = repair_job(platform_encode(path), new_nzb, password)
nzo_id = NzbQueue.do.repair_job(platform_encode(path), new_nzb, password)
history_db.remove_history(job)
return nzo_id
return None
@@ -1592,6 +1584,7 @@ def Tspec(txt):
else:
return txt
_SKIN_CACHE = {} # Stores pre-translated acronyms
# This special is to be used in interface.py for template processing
# to be passed for the $T function: so { ..., 'T' : Ttemplate, ...}
@@ -1629,8 +1622,7 @@ def build_header(webdir='', output=None):
if speed_limit_abs <= 0:
speed_limit_abs = ''
disk_total1, disk_free1 = diskspace(cfg.download_dir.get_path())
disk_total2, disk_free2 = diskspace(cfg.complete_dir.get_path())
diskspace_info = diskspace()
header = {}
@@ -1662,18 +1654,17 @@ def build_header(webdir='', output=None):
header['session'] = cfg.api_key()
header['new_release'], header['new_rel_url'] = sabnzbd.NEW_VERSION
header['version'] = sabnzbd.__version__
header['paused'] = Downloader.do.paused or Downloader.do.postproc
header['pause_int'] = scheduler.pause_int()
header['paused_all'] = sabnzbd.PAUSED_ALL
header['diskspace1'] = "%.2f" % disk_free1
header['diskspace2'] = "%.2f" % disk_free2
header['diskspace1_norm'] = to_units(disk_free1 * GIGI)
header['diskspace2_norm'] = to_units(disk_free2 * GIGI)
header['diskspacetotal1'] = "%.2f" % disk_total1
header['diskspacetotal2'] = "%.2f" % disk_total2
header['diskspace1'] = "%.2f" % diskspace_info['download_dir'][1]
header['diskspace2'] = "%.2f" % diskspace_info['complete_dir'][1]
header['diskspace1_norm'] = to_units(diskspace_info['download_dir'][1] * GIGI)
header['diskspace2_norm'] = to_units(diskspace_info['complete_dir'][1] * GIGI)
header['diskspacetotal1'] = "%.2f" % diskspace_info['download_dir'][0]
header['diskspacetotal2'] = "%.2f" % diskspace_info['complete_dir'][0]
header['loadavg'] = loadavg()
header['speedlimit'] = "{1:0.{0}f}".format(int(speed_limit % 1 > 0), speed_limit)
header['speedlimit_abs'] = "%s" % speed_limit_abs
@@ -1812,9 +1803,6 @@ def build_history(start=None, limit=None, verbose=False, verbose_list=None, sear
cookie = cherrypy.request.cookie
if 'history_verbosity' in cookie:
k = cookie['history_verbosity'].value
c_path = cookie['history_verbosity']['path']
c_age = cookie['history_verbosity']['max-age']
c_version = cookie['history_verbosity']['version']
if k == 'all':
details_show_all = True

View File

@@ -30,24 +30,26 @@ from sabnzbd.constants import GIGI, ANFO
ARTICLE_LOCK = threading.Lock()
class ArticleCache(object):
""" Operations on lists/dicts are atomic enough that we
do not have to put locks. Only the cache-size needs
a lock since the integer needs to stay synced.
With less locking, the decoder and assembler do not
have to wait on each other.
"""
do = None
def __init__(self):
self.__cache_limit_org = 0
self.__cache_limit = 0
self.__cache_size = 0
self.__article_list = [] # List of buffered articles
self.__article_table = {} # Dict of buffered articles
ArticleCache.do = self
@synchronized(ARTICLE_LOCK)
def cache_info(self):
return ANFO(len(self.__article_list), abs(self.__cache_size), self.__cache_limit_org)
@synchronized(ARTICLE_LOCK)
def new_limit(self, limit):
""" Called when cache limit changes """
self.__cache_limit_org = limit
@@ -57,23 +59,28 @@ class ArticleCache(object):
self.__cache_limit = min(limit, GIGI)
@synchronized(ARTICLE_LOCK)
def increase_cache_size(self, value):
self.__cache_size += value
@synchronized(ARTICLE_LOCK)
def decrease_cache_size(self, value):
self.__cache_size -= value
def reserve_space(self, data):
""" Is there space left in the set limit? """
data_size = sys.getsizeof(data) * 64
self.__cache_size += data_size
self.increase_cache_size(data_size)
if self.__cache_size + data_size > self.__cache_limit:
return False
else:
return True
@synchronized(ARTICLE_LOCK)
def free_reserve_space(self, data):
""" Remove previously reserved space """
data_size = sys.getsizeof(data) * 64
self.__cache_size -= data_size
self.decrease_cache_size(data_size)
return self.__cache_size + data_size < self.__cache_limit
@synchronized(ARTICLE_LOCK)
def save_article(self, article, data):
nzf = article.nzf
nzo = nzf.nzo
@@ -81,8 +88,6 @@ class ArticleCache(object):
if nzo.is_gone():
# Do not discard this article because the
# file might still be processed at this moment!!
if sabnzbd.LOG_ALL:
logging.debug("%s is discarded", article)
return
saved_articles = article.nzf.nzo.saved_articles
@@ -93,7 +98,6 @@ class ArticleCache(object):
if self.__cache_limit:
if self.__cache_limit < 0:
self.__add_to_cache(article, data)
else:
data_size = len(data)
@@ -102,7 +106,7 @@ class ArticleCache(object):
# Flush oldest article in cache
old_article = self.__article_list.pop(0)
old_data = self.__article_table.pop(old_article)
self.__cache_size -= len(old_data)
self.decrease_cache_size(len(old_data))
# No need to flush if this is a refreshment article
if old_article != article:
self.__flush_article(old_article, old_data)
@@ -116,7 +120,6 @@ class ArticleCache(object):
else:
self.__flush_article(article, data)
@synchronized(ARTICLE_LOCK)
def load_article(self, article):
data = None
nzo = article.nzf.nzo
@@ -124,9 +127,7 @@ class ArticleCache(object):
if article in self.__article_list:
data = self.__article_table.pop(article)
self.__article_list.remove(article)
self.__cache_size -= len(data)
if sabnzbd.LOG_ALL:
logging.debug("Loaded %s from cache", article)
self.decrease_cache_size(len(data))
elif article.art_id:
data = sabnzbd.load_data(article.art_id, nzo.workpath, remove=True,
do_pickle=False, silent=True)
@@ -136,23 +137,19 @@ class ArticleCache(object):
return data
@synchronized(ARTICLE_LOCK)
def flush_articles(self):
self.__cache_size = 0
while self.__article_list:
article = self.__article_list.pop(0)
data = self.__article_table.pop(article)
self.__flush_article(article, data)
self.__cache_size = 0
@synchronized(ARTICLE_LOCK)
def purge_articles(self, articles):
if sabnzbd.LOG_ALL:
logging.debug("Purgeable articles -> %s", articles)
for article in articles:
if article in self.__article_list:
self.__article_list.remove(article)
data = self.__article_table.pop(article)
self.__cache_size -= len(data)
self.decrease_cache_size(len(data))
if article.art_id:
sabnzbd.remove_data(article.art_id, article.nzf.nzo.workpath)
@@ -163,14 +160,10 @@ class ArticleCache(object):
if nzo.is_gone():
# Do not discard this article because the
# file might still be processed at this moment!!
if sabnzbd.LOG_ALL:
logging.debug("%s is discarded", article)
return
art_id = article.get_art_id()
if art_id:
if sabnzbd.LOG_ALL:
logging.debug("Flushing %s to disk", article)
# Save data, but don't complain when destination folder is missing
# because this flush may come after completion of the NZO.
sabnzbd.save_data(data, art_id, nzo.workpath, do_pickle=False, silent=True)
@@ -179,14 +172,12 @@ class ArticleCache(object):
def __add_to_cache(self, article, data):
if article in self.__article_table:
self.__cache_size -= len(self.__article_table[article])
self.decrease_cache_size(len(self.__article_table[article]))
else:
self.__article_list.append(article)
self.__article_table[article] = data
self.__cache_size += len(data)
if sabnzbd.LOG_ALL:
logging.debug("Added %s to cache", article)
self.increase_cache_size(len(data))
# Create the instance

View File

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

View File

@@ -73,18 +73,6 @@ def last_month_day(tm):
return day
def this_month_day(t=None):
""" Return current day of the week, month 1..31 """
t = t or time.localtime(t)
return time.localtime(t).tm_mday
def this_week_day(t=None):
""" Return current day of the week 1..7 """
t = t or time.localtime(t)
return time.localtime(t).tm_wday + 1
def next_month(t):
""" Return timestamp for start of next month """
now = time.localtime(t)

View File

@@ -22,7 +22,7 @@ import re
import sabnzbd
from sabnzbd.constants import DEF_HOST, DEF_PORT, DEF_STDINTF, DEF_ADMIN_DIR, \
DEF_DOWNLOAD_DIR, DEF_NZBBACK_DIR, DEF_SCANRATE, DEF_COMPLETE_DIR
DEF_DOWNLOAD_DIR, DEF_NZBBACK_DIR, DEF_SCANRATE, DEF_COMPLETE_DIR, QUEUE_VERSION
from sabnzbd.config import OptionBool, OptionNumber, OptionPassword, \
OptionDir, OptionStr, OptionList, no_nonsense, \
@@ -74,6 +74,7 @@ email_endjob = OptionNumber('misc', 'email_endjob', 0, 0, 2)
email_full = OptionBool('misc', 'email_full', False)
email_dir = OptionDir('misc', 'email_dir', create=True)
email_rss = OptionBool('misc', 'email_rss', False)
email_cats = OptionList('misc', 'email_cats', ['*'])
version_check = OptionNumber('misc', 'check_new_rel', 1)
autobrowser = OptionBool('misc', 'auto_browser', True)
@@ -91,6 +92,7 @@ enable_7zip = OptionBool('misc', 'enable_7zip', True)
enable_recursive = OptionBool('misc', 'enable_recursive', True)
enable_filejoin = OptionBool('misc', 'enable_filejoin', True)
enable_tsjoin = OptionBool('misc', 'enable_tsjoin', True)
enable_par_cleanup = OptionBool('misc', 'enable_par_cleanup', True)
enable_all_par = OptionBool('misc', 'enable_all_par', False)
ignore_unrar_dates = OptionBool('misc', 'ignore_unrar_dates', False)
overwrite_files = OptionBool('misc', 'overwrite_files', False)
@@ -100,9 +102,7 @@ par_option = OptionStr('misc', 'par_option', '', validation=no_nonsense)
nice = OptionStr('misc', 'nice', '', validation=no_nonsense)
ionice = OptionStr('misc', 'ionice', '', validation=no_nonsense)
ignore_wrong_unrar = OptionBool('misc', 'ignore_wrong_unrar', False)
par2_multicore = OptionBool('misc', 'par2_multicore', True)
multipar = OptionBool('misc', 'multipar', sabnzbd.WIN32)
allow_streaming = OptionBool('misc', 'allow_streaming', False)
pre_check = OptionBool('misc', 'pre_check', False)
fail_hopeless_jobs = OptionBool('misc', 'fail_hopeless_jobs', True)
req_completion_rate = OptionNumber('misc', 'req_completion_rate', 100.2, 100, 200)
@@ -141,11 +141,12 @@ backup_for_duplicates = OptionBool('misc', 'backup_for_duplicates', True)
ignore_samples = OptionBool('misc', 'ignore_samples', False)
auto_sort = OptionBool('misc', 'auto_sort', False)
direct_unpack = OptionBool('misc', 'direct_unpack', False)
direct_unpack_tested = OptionBool('misc', 'direct_unpack_tested', False)
propagation_delay = OptionNumber('misc', 'propagation_delay', 0)
folder_rename = OptionBool('misc', 'folder_rename', True)
folder_max_length = OptionNumber('misc', 'folder_max_length', DEF_FOLDER_MAX, 20, 65000)
pause_on_pwrar = OptionNumber('misc', 'pause_on_pwrar', 1)
enable_meta = OptionBool('misc', 'enable_meta', True)
safe_postproc = OptionBool('misc', 'safe_postproc', True)
empty_postproc = OptionBool('misc', 'empty_postproc', False)
@@ -171,7 +172,7 @@ movie_categories = OptionList('misc', 'movie_categories', ['movies'])
enable_date_sorting = OptionBool('misc', 'enable_date_sorting', False)
date_sort_string = OptionStr('misc', 'date_sort_string')
date_categories = OptionStr('misc', 'date_categories', ['tv'])
date_categories = OptionList('misc', 'date_categories', ['tv'])
configlock = OptionBool('misc', 'config_lock', 0)
@@ -208,8 +209,9 @@ cache_limit = OptionStr('misc', 'cache_limit')
web_dir = OptionStr('misc', 'web_dir', DEF_STDINTF)
web_color = OptionStr('misc', 'web_color', '')
cleanup_list = OptionList('misc', 'cleanup_list')
warned_old_queue = OptionBool('misc', 'warned_old_queue9', False)
warned_old_queue = OptionNumber('misc', 'warned_old_queue', QUEUE_VERSION)
notified_new_skin = OptionNumber('misc', 'notified_new_skin', 0)
converted_nzo_pickles = OptionBool('misc', 'converted_nzo_pickles', False)
unwanted_extensions = OptionList('misc', 'unwanted_extensions')
action_on_unwanted_extensions = OptionNumber('misc', 'action_on_unwanted_extensions', 0)
@@ -243,6 +245,7 @@ fixed_ports = OptionBool('misc', 'fixed_ports', False)
# [ncenter]
ncenter_enable = OptionBool('ncenter', 'ncenter_enable', sabnzbd.DARWIN)
ncenter_cats = OptionList('ncenter', 'ncenter_cats', ['*'])
ncenter_prio_startup = OptionBool('ncenter', 'ncenter_prio_startup', True)
ncenter_prio_download = OptionBool('ncenter', 'ncenter_prio_download', False)
ncenter_prio_pp = OptionBool('ncenter', 'ncenter_prio_pp', False)
@@ -257,6 +260,7 @@ ncenter_prio_other = OptionBool('ncenter', 'ncenter_prio_other', False)
# [acenter]
acenter_enable = OptionBool('acenter', 'acenter_enable', sabnzbd.WIN32)
acenter_cats = OptionList('acenter', 'acenter_cats', ['*'])
acenter_prio_startup = OptionBool('acenter', 'acenter_prio_startup', False)
acenter_prio_download = OptionBool('acenter', 'acenter_prio_download', False)
acenter_prio_pp = OptionBool('acenter', 'acenter_prio_pp', False)
@@ -271,6 +275,7 @@ acenter_prio_other = OptionBool('acenter', 'acenter_prio_other', False)
# [ntfosd]
ntfosd_enable = OptionBool('ntfosd', 'ntfosd_enable', not sabnzbd.WIN32 and not sabnzbd.DARWIN)
ntfosd_cats = OptionList('ntfosd', 'ntfosd_cats', ['*'])
ntfosd_prio_startup = OptionBool('ntfosd', 'ntfosd_prio_startup', True)
ntfosd_prio_download = OptionBool('ntfosd', 'ntfosd_prio_download', False)
ntfosd_prio_pp = OptionBool('ntfosd', 'ntfosd_prio_pp', False)
@@ -285,6 +290,7 @@ ntfosd_prio_other = OptionBool('ntfosd', 'ntfosd_prio_other', False)
# [growl]
growl_enable = OptionBool('growl', 'growl_enable', False)
growl_cats = OptionList('growl', 'growl_cats', ['*'])
growl_server = OptionStr('growl', 'growl_server')
growl_password = OptionPassword('growl', 'growl_password')
growl_prio_startup = OptionBool('growl', 'growl_prio_startup', True)
@@ -301,6 +307,7 @@ growl_prio_other = OptionBool('growl', 'growl_prio_other', False)
# [prowl]
prowl_enable = OptionBool('prowl', 'prowl_enable', False)
prowl_cats = OptionList('prowl', 'prowl_cats', ['*'])
prowl_apikey = OptionStr('prowl', 'prowl_apikey')
prowl_prio_startup = OptionNumber('prowl', 'prowl_prio_startup', -3)
prowl_prio_download = OptionNumber('prowl', 'prowl_prio_download', -3)
@@ -319,6 +326,7 @@ pushover_token = OptionStr('pushover', 'pushover_token')
pushover_userkey = OptionStr('pushover', 'pushover_userkey')
pushover_device = OptionStr('pushover', 'pushover_device')
pushover_enable = OptionBool('pushover', 'pushover_enable')
pushover_cats = OptionList('pushover', 'pushover_cats', ['*'])
pushover_prio_startup = OptionNumber('pushover', 'pushover_prio_startup', -3)
pushover_prio_download = OptionNumber('pushover', 'pushover_prio_download', -2)
pushover_prio_pp = OptionNumber('pushover', 'pushover_prio_pp', -3)
@@ -333,6 +341,7 @@ pushover_prio_other = OptionNumber('pushover', 'pushover_prio_other', -3)
# [pushbullet]
pushbullet_enable = OptionBool('pushbullet', 'pushbullet_enable')
pushbullet_cats = OptionList('pushbullet', 'pushbullet_cats', ['*'])
pushbullet_apikey = OptionStr('pushbullet', 'pushbullet_apikey')
pushbullet_device = OptionStr('pushbullet', 'pushbullet_device')
pushbullet_prio_startup = OptionNumber('pushbullet', 'pushbullet_prio_startup', 0)
@@ -349,6 +358,7 @@ pushbullet_prio_other = OptionNumber('pushbullet', 'pushbullet_prio_other', 0)
# [nscript]
nscript_enable = OptionBool('nscript', 'nscript_enable')
nscript_cats = OptionList('nscript', 'nscript_cats', ['*'])
nscript_script = OptionStr('nscript', 'nscript_script')
nscript_parameters = OptionStr('nscript', 'nscript_parameters')
nscript_prio_startup = OptionBool('nscript', 'nscript_prio_startup', True)

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'):
@@ -791,7 +792,6 @@ def _read_config(path, try_backup=False):
def save_config(force=False):
""" Update Setup file with current option values """
global CFG, database, modified
if 0: assert isinstance(CFG, configobj.ConfigObj)
if not (modified or force):
return True
@@ -978,6 +978,21 @@ def define_rss():
def get_rss():
global database
try:
# We have to remove non-seperator commas by detecting if they are valid URL's
for feed_key in database['rss']:
feed = database['rss'][feed_key]
# Create a new corrected list
new_feed_uris = []
for feed_uri in feed.uri():
if new_feed_uris and not urlparse(feed_uri).scheme and urlparse(new_feed_uris[-1]).scheme:
# Current one has no scheme but previous one does, append to previous
new_feed_uris[-1] += '%2C' + feed_uri
continue
# Add full working URL
new_feed_uris.append(feed_uri)
# Set new list
feed.uri.set(new_feed_uris)
return database['rss']
except KeyError:
return {}
@@ -1065,15 +1080,6 @@ def validate_safedir(root, value, default):
return T('Error: Queue not empty, cannot change folder.'), None
def validate_dir_exists(root, value, default):
""" Check if directory exists """
p = sabnzbd.misc.real_path(root, value)
if os.path.exists(p):
return None, value
else:
return T('Folder "%s" does not exist') % p, None
def validate_notempty(root, value, default):
""" If value is empty, return default """
if value:
@@ -1084,12 +1090,6 @@ def validate_notempty(root, value, default):
def create_api_key():
""" Return a new randomized API_KEY """
import time
try:
from hashlib import md5
except ImportError:
from md5 import md5
import random
# Create some values to seed md5
t = str(time.time())
r = str(random.random())
@@ -1100,22 +1100,3 @@ def create_api_key():
# Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b
return m.hexdigest()
_FIXES = (
('enable_par_multicore', 'par2_multicore'),
)
def compatibility_fix(cf):
""" Convert obsolete INI entries """
for item in _FIXES:
old, new = item
try:
cf[new]
except KeyError:
try:
cf[new] = cf[old]
del cf[old]
except KeyError:
pass

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'
@@ -55,19 +54,14 @@ REPAIR_REQUEST = 'repair-all.sab'
SABYENC_VERSION_REQUIRED = '3.0.2'
DB_HISTORY_VERSION = 1
DB_QUEUE_VERSION = 1
DB_HISTORY_NAME = 'history%s.db' % DB_HISTORY_VERSION
DB_QUEUE_NAME = 'queue%s.db' % DB_QUEUE_VERSION
DEF_DOWNLOAD_DIR = 'Downloads/incomplete'
DEF_COMPLETE_DIR = 'Downloads/complete'
DEF_ADMIN_DIR = 'admin'
DEF_LOG_DIR = 'logs'
DEF_NZBBACK_DIR = ''
DEF_LANGUAGE = 'locale'
DEF_INTERFACES = 'interfaces'
DEF_INT_LANGUAGE = 'locale'
DEF_EMAIL_TMPL = 'email'
DEF_STDCONFIG = 'Config'
DEF_STDINTF = 'Glitter'
@@ -82,11 +76,7 @@ DEF_LOG_ERRFILE = 'sabnzbd.error.log'
DEF_LOG_CHERRY = 'cherrypy.log'
DEF_CACHE_LIMIT = '450M'
DEF_TIMEOUT = 60
MIN_TIMEOUT = 10
MAX_TIMEOUT = 200
DEF_LOGLEVEL = 1
DEF_SCANRATE = 5
DEF_QRATE = 0
MAX_DECODE_QUEUE = 10
LIMIT_DECODE_QUEUE = 100
MAX_WARNINGS = 20

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 """
@@ -531,7 +535,7 @@ def unpack_history_info(item):
if item['script_log']:
item['script_log'] = ''
# The action line is only available for items in the postproc queue
if not item.has_key('action_line'):
if 'action_line' not in item:
item['action_line'] = ''
return item

View File

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

View File

@@ -20,9 +20,7 @@
##############################################################################
from threading import RLock, Condition
NZBQUEUE_LOCK = RLock()
CV = Condition(NZBQUEUE_LOCK)
IO_LOCK = RLock()
DOWNLOADER_CV = Condition(RLock())
def synchronized(lock):
def wrap(f):
@@ -36,13 +34,13 @@ def synchronized(lock):
return wrap
def synchronized_CV(func):
global CV
def notify_downloader(func):
global DOWNLOADER_CV
def call_func(*params, **kparams):
CV.acquire()
DOWNLOADER_CV.acquire()
try:
return func(*params, **kparams)
finally:
CV.notifyAll()
CV.release()
DOWNLOADER_CV.notify_all()
DOWNLOADER_CV.release()
return call_func

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

@@ -99,7 +99,6 @@ def ProcessArchiveFile(filename, path, pp=None, script=None, cat=None, catdir=No
returns (status, nzo_ids)
status: -1==Error/Retry, 0==OK, 1==Ignore
"""
from sabnzbd.nzbqueue import add_nzo
nzo_ids = []
if catdir is None:
catdir = cat
@@ -145,7 +144,7 @@ def ProcessArchiveFile(filename, path, pp=None, script=None, cat=None, catdir=No
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo_id, add_to_history=False)
nzo.nzo_id = nzo_id
nzo_id = None
nzo_ids.append(add_nzo(nzo))
nzo_ids.append(sabnzbd.nzbqueue.NzbQueue.do.add(nzo))
nzo.update_rating()
zf.close()
try:
@@ -170,7 +169,6 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
returns (status, nzo_ids)
status: -2==Error/retry, -1==Error, 0==OK, 1==OK-but-ignorecannot-delete
"""
from sabnzbd.nzbqueue import add_nzo
nzo_ids = []
if catdir is None:
catdir = cat
@@ -231,7 +229,7 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
# Re-use existing nzo_id, when a "future" job gets it payload
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo_id, add_to_history=False)
nzo.nzo_id = nzo_id
nzo_ids.append(add_nzo(nzo, quiet=reuse))
nzo_ids.append(sabnzbd.nzbqueue.NzbQueue.do.add(nzo, quiet=reuse))
nzo.update_rating()
try:
if not keep:

View File

@@ -30,7 +30,7 @@ import sys
import Queue
import sabnzbd
from sabnzbd.decorators import synchronized, synchronized_CV, CV
from sabnzbd.decorators import synchronized, notify_downloader, DOWNLOADER_CV
from sabnzbd.constants import MAX_DECODE_QUEUE, LIMIT_DECODE_QUEUE
from sabnzbd.decoder import Decoder
from sabnzbd.newswrapper import NewsWrapper, request_server_info
@@ -252,12 +252,12 @@ class Downloader(Thread):
return
@synchronized_CV
@notify_downloader
def set_paused_state(self, state):
""" Set downloader to specified paused state """
self.paused = state
@synchronized_CV
@notify_downloader
def resume(self):
# Do not notify when SABnzbd is still starting
if self.paused and sabnzbd.WEB_DIR:
@@ -265,7 +265,7 @@ class Downloader(Thread):
notifier.send_notification("SABnzbd", T('Resuming'), 'download')
self.paused = False
@synchronized_CV
@notify_downloader
def pause(self, save=True):
""" Pause the downloader, optionally saving admin """
if not self.paused:
@@ -279,22 +279,20 @@ class Downloader(Thread):
if save:
ArticleCache.do.flush_articles()
@synchronized_CV
def delay(self):
logging.debug("Delaying")
self.delayed = True
@synchronized_CV
@notify_downloader
def undelay(self):
logging.debug("Undelaying")
self.delayed = False
@synchronized_CV
def wait_for_postproc(self):
logging.info("Waiting for post-processing to finish")
self.postproc = True
@synchronized_CV
@notify_downloader
def resume_from_postproc(self):
logging.info("Post-processing finished, resuming download")
self.postproc = False
@@ -302,7 +300,6 @@ class Downloader(Thread):
def disconnect(self):
self.force_disconnect = True
@synchronized_CV
def limit_speed(self, value):
''' Set the actual download speed in Bytes/sec
When 'value' ends with a '%' sign or is within 1-100, it is interpreted as a pecentage of the maximum bandwidth
@@ -356,7 +353,6 @@ class Downloader(Thread):
""" Return True when this server has the highest priority of the active ones
0 is the highest priority
"""
for server in self.servers:
if server is not me and server.active and server.priority < me.priority:
return False
@@ -421,7 +417,6 @@ class Downloader(Thread):
while 1:
for server in self.servers:
if 0: assert isinstance(server, Server) # Assert only for debug purposes
for nw in server.busy_threads[:]:
if (nw.nntp and nw.nntp.error_msg) or (nw.timeout and time.time() > nw.timeout):
if nw.nntp and nw.nntp.error_msg:
@@ -445,7 +440,6 @@ class Downloader(Thread):
# Restart pending, don't add new articles
continue
if 0: assert isinstance(server, Server) # Assert only for debug purposes
if not server.idle_threads or server.restart or self.is_paused() or self.shutdown or self.delayed or self.postproc:
continue
@@ -453,7 +447,6 @@ class Downloader(Thread):
continue
for nw in server.idle_threads[:]:
if 0: assert isinstance(nw, NewsWrapper) # Assert only for debug purposes
if nw.timeout:
if time.time() < nw.timeout:
continue
@@ -471,10 +464,11 @@ class Downloader(Thread):
break
if server.retention and article.nzf.nzo.avg_stamp < time.time() - server.retention:
# Article too old for the server, treat as missing
if sabnzbd.LOG_ALL:
logging.debug('Article %s too old for %s', article.article, server.id)
self.decode(article, None, None)
# Let's get rid of all the articles for this server at once
logging.info('Job %s too old for %s, moving on', article.nzf.nzo.work_name, server.id)
while article:
self.decode(article, None, None)
article = article.nzf.nzo.get_article(server, self.servers)
break
server.idle_threads.remove(nw)
@@ -553,11 +547,11 @@ class Downloader(Thread):
time.sleep(1.0)
CV.acquire()
DOWNLOADER_CV.acquire()
while (sabnzbd.nzbqueue.NzbQueue.do.is_empty() or self.is_paused() or self.delayed or self.postproc) and not \
self.shutdown and not self.__restart:
CV.wait()
CV.release()
DOWNLOADER_CV.wait()
DOWNLOADER_CV.release()
self.force_disconnect = False
@@ -714,7 +708,7 @@ class Downloader(Thread):
elif nw.status_code in ('411', '423', '430'):
done = True
logging.info('Thread %s@%s: Article %s missing (error=%s)',
logging.debug('Thread %s@%s: Article %s missing (error=%s)',
nw.thrdnum, nw.server.id, article.article, nw.status_code)
nw.clear_data()
@@ -866,7 +860,7 @@ class Downloader(Thread):
del self._timers[server_id]
self.init_server(server_id, server_id)
@synchronized_CV
@notify_downloader
@synchronized(TIMER_LOCK)
def unblock(self, server_id):
# Remove timer
@@ -885,7 +879,7 @@ class Downloader(Thread):
for server_id in self._timers.keys():
self.unblock(server_id)
@synchronized_CV
@notify_downloader
@synchronized(TIMER_LOCK)
def check_timers(self):
""" Make sure every server without a non-expired timer is active """
@@ -905,11 +899,10 @@ class Downloader(Thread):
logging.debug('Forcing activation of server %s', server.id)
self.init_server(server.id, server.id)
@synchronized_CV
def update_server(self, oldserver, newserver):
self.init_server(oldserver, newserver)
@synchronized_CV
@notify_downloader
def wakeup(self):
""" Just rattle the semaphore """
pass
@@ -920,12 +913,12 @@ class Downloader(Thread):
def stop():
CV.acquire()
DOWNLOADER_CV.acquire()
try:
Downloader.do.stop()
finally:
CV.notifyAll()
CV.release()
DOWNLOADER_CV.notify_all()
DOWNLOADER_CV.release()
try:
Downloader.do.join()
except:

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