Compare commits

...

179 Commits

Author SHA1 Message Date
ShyPike
281ed6766c Update text files for 0.7.4 RC2 2012-09-27 20:38:16 +02:00
shypike
cd78c89de1 Update translations 2012-09-27 20:37:14 +02:00
ShyPike
c6c983e8f2 Linux: add memory usage to status display and add special option to control display.
Show "Total Program size" (V) and "Resident set size" (R).
Option "show_sysload" has three values:
0 = Off
1 = Show CPU load
2 = Show CPU load + memory usage (default)
2012-09-26 20:44:18 +02:00
ShyPike
ef4d1ce54f Fix problems with timing of quota reset at end of period. 2012-09-25 22:51:36 +02:00
ShyPike
b1177f4265 Fix issue with queue and history not updating in Safari-IOS6.
Add "Cache-Control":"no-cache" header to each Ajax POST action.
2012-09-24 21:38:30 +02:00
shypike
02d373e4a6 Remove warning about Growl when user has disabled message class in Growl itself.
Growl 2.0 uses different text than 1.4.
Test is now compatible with both versions.
2012-09-24 17:31:41 +02:00
shypike
58c8608667 Pre-check didn't check all available par2 files.
Result was that any post with a missing article was rejected.
Regression error introduced when fixing stalling fetches of extra par2 files.
2012-09-23 14:35:27 +02:00
ShyPike
848110ac3e Fix transmission of Growl icon when running on Linux/Unix.
There's a bug in the GNTP library that prevents sending the icon as
a binary attribute along with the registration.
Work-around is to send an URL to an external location.
2012-09-22 14:37:21 +02:00
ShyPike
74d677cf09 Update text files for 0.7.4 RC1. 2012-09-22 10:19:15 +02:00
ShyPike
f029c4eb4f Add a default limit to API-call "history" in order to prevent over-sized output.
Some external tools query the full history repeatedly.
Add a default value for the API call "history" as a work-around.
Calls that have a start and limit parameter will work unchanged.
The default limit can be set in the Special "history_limit".
2012-09-22 10:16:31 +02:00
ShyPike
838811f085 Update text files for 0.7.4 RC1 2012-09-21 20:50:40 +02:00
ShyPike
e212ec7ca3 Remove potential stalling when fetching extra par fetches.
Assembler didn't set the "completed" flag on a finished file,
leading to potential deadlock when fetching extra par2 files.
2012-09-21 20:47:25 +02:00
ShyPike
6314a536af Windows: be less eager to run par2-classic instead of the multi-core version. 2012-09-21 20:46:43 +02:00
shypike
df33765ce0 Update translations 2012-09-21 20:39:20 +02:00
ShyPike
2524333e79 Update text files for 0.7.4 RC1 2012-09-20 21:04:42 +02:00
ShyPike
cf81e815ee Classic and Plush skins must specify their character encoding.
Some browsers get it wrong when they need to guess the encoding.
Added:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
2012-09-19 20:29:26 +02:00
ShyPike
7bd7f1826c Win32: fix display of system name in startup notification message. 2012-09-19 20:00:19 +02:00
shypike
9ce7b528e3 Improved OSX DMG template. 2012-09-19 19:49:41 +02:00
shypike
db7b3cf0b5 Update translations 2012-09-18 21:52:26 +02:00
ShyPike
201b30b6c4 Fix packaging for Windows and add tar.gz for OSX again. 2012-09-18 20:47:57 +02:00
ShyPike
36a6bcd1f5 Update text files for 0.7.4 Beta3 2012-09-17 21:00:37 +02:00
shypike
20cfca5bb6 Combine OSX builds into one DMG image.
Split DMG creation off into a separate file.
`package.py` will now just build one OSX platform.
`make_dmg.py` will combine the three builds into one DMG.
2012-09-17 20:55:46 +02:00
ShyPike
63fe4b15c8 Show correct base folder for HTTPS certificate files (corrected). 2012-09-17 19:43:31 +02:00
ShyPike
aabc57a1f8 Show correct base folder for HTTPS certificate files. 2012-09-17 19:34:19 +02:00
ShyPike
9a8eca0993 Some code cleanup. 2012-09-17 19:16:22 +02:00
ShyPike
f8723d7e52 Prevent potential crash due to missing attribute in NzfObject. 2012-09-14 22:10:03 +02:00
ShyPike
59bb5528ed Ignore new schedule without days instead of crashing on it. 2012-09-14 21:46:36 +02:00
ShyPike
247c10692a Put "missing ssl support" warning in Wizard page One too. 2012-09-10 22:09:40 +02:00
shypike
63bed3c127 Update text files for 0.7.4Beta2 2012-09-10 19:27:58 +02:00
shypike
ca96743bad Update translations 2012-09-10 19:07:53 +02:00
shypike
525fb4de61 Update POT file, for new texts. 2012-09-10 19:06:54 +02:00
shypike
f294084dbc Build three separate DMG files for OSX.
Lion and MountainLion use the most recent native Python.
Leopard/SnowLeopard uses ActiveState Python 2.6.3.
Use a separate template for each OS version.
2012-09-09 22:19:10 +02:00
shypike
ca1327a9ae Improve handling of badly formatted subject lines in NZBs.
The NZB parser overwrote the subject even when there was no proper filename extracted.
This compromized recognition of par2 files after a first failed verification run.
Improve ability to extract a proper file name so that sorting and displaying is better.
The extraction still won't work for every malformed subject, but at least the par2 recognition should always work now.
2012-09-09 14:49:30 +02:00
shypike
dcb1b0b3dc Improve startup notification.
- Move hostname() from misc.py to growler.py.
- Improve startup notification
2012-09-08 21:00:28 +02:00
shypike
ec4b613498 Modify growler.py for new GNTP module. Fix transmission of icon.
- use 'localhost' for local Growl instead of "None".
- Sending binary icon now works properly, so external icon not needed any more.
- No need to send icon with every message, only when registering.
- Log more info about gntp-error exceptions
2012-09-08 20:23:59 +02:00
shypike
c3f4eccfbc Re-apply custom patches to new GNTP.
Reduce logging verbosity for sent messages.
Convert "info" logging to "debug" logging.
Suppress 404 + "user has disabled" error (which isn't really an error).
Time-out patch no longer needed, built-in now.
2012-09-08 20:18:13 +02:00
shypike
1aafe25a83 Update GNTP module.
Version 0.8 from github.
2012-09-08 19:52:46 +02:00
shypike
cc25ef0af0 Prevent filing an error message when the user has disabled message classes in Growl-preferences.
Problem is in the GNTP library, while at the same time it is very odd that Growl sends an error response for this.
2012-09-08 15:45:17 +02:00
shypike
5c221f4a14 After successful pre-check, preserve a job's spot in the queue.
Previously a pre-checked job re-entered at the end of the queue.
Moved the quality check from the post-processor to nzbqueue, so a successful job doesn't need to go through the post-processor.
Improvement: all other ways for a job to be sent to post-processing need to go through the end_job() method.
2012-09-08 15:01:14 +02:00
shypike
03221fc645 Config skin: "check new releases" setting did not work properly. 2012-09-08 09:46:40 +02:00
ShyPike
febf81e597 Update text files for 0.7.4Beta1 2012-09-06 21:53:52 +02:00
shypike
447ec55822 OSX: create one build for ML and one for others.
Also remove 64bit code from binaries.
2012-09-06 21:51:12 +02:00
ShyPike
eee1f49c4a Fix translation of notification classes in Config->Notification. 2012-09-05 20:00:33 +02:00
shypike
140b903783 Update translations 2012-09-05 19:43:57 +02:00
ShyPike
8361bc9f3a A job with priority "forced" should keep that when fetching more par2 files.
It was now set to "repair" priority, which doesn't ignore paused state.
Other priorities will still be promoted to "repair".
2012-09-04 19:10:13 +02:00
ShyPike
744290c228 Log failed attempts to login to the Web UI. 2012-09-03 23:18:08 +02:00
ShyPike
20768df430 Remove leak of SQLite database handles.
The non-UI threads need to close the db handles after using.
Move the unnecessary proxy for the database sizes for the bpsmeter.
2012-09-03 22:35:41 +02:00
ShyPike
7ec7e8d432 Allow compression of more MIME type in API-calls. 2012-09-03 19:30:29 +02:00
shypike
7b657a85ba Update POT files. 2012-09-01 13:23:04 +02:00
ShyPike
258699f1db Add scheduled task "Remove failed jobs". 2012-09-01 12:43:33 +02:00
Lucas Parry
0412f45323 Make scheduler more flexible
Schedules can now be set for any arbitrary group of days of the week.
2012-09-01 12:09:11 +02:00
shypike
997eb93cd9 Don't do an SFV-based verification when the job is already marked as verified.
When retrying a job that has passed succesful verification, the par2 files will already have been removed. When doing an SFV-based verification, this may lead to errors because the PAR2 files are gone. So when the "__verified__" flag-file is present and there's no par2-set, skip the SFV check.
2012-09-01 11:42:41 +02:00
shypike
b94192486b Add special option "overwrite_files".
This will overwrite existing files in the "complete" folders.
Useful for effective in "Single folder" mode and "Season Sorting".
2012-08-29 22:08:07 +02:00
shypike
73bdd2c5bf Prevent error and pause when last file of a removed job happens to be written to (and fails). 2012-08-25 12:49:07 +02:00
shypike
56e9b54cd9 Add parameter "pp_active" to the "qstatus" api call, showing state of Postprocessing. 2012-08-25 11:13:51 +02:00
shypike
3308074f81 Don't also show job in post-processing queue while fetching more par2 files. 2012-08-23 22:24:46 +02:00
shypike
c2305034a1 Handle unexpected missing par2 file in par2-check properly. 2012-08-23 20:06:44 +02:00
shypike
d77b22be37 Add support for HTTPS chain certificate file.
Just enabling the user to specify the file and then passing it on to CherryPy.
2012-08-18 14:51:21 +02:00
ShyPike
9717912ff7 When a numeric option has no value, it should be set to the default and not to the minimum value. 2012-08-16 13:47:29 +02:00
ShyPike
cfa79c08b2 When retrying post processing don't attempt to download existing par2 files again. 2012-08-16 12:10:48 +02:00
ShyPike
4f65f87ad6 Fix syntax of command used in server test. 2012-08-16 11:37:57 +02:00
shypike
aea0d21fd2 Log the output of the PAR2 command.
This will help diagnosing repair trouble that users might have.
2012-08-15 19:17:43 +02:00
shypike
ba77b43364 Default for Growl is off when on MountainLion. 2012-08-15 18:36:41 +02:00
shypike
7727eb58a8 Sort 'Done' list on RSS page in reverse time order (latest first). 2012-08-15 18:31:16 +02:00
shypike
dbd2e3f54b Make list of RSS sites that use weird titles a "Special" option, "misc, rss_odd_titles". 2012-08-15 18:13:41 +02:00
shypike
7ac6e07576 Update translatable texts 2012-08-14 20:42:34 +02:00
shypike
24ffd90fb4 sleepless.keep_awake() call needs a Unicode argument 2012-08-14 20:37:57 +02:00
shypike
011b680337 Fix path for OSX-Leopard build 2012-08-11 17:18:22 +02:00
shypike
5328e07a93 OSX: add notifier package and licencse files to DMG. 2012-08-11 14:37:37 +02:00
shypike
2d07af7cc8 Add parameter to sleepless.keep_awake() call. 2012-08-11 13:23:32 +02:00
shypike
a40d2da2ab Improve OSX Mountain Lion "stay awake" support with native OS calls.
Also add Special option "keep_awake" for Windows and OSX.
2012-08-10 22:49:41 +02:00
shypike
25ac101751 Add OSX Notification Center support.
Add option when support available on OSX ML.
Add class selection for notifications. Useful for N-Center and NtfOSD.
2012-08-09 22:55:07 +02:00
ShyPike
a666165a5e Plush: remove mailto link in Help pop-up. 2012-08-04 13:44:22 +02:00
ShyPike
e24aedc6ac Update text files for 0.7.3 Final. 2012-08-04 11:05:58 +02:00
ShyPike
d10d69e44b Correct typo in dirscanner.py. 2012-08-04 10:54:40 +02:00
ShyPike
af5c01ee3a Update Windows install size. 2012-08-03 22:58:34 +02:00
ShyPike
bf350cddc8 Update text files for 0.7.3
Fix package.py for README.mkd handling.
2012-08-03 22:28:44 +02:00
ShyPike
9bc4d909b5 Use 'pandoc' to generate README.rtf from README.mkd. 2012-08-03 21:05:08 +02:00
ShyPike
42236de5bd Update text files for 0.7.3 Final 2012-08-03 19:41:17 +02:00
ShyPike
02e2fe2cc8 Correct errors in Danish email templates. 2012-08-03 19:27:30 +02:00
ShyPike
b75bcb90f4 Server 502 message about expired subscription will now block server. 2012-08-03 19:08:21 +02:00
shypike
56b88eb406 Update translations 2012-08-03 16:01:20 +02:00
ShyPike
93741ea9ab SFV-check: don't issue a warning for each problem file, but just list in History entry. 2012-08-03 11:57:13 +02:00
ShyPike
f7509132fc Rename 'random_server_ip' to 'randomize_server_ip' and make the default 'False'.
This is to avoid confusing speed loss with some providers. Now the user has to enable this consciously.
2012-08-02 21:18:03 +02:00
ShyPike
2d0d62ec00 The "watched folder" scan should ignore all files starting with a period (hidden on Linux and OSX). 2012-08-01 00:27:23 +02:00
ShyPike
4e07a84102 Update POT files. 2012-08-01 00:25:35 +02:00
ShyPike
32a048a879 Update text files for 0.7.3 Beta2 2012-07-30 16:08:48 +02:00
shypike
caac28fcbc Try to keep OSX Mountain Lion awake as long as downloading/postprocessing runs.
Quick & Dirty solution by launching 'caffeinate' every 5 minutes.
2012-07-30 15:40:13 +02:00
ShyPike
5b0bbf57c6 Prevent queue deadlock in case of fatally damaged par2 files.
Basic problem: prevent par2 files that have already been downloaded from re-entering the queue.
The issue was introduced after removing the habit of 0.6.x to download a part of each par2 file.
The advantage of the 0.6.x method was that more was known about the par2 files,
the disadvantage that quite some data was downloaded that was never used.

Add job flag that prevents further QuickCheck attempts on known to be damaged jobs.
The method of inspecting large par2 files is time-consuming if nothing is ever found.
2012-07-30 12:41:55 +02:00
ShyPike
072af938c2 Prevent deadlock in fetching more par2 files when first par2 file is so corrupt that no info can be retrieved. 2012-07-29 14:48:27 +02:00
ShyPike
9e8202371e Add filter-enable check-boxes to Classic, Plush and smpl skinms.
Necessary because new filters created with those skins were not enabled.
2012-07-29 00:09:53 +02:00
ShyPike
27dd253c5d Fix UI crash on saving parameters of an already enabled server in Config->Servers. 2012-07-28 23:35:12 +02:00
ShyPike
8d651af2f8 Add some forward compatibility with changes coming to Scheduler in 0.8.x 2012-07-28 14:05:27 +02:00
ShyPike
6358312272 Replaced a few missed strings to be substituted with constants.Status values. 2012-07-24 21:35:03 +02:00
ShyPike
42c8367e13 Extend "check for release" with option "Also test releases". 2012-07-24 20:38:44 +02:00
ShyPike
c1e38b5e81 Update text files for 0.7.3 Beta1 2012-07-24 15:06:49 +02:00
ShyPike
1b4ce24037 Config->Servers: optimize layout 2012-07-24 11:17:40 +02:00
ShyPike
cf440750b6 Config->Servers: hide server details initially and have a simple checkbox to enable/disable servers quickly. 2012-07-24 09:14:02 +02:00
ShyPike
30c480df36 Add unofficial support for nzbmatrix's adult cousin. 2012-07-23 14:29:02 +02:00
ShyPike
9c3dbd39ef Make detection of samples less aggressive. 2012-07-22 16:35:34 +02:00
ShyPike
1af2f92828 Improve the Sort functions.
- Renaming secondary files after the main file didn't always work
- Collapsing folder structures could fail
- Joined (instead of unpacked) media files were not seen as candidates for renaming
- Presence of DVD/BRD special folders will now block collapsing
- Non-functional improvements: coding std, removed unused code and needless access functions, added doc-strings.
2012-07-22 15:43:26 +02:00
ShyPike
45277bb00f Add another keyword for canceled article detection. 2012-07-20 19:31:31 +02:00
ShyPike
10e21a3af9 Remove extra '\r' in Windows logging. 2012-07-20 19:16:35 +02:00
ShyPike
19cbadd420 Plush: fix odd formatting of multi-line warnings on Status page. 2012-07-20 19:11:52 +02:00
ShyPike
a28cbe52b9 Recovery from corrupt totals9.sab doesn't always remove bad entry. 2012-07-19 21:00:01 +02:00
shypike
37f1d64e46 Fix incorrect explanation of --no_ipv6 flag. 2012-07-18 18:00:39 +03:00
ShyPike
7170325df5 Update text files for 0.7.2 Final 2012-07-17 21:46:17 +02:00
ShyPike
c44d98da66 When localhost resolves to ['127.0.0.1', '::1'] instead of ['::1', '127.0.0.1'], SABnzbd would try to register twice on '::1'. 2012-07-17 21:29:33 +02:00
ShyPike
018410afb0 Validate the values read from totals9.sab 2012-07-17 19:38:49 +02:00
ShyPike
fc47238a7a Temporary fix for missing NZB icon when associating NZB files on a system that had a 0.7.0 release installed. 2012-07-16 18:51:39 +02:00
ShyPike
9561b8a64e Update text files for 0.7.2 RC2 2012-07-14 17:01:07 +02:00
ShyPike
7b0e56b55f Fix little anomaly in Wizard-Four. 2012-07-14 15:16:30 +02:00
ShyPike
c6d5a79776 Improve web-host address selection on systems where the hostname does not resolve to an IP address. 2012-07-14 14:27:18 +02:00
ShyPike
faa4cacd3e Improve detection of bad articles.
For now, don't let precheck use "HEAD" until method is improved.
2012-07-14 13:12:22 +02:00
ShyPike
56e417eea1 Change nzbsrus support to use their API response codes. 2012-07-14 09:40:46 +02:00
ShyPike
5f02ec00f9 Don't show nonsense NZB age when still trying to get it from an indexer. 2012-07-13 21:29:37 +02:00
ShyPike
5ea35db922 Add limited API support for nzbsrus.com 2012-07-13 21:19:09 +02:00
ShyPike
5dcf26a56c Update text files for 0.7.2RC1 2012-07-12 21:45:13 +02:00
ShyPike
35b598d10e Improve detection (and ignoring) of invalid articles.
This also requires reading headers during pre-check instead of just asking for article presence.
2012-07-12 21:24:38 +02:00
ShyPike
5e7b27c4ef Prevent potential crash in NzbQueue.repair_job() 2012-07-12 18:28:17 +02:00
ShyPike
9ed408d35b Windows Installer: don't uninstall settings by default. 2012-07-12 18:19:19 +02:00
ShyPike
6c782fe255 Fix nzbrus.com fatal error (when <nzb is not within the first 100 characters of the file). 2012-07-10 23:01:11 +02:00
ShyPike
1689323dc3 Fix sorting of files in a job so that .rar comes before .r00 again. 2012-07-10 22:12:04 +02:00
ShyPike
a3c50a907a Fix wrapping problem of queue titles in Plush skin by inserting zero-width spaces in titles. 2012-07-10 19:59:59 +02:00
ShyPike
36a3792846 After setting quota for the first time, the initial "quota left" was set to the already consumed amount of the current period, instead of the actual still available amount (which is quota-consumed). 2012-07-08 11:45:09 +02:00
ShyPike
4cd0c0691a Windows installer: fix NZB association so that a Windows reboot is not required to register the NZB icon. 2012-07-08 11:33:57 +02:00
ShyPike
6ac98dcacd Handle incorrect regular expression in RSS filters.
Ignore during scanning and signal in user interface.
2012-07-08 10:45:57 +02:00
ShyPike
0a0d00930a Update text files for 0.7.1 Final. 2012-07-06 16:50:00 +02:00
ShyPike
28a0d041f9 Correction on fix 7258e56a20.
nzf.completed is a @property attribute and should not be set directly.
2012-07-06 16:40:36 +02:00
ShyPike
85bb91a7ea Disable VC90 check in Windows Installer as long as we're still on Python 2.5 2012-07-04 21:10:49 +02:00
ShyPike
6561e0abfa Make Windows path joining of base and category paths more robust to work around different behaviors in Python and Windows versions combinations. 2012-07-04 20:36:52 +02:00
ShyPike
6715e61a68 Try to check for "maintenance" mode of nzbsrus.com 2012-07-04 19:34:00 +02:00
ShyPike
a886b284b6 Prevent "not committed" flag in baseline variable for official builds. 2012-07-04 19:09:55 +02:00
ShyPike
a349c82b6f Update text files for 0.7.1RC5 2012-07-03 21:38:20 +02:00
ShyPike
1f4df0ebf4 Only send "bad fetch" emails when emails are enabled. 2012-07-03 21:28:59 +02:00
ShyPike
0221e7bf93 Add some support for nzbsrus.com's awkward non-VIP limiting. 2012-07-03 21:14:30 +02:00
ShyPike
f9cf14e7d8 Update text files for 0.7.1 Final. 2012-07-03 19:59:36 +02:00
ShyPike
7258e56a20 When retrying a job, existing files were not flagged properly as "completed", leading to an endless loop in par2 fetching. 2012-07-03 19:58:02 +02:00
ShyPike
90bd495d44 Update text files fr 0.7.1 Final. 2012-07-03 17:54:18 +02:00
shypike
6c216d6dfe Update translations 2012-07-03 17:49:10 +02:00
ShyPike
e1f3fae6c7 Windows installer: detect incompatible older version by looking for python27.dll. 2012-07-02 22:31:28 +02:00
ShyPike
29f126ca47 Make warning about Python3 easier to read. 2012-07-02 22:13:01 +02:00
shypike
8b4b742466 Remove remaining .py files from OSX DMG image, to prevent invalid signatures after first run. 2012-07-02 18:44:36 +02:00
ShyPike
57a9d362bc Update text files for 0.7.1RC4 2012-07-01 17:26:00 +02:00
ShyPike
b7d54c2bea Fix failure to grab NZBs from indexers that send compressed files. 2012-07-01 17:23:16 +02:00
ShyPike
59f9833076 Update text files for 0.7.1 RC3 2012-07-01 14:03:59 +02:00
ShyPike
8e360fe53e Improve retry handling of URL fetches.
Add detection of error messages from nzbsrus.com and handle retries accordingly.
2012-07-01 13:49:24 +02:00
ShyPike
afc5005382 Make sure that badly formatted or otherwise incorrect articles are retried on other servers (if fail_on_crc option is on).
Make default of fail_on_crc True.
2012-07-01 13:47:55 +02:00
ShyPike
3a531c6d2b API calls "addurl" and "addid" can use the same code so that either will take newzbin IDs or regular URLs. 2012-07-01 12:53:54 +02:00
ShyPike
f056ad6347 Solve problem of stalling par2 fetching.
The attributes of the vol-par files were analyzed to late,
leading to a race condition.
2012-07-01 10:01:56 +02:00
ShyPike
5c1342a663 Tell user that Python 3.0 won't work. 2012-07-01 09:12:25 +02:00
ShyPike
dfe8a47a2a Verification/repair would not be executed properly when one more RAR files missed their first article. 2012-06-26 23:26:38 +02:00
ShyPike
e293a439dd Don't set __verified__ flag file when more par2 files need to be fetched. 2012-06-26 22:24:01 +02:00
ShyPike
7e0027922a Prevent QuickCheck crash when expected par2 file wasn't downloaded (due to missing articles). 2012-06-26 19:12:56 +02:00
shypike
00b5302ba9 Update text files for 0.7.1 Final. 2012-06-25 22:40:39 +02:00
shypike
79488c4785 Add option to suppress listening on web host address ::1 2012-06-25 22:34:07 +02:00
ShyPike
c3d0438250 Update texts for 0.7.1 RC2 2012-06-23 12:31:14 +02:00
ShyPike
2909d4636b Fix parameter bug in Swedish translation. 2012-06-23 12:28:57 +02:00
shypike
a1ee8b6af4 Update translations 2012-06-23 12:20:05 +02:00
ShyPike
cfe3b58f7f On Windows, the Python runtime starts the wrong browser when using http://::1:8080/sabnzbd. Convert URL to http://[::1]:8080/sabnzbd. 2012-06-23 12:05:38 +02:00
ShyPike
ff6b87ef5b Improve the INI file handling.
Simplify and make the save more robust.
Use the INI.BAK file when the INI file is missing or corrupt.
2012-06-21 21:46:14 +02:00
ShyPike
8fbcfd0d5a Update text files for 0.7.1RC1 2012-06-20 20:54:53 +02:00
shypike
347ba999b4 Reduce amount of info requested when updating Windows Tray icon and OSX top menu.
The Windows Tray icon queried for free diskspace, while it is never shown.
The OSX top menu should query for no more queue entries than it will show (10).
The OSX top menu queue didn't show accented characters properly.
2012-06-20 20:48:55 +02:00
ShyPike
503bcf64c9 Prevent problems on systems that don't bother to resolve "localhost". 2012-06-19 22:31:47 +02:00
ShyPike
2a667470a1 Add retries when creating "final folder" to allow for slow activation of mounted volumes. 2012-06-19 19:05:33 +02:00
ShyPike
c61165b840 Plush: fix Speedlimit, Pause and Options pulldown menus for Mobile Safari. 2012-06-18 22:17:09 +02:00
ShyPike
3672189bc8 Remove another unwanted Windows DLL when building on Windows 7. 2012-06-16 13:41:44 +02:00
ShyPike
5b38c772fb When the "uname -n" name of a system doesn't resolve to an IP, only use 0.0.0.0 as hostname when the user hadn't set "localhost", "127.0.0.1" or "::1" as the hostname.
Otherwise any such system will still listen on the external IP despite the fact that user didn't want this.
2012-06-16 12:18:12 +02:00
ShyPike
f28bc4dd9c Windows: the installer did not set an icon for NZB files (association). 2012-06-16 10:45:04 +02:00
ShyPike
e16cc49a17 Config-skin: "server" field in Config->Servers should have html5 tag "text" instead of "url". 2012-06-16 10:44:48 +02:00
ShyPike
f686cc94fd Modify server test to please very critical Usenet server. 2012-06-16 10:44:32 +02:00
ShyPike
d88b5a3b3e Restore proper support for Python 2.5 on Windows by including curl tool again. 2012-06-10 15:35:56 +02:00
shypike
bccc5665f5 Prevent Growl crash at shutdown, due to accented characters in message. 2012-06-10 15:03:56 +02:00
ShyPike
69ac9d39ad Update text files for release 0.7.0Final. 2012-06-09 10:36:31 +02:00
ShyPike
b5b6999bc9 Default value of cache_limit should remain empty. 2012-06-09 10:25:09 +02:00
shypike
7ddb3d2752 Update translations 2012-06-08 18:49:31 +02:00
ShyPike
9febaf919c Update translations. 2012-06-06 17:34:12 +02:00
shypike
2225383485 Removed gntp and gnutext as required modules, because they are included in the distribution. 2012-06-06 17:36:32 +03:00
shypike
e3e500326c Update translations 2012-06-05 19:16:06 +02:00
88 changed files with 13801 additions and 12288 deletions

View File

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

View File

@@ -1,3 +1,151 @@
-------------------------------------------------------------------------------
0.7.4RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Pre-check failed to consider extra par2 files
- Fixed unjustified warning that can occur with OSX Growl 2.0
- Show memory usage on Linux systems
- Fix incorrect end-of-month quota reset
- Fix UI refresh issue when using Safari on iOS6
-------------------------------------------------------------------------------
0.7.4RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Remove potential queue stalling when downloading extra par2 files
- Make Windows version less eager to use par2-classic
- Fixed DMG images
- Add missing encoding directive to Plush and Classic skins
- Prevent oversized data in API-call "history"
-------------------------------------------------------------------------------
0.7.4Beta3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- All three OSX build in one DMG again
- Minor bugfixes
-------------------------------------------------------------------------------
0.7.4Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix failure to fetch more par2-files for posts with badly formatted subject lines
- After successful pre-check, preserve a job's position in the queue
- Restore SABnzbd icon for Growl
- Fix "check new releases" option in Config skin
- Separate DMG files for OSX Leopard/SL, Lion and MLion
-------------------------------------------------------------------------------
0.7.4Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- OSX Mountain Lion Notification Center support
- OSX Mountain Lion improved "keep awake" support
- OSX: separate builds: one for Mountain Lion and one for all others
- OSX removed 64bit code
- Scheduler: action can now run on multiple weekdays
- Scheduler: add "remove failed jobs" action
- Special option: rss_odd_titles (see Wiki)
- Support for HTTPS chain files (needed when you buy your own certificate)
- Prevent jobs from showing up in queue and history simultaneously
- Add parameter 'pp_active' to history elements in qstatus API call
- Fix some minor par2 handling bugs
- Prevent potential crash when an actively downloading job is deleted from the queue
- Special option: 'overwrite_files' (See Wiki)
- Don't try an SFV check when a retried job was already successfully verified by par2
- Enable compression of API call results
- Log failed attempts to log in to the Web UI
- A job with "forced" priority should keep that when fetching more par2 files
- Updated translations
-------------------------------------------------------------------------------
0.7.3Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Rename Special "random_server_ip" to "randomize_server_ip" so that we
can force the default to "Off". "On" kills speed on some servers.
- Ignore pseudo NZB files that start with a period in the name
- SFV failure now listed in History instead of issuing warnings
- Translation updates
- "502" errors about payments/credits will now block a server
-------------------------------------------------------------------------------
0.7.3Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Try to keep OSX Mountain Lion awake as long as downloading/postprocessing runs
- Prevent queue deadlock in case of fatally damaged par2 files
- Add RSS filter-enable checkboxes to Plush, Smpl and Classic skins
- Fix problem with saving modified paramters of an already enabled server
- Extend "check new release" option with test releases
-------------------------------------------------------------------------------
0.7.3Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Correct several errors in Sort function
- Improve organization of Config->Servers
- Support for nzbxxx.com
- Make detection of samples less aggressive
- Some minor corrections
-------------------------------------------------------------------------------
0.7.2Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix for NZB-icon issue when 0.7.0 was previously installed
- Check validity of totals9.sab file
- Fix startup problem when localhost has unexpected order of IP addresses
-------------------------------------------------------------------------------
0.7.2RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Improve support for nzbsrus.com
- Don't try to show NZB age when not known yet
- Prevent systems with unresolvable hostnames from always using 0.0.0.0
-------------------------------------------------------------------------------
0.7.2RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix fatal error in nzbsrus.com support
- Initial "quota left" was not set correctly when enabling quota
- Report incorrect RSS filter expressions (instead of aborting analysis)
- Improve detection of invalid articles (so that backup server will be tried)
- Windows installer: improve NZB association so that a reboot isn't needed
- Windows installer: don't remove settimngs by default when uninstalling
- Fix sorting of rar files in job so that .rar preceeds .r00
-------------------------------------------------------------------------------
0.7.1Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Disable VC90 check in Windows Installer as long as we're still on Python 2.5
- Windows: make sure \\server\share notation is never seen as a relative path
-------------------------------------------------------------------------------
0.7.1RC5 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix signing of OSX DMG
- Fix endless par2-fetch loop after retrying failed job
- Don't send "bad fetch" email when emailing is off
- Add some support for nzbrus.com's non-VIP limiting
-------------------------------------------------------------------------------
0.7.1RC4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix failure to grab NZBs from indexers that send compressed files.
-------------------------------------------------------------------------------
0.7.1RC3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed stalling par2 fetches (after first verification run)
- Fixed retry behaviour of NZB fetching from URL
and add handling of nzbsrus.com error codes
- Make sure that all malformed articles are retried on another server
- Add no_ipv6 option that suppresses listing on ::1
(to be used if your system cannot handle that)
- Prevent crash in QuickCheck when expected par2 file wasn't downloaded
- Verification/repair would not be executed properly when one more RAR files
missed their first article.
- API calls "addurl" and "addid" (newzbin) can be used interchangeably
(Fixes a problem in Qouch)
-------------------------------------------------------------------------------
0.7.1RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Improved backup of sabnzbd.ini file
Will use backup when original is gone or become corrupt
- Windows: Using ::1 as single webhost address would start IE instead of default browser
-------------------------------------------------------------------------------
0.7.1RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Plush skin: fix problems with pull-down menus in Mobile Safari
- On some Linux and OSX systems using localhost would still make SABnzbd
give access to other computers
- Windows: the installer did not set an icon when associating NZB files with SABnzbd
- Fix problem that the Opera browser had with Config->Servers
- Retry a few times when accessing a mounted drive to create the
final destination folder
- Reduce load caused by WinTray and OSX topmenu
-------------------------------------------------------------------------------
0.7.0Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Updated translations
-------------------------------------------------------------------------------
0.7.0RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------

View File

@@ -1,4 +1,4 @@
SABnzbd 0.7.0
SABnzbd 0.7.4
-------------------------------------------------------------------------------
0) LICENSE
@@ -75,8 +75,6 @@ Optional modules
unzip >= 5.52 http://www.info-zip.org/
yenc module >= 0.3 http://sabnzbd.sourceforge.net/yenc-0.3.tar.gz
http://sabnzbd.sourceforge.net/yenc-0.3-w32fixed.zip (Win32-only)
gnu gettext http://www.gnu.org/software/gettext/
gntp https://github.com/kfdm/gntp/ (or use "pypm install gntp")
Optional modules Windows
pyopenssl >= 0.11 http://pypi.python.org/pypi/pyOpenSSL

View File

@@ -21,6 +21,7 @@
!include "MUI2.nsh"
!include "registerExtension.nsh"
!include "FileFunc.nsh"
!include "LogicLib.nsh"
!include "WinVer.nsh"
!include "WinSxSQuery.nsh"
@@ -84,6 +85,7 @@
RMDir /r "${idir}\interfaces\wizard"
RMDir /r "${idir}\interfaces\Config"
RMDir "${idir}\interfaces"
RMDir /r "${idir}\win\curl"
RMDir /r "${idir}\win\par2"
RMDir /r "${idir}\win\unrar"
RMDir /r "${idir}\win\unzip"
@@ -244,6 +246,7 @@ Function .onInit
;--------------------------------
;make sure that the requires MS Runtimes are installed
;
goto nodownload ; Not needed while still using Python25
runtime_loop:
push 'msvcr90.dll'
push 'Microsoft.VC90.CRT,version="9.0.21022.8",type="win32",processorArchitecture="x86",publicKeyToken="1fc8b3b9a1e18e3b"'
@@ -296,7 +299,7 @@ SetOutPath "$INSTDIR"
;------------------------------------------------------------------
; Make sure old versions are gone
IfFileExists $INSTDIR\sabnzbd.exe 0 endWarnExist
IfFileExists $INSTDIR\python25.dll 0 endWarnExist
IfFileExists $INSTDIR\python27.dll 0 endWarnExist
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "$(MsgRemoveOld)$\n$\n$(MsgRemoveOld2)" IDOK uninst
Abort
uninst:
@@ -317,7 +320,7 @@ WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninst
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLUpdateInfo" 'http://sabnzbd.org/'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Comments" 'The automated Usenet download tool'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayIcon" '$INSTDIR\interfaces\Classic\templates\static\images\favicon.ico'
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "EstimatedSize" 18400
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "EstimatedSize" 29622
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoRepair" -1
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoModify" -1
; write out uninstaller
@@ -348,8 +351,8 @@ Section $(MsgIcon) desktop
SectionEnd ; end of desktop icon section
Section /o $(MsgAssoc) assoc
${registerExtension} "$INSTDIR\nzb.ico" "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
;${registerExtension} "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
${registerExtension} "$INSTDIR\icons\nzb.ico" "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
${RefreshShellIcons}
SectionEnd ; end of file association section
; begin uninstall settings/section
@@ -405,11 +408,11 @@ Section "un.$(MsgDelProgram)" Uninstall
DeleteRegKey HKEY_CURRENT_USER "Software\SABnzbd"
${unregisterExtension} ".nzb" "NZB File"
${RefreshShellIcons}
SectionEnd ; end of uninstall section
Section "un.$(MsgDelSettings)" DelSettings
Section /o "un.$(MsgDelSettings)" DelSettings
DetailPrint "Uninstall settings $LOCALAPPDATA"
Delete "$LOCALAPPDATA\sabnzbd\sabnzbd.ini"
RMDir /r "$LOCALAPPDATA\sabnzbd"

View File

@@ -1,8 +1,8 @@
Metadata-Version: 1.0
Name: SABnzbd
Version: 0.7.0RC2
Summary: SABnzbd-0.7.0RC2
Home-page: http://sourceforge.net/projects/sabnzbdplus
Version: 0.7.4RC2
Summary: SABnzbd-0.7.4RC2
Home-page: http://sabnzbd.org
Author: The SABnzbd Team
Author-email: team@sabnzbd.org
License: GNU General Public License 2 (GPL2 or later)

82
README.mkd Normal file
View File

@@ -0,0 +1,82 @@
Release Notes - SABnzbd 0.7.4RC2
==================================
## Fixes in 0.7.4RC2
- Pre-check failed to consider extra par2 files
- Fixed unjustified warning that can occur with OSX Growl 2.0
- Show memory usage on Linux systems
- Fix incorrect end-of-month quota reset
- Fix UI refresh issue when using Safari on iOS6
## Fixes in 0.7.4RC1
- OSX Mountain Lion: Notification Center support
- OSX Mountain Lion: improved "keep awake" support
- Restore SABnzbd icon for Growl
- Scheduler: action can now run on multiple weekdays
- Scheduler: add "remove failed jobs" action
- After successful pre-check, preserve a job's position in the queue
- Make Windows version less eager to use par2-classic
- Support for HTTPS chain files (needed when you buy your own certificate)
- Prevent jobs from showing up in queue and history simultaneously
- Fix failure to fetch more par2-files for posts with badly formatted subject lines
- Special option: rss_odd_titles (see Wiki)
- Special option: 'overwrite_files' (See Wiki)
- A number of small issues (see changelog)
- Fix for third-party tools requesting too much history
## What's new in 0.7.0
- Download quota management
- Windows: simple system tray menu
- Multi-platform Growl support
- NotifyOSD support for Linux distros that have it
- Option to set maximum number of retries for servers (prevents deadlock)
- Pre-download check to estimate completeness (reliability is limited)
- Prevent partial downloading of par2 files that are not needed yet
- Config->Special for settings previously only available in the sabnzbd.ini file
- For Usenet servers with multiple IP addresses, pick a random one per connection
- Add pseudo-priority "Stop" that will send the job immediately to the post-processing queue
- Allow jobs still waiting for post-processing to be deleted too
- More persistent retries for unreliable indexers
- Single Configuration skin for all others skins (there is an option for the old style)
- Config->Special for settings that were previously only changeable in the sabnzbd.ini file
- Add Spanish, Portuguese (Brazil) and Polish translations
- Individual RSS filter toggle
- Unified OSX DMG
## About
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
thanks to its web-based user interface and advanced
built-in post-processing options that automatically verify, repair,
extract and clean up posts downloaded from Usenet.
(c) Copyright 2007-2012 by "The SABnzbd-team" \<team@sabnzbd.org\>
### IMPORTANT INFORMATION about release 0.7.x
<http://wiki.sabnzbd.org/introducing-0-7-0>
### Known problems and solutions
- Read the file "ISSUES.txt"
### Upgrading from 0.6.x
- Stop SABnzbd
- Install new version
- Start SABnzbd
### Upgrading from 0.5.x
- Stop SABnzbd
- Install new version
- Start SABnzbd.
The organization of the download queue is different from 0.5.x.
0.7.x will finish downloading an existing queue, but you
cannot go back to an older version without losing your queue.
Also, your sabnzbd.ini file will be upgraded, making it
incompatible with release 0.5.x
### Upgrading from 0.4.x
Download your current queue before upgrading.

View File

@@ -1,99 +0,0 @@
{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf320
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\paperw11900\paperh16840\vieww16360\viewh15680\viewkind0
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
\f0\b\fs48 \cf0 SABnzbd 0.7.0RC2\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural
\b0\fs26 \cf0 \
\b What's new
\b0 \
- Download quota management\
- Windows: simple system tray menu\
- Multi-platform Growl support\
- NotifyOSD support for Linux distros that have it\
- Option to set maximum number of retries for servers (prevents deadlock)\
- Pre-download check to estimate completeness (reliability is limited)\
- Prevent partial downloading of par2 files that are not needed yet\
- Config->Special for settings previously only available in the sabnzbd.ini file\
- For Usenet servers with multiple IP addresses, pick a random one per connection\
- Add pseudo-priority "Stop" that will send the job immediately to the post-processing queue\
- Allow jobs still waiting for post-processing to be deleted too\
- More persistent retries for unreliable indexers\
- Single Configuration skin for all others skins (there is an option for the old style)\
- Config->Special for settings that were previously only changeable in the sabnzbd.ini file\
- Add Spanish, Portuguese (Brazil) and Polish translations\
- Individual RSS filter toggle\
- Unified OSX DMG\
\
For problems fixed in the various Betas, see CHANGELOG.txt\
\
\b About
\b0 \
SABnzbd is an open-source cross-platform binary newsreader.\
It simplifies the process of downloading from Usenet dramatically,\
thanks to its friendly web-based user interface and advanced\
built-in post-processing options that automatically verify, repair,\
extract and clean up posts downloaded from Usenet.\
SABnzbd also has a fully customizable user interface,\
and offers a complete API for third-party applications to hook into.\
\
(c) Copyright 2007-2012 by "The SABnzbd-team" <team@sabnzbd.org>\
\
There is an extensive Wiki on the use of SABnzbd.\
{\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/"}}{\fldrslt http://wiki.sabnzbd.org/}}\
\
\b IMPORTANT INFORMATION
\b0 about release 0.7.0:\
{\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/introducing-0-7-0"}}{\fldrslt http://wiki.sabnzbd.org/introducing-0-7-0}}\
\
\b Known problems and solutions\
\b0 Read the file "ISSUES.txt"
\b \
\b0 \
\
\b\fs40 Upgrading from 0.6.x
\b0\fs26 \
Stop SABnzbd.\
Install new version\
Start SABnzbd.\
\
\b\fs40 Upgrading from 0.5.x
\b0\fs26 \
Stop SABnzbd.\
Uninstall current version, keeping the data.\
Install new version\
Start SABnzbd.\
\
The organization of the download queue is different from 0.5.x.\
0.6.x will finish downloading an existing queue, but you\
cannot go back to an older version without losing your queue.\
Also, your sabnzbd.ini file will be upgraded, making it\
incompatible with release 0.5.x\
\
\b\fs40 \
Upgrading from 0.4.x
\b0\fs26 \
\
\b PLEASE DOWNLOAD YOUR CURRENT QUEUE BEFORE UPGRADING
\b0 \
\
When upgrading from a 0.4.x release such as 0.4.12 your old settings will be kept.\
You will however be given a fresh queue and history. If you have items in your queue\
from the older version of SABnzbd, you can either re-import the nzb files if you kept\
an nzb backup folder, or temporarily go back to 0.4.x until your queue is complete.\
The history is now stored in a better format meaning future upgrades should be backwards\
compatible.\
}

View File

@@ -1,33 +0,0 @@
************************ SABnzbd 0.7.0RC2 ************************
What's new:
- Download quota management
- Windows: simple system tray menu
- Multi-platform Growl support
- NotifyOSD support for Linux distros that have it
- Option to set maximum number of retries for servers (prevents deadlock)
- Pre-download check to estimate completeness (reliability is limited)
- Prevent partial downloading of par2 files that are not needed yet
- Config->Special for settings previously only available in the sabnzbd.ini file
- For Usenet servers with multiple IP addresses, pick a random one per connection
- Add pseudo-priority "Stop" that will send the job immediately to the post-processing queue
- Allow jobs still waiting for post-processing to be deleted too
- More persistent retries for unreliable indexers
- Single Configuration skin for all others skins (there is an option for the old style)
- Config->Special for settings that were previously only changeable in the sabnzbd.ini file
- Add Spanish, Portuguese (Brazil) and Polish translations
- Individual RSS filter toggle
- Unified OSX DMG
For problems fixed in the various Betas, see CHANGELOG.txt
About:
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
thanks to its friendly web-based user interface and advanced
built-in post-processing options that automatically verify, repair,
extract and clean up posts downloaded from Usenet.
(c) Copyright 2007-2012 by "The SABnzbd-team" <team@sabnzbd.org>

View File

@@ -16,8 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import sys
if sys.version_info < (2,5):
print "Sorry, requires Python 2.5 or higher."
if sys.version_info < (2, 5):
print "Sorry, requires Python 2.5, 2.6 or 2.7."
sys.exit(1)
import logging
@@ -253,6 +253,7 @@ def print_help():
print " --log-all Log all article handling (for developers)"
print " --console Force console logging for OSX app"
print " --new Run a new instance of SABnzbd"
print " --no_ipv6 Do not listen on IPv6 address [::1]"
def print_version():
print """
@@ -524,30 +525,24 @@ def all_localhosts():
return ips
def ipv_localhost(v):
""" Return True if localhost resolves to some IPV4 ('4') or IPV6 ('6') address
def check_resolve(host):
""" Return True if 'host' resolves
"""
try:
info = socket.getaddrinfo('localhost', None)
info = socket.getaddrinfo(host, None)
except:
# localhost does not resolve
# Does not resolve
return False
for item in info:
item = item[4][0]
if v == '4' and ':' not in item:
return True
elif v == '6' and ':' in item:
return True
return False
return True
#------------------------------------------------------------------------------
def get_webhost(cherryhost, cherryport, https_port):
""" Determine the webhost address and port,
return (host, port, browserhost)
"""
if cherryhost == '0.0.0.0' and not ipv_localhost('4'):
if cherryhost == '0.0.0.0' and not check_resolve('127.0.0.1'):
cherryhost = ''
elif cherryhost == '::' and not ipv_localhost('6'):
elif cherryhost == '::' and not check_resolve('::1'):
cherryhost = ''
if cherryhost is None:
@@ -562,9 +557,18 @@ def get_webhost(cherryhost, cherryport, https_port):
try:
info = socket.getaddrinfo(socket.gethostname(), None)
except:
# Hostname does not resolve, use 0.0.0.0
cherryhost = '0.0.0.0'
info = socket.getaddrinfo(localhost, None)
# Hostname does not resolve
try:
# Valid user defined name?
info = socket.getaddrinfo(cherryhost, None)
except:
if cherryhost not in ('localhost', '127.0.0.1', '::1'):
cherryhost = '0.0.0.0'
try:
info = socket.getaddrinfo(localhost, None)
except:
info = socket.getaddrinfo('127.0.0.1', None)
localhost = '127.0.0.1'
for item in info:
ip = str(item[4][0])
if ip.startswith('169.254.'):
@@ -667,16 +671,18 @@ def get_webhost(cherryhost, cherryport, https_port):
return cherryhost, cherryport, browserhost, https_port
def attach_server(host, port, cert=None, key=None):
def attach_server(host, port, cert=None, key=None, chain=None):
""" Define and attach server, optionally HTTPS
"""
http_server = _cpwsgi_server.CPWSGIServer()
http_server.bind_addr = (host, port)
if cert and key:
http_server.ssl_certificate = cert
http_server.ssl_private_key = key
adapter = _cpserver.ServerAdapter(cherrypy.engine, http_server, http_server.bind_addr)
adapter.subscribe()
if not (sabnzbd.cfg.no_ipv6() and '::1' in host):
http_server = _cpwsgi_server.CPWSGIServer()
http_server.bind_addr = (host, port)
if cert and key:
http_server.ssl_certificate = cert
http_server.ssl_private_key = key
http_server.ssl_certificate_chain = chain
adapter = _cpserver.ServerAdapter(cherrypy.engine, http_server, http_server.bind_addr)
adapter.subscribe()
def is_sabnzbd_running(url):
@@ -773,7 +779,7 @@ def evaluate_inipath(path):
inipath = os.path.join(path, DEF_INI_FILE)
if os.path.isdir(path):
return inipath
elif os.path.isfile(path):
elif os.path.isfile(path) or os.path.isfile(path + '.bak'):
return path
else:
dirpart, name = os.path.split(path)
@@ -836,7 +842,7 @@ def commandline_handler(frozen=True):
try:
opts, args = getopt.getopt(info, "phdvncw:l:s:f:t:b:2:",
['pause', 'help', 'daemon', 'nobrowser', 'clean', 'logging=',
'weblogging=', 'server=', 'templates',
'weblogging=', 'server=', 'templates', 'no_ipv6',
'template2', 'browser=', 'config-file=', 'force',
'version', 'https=', 'autorestarted', 'repair', 'repair-all',
'log-all', 'no-login', 'pid=', 'new', 'sessions', 'console',
@@ -914,6 +920,7 @@ def main():
new_instance = False
force_sessions = False
osx_console = False
no_ipv6 = False
service, sab_opts, serv_opts, upload_nzbs = commandline_handler()
@@ -1000,6 +1007,8 @@ def main():
elif opt in ('--console',):
re_argv.append(opt)
osx_console = True
elif opt in ('--no_ipv6',):
no_ipv6 = True
sabnzbd.MY_FULLNAME = os.path.normpath(os.path.abspath(sabnzbd.MY_FULLNAME))
sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME)
@@ -1053,7 +1062,7 @@ def main():
GetProfileInfo(vista_plus)
# Find out where INI file is
inifile = os.path.abspath(sabnzbd.DIR_PROG + '/' + DEF_INI_FILE)
if not os.path.exists(inifile):
if not os.path.exists(inifile) and not os.path.exists(inifile + '.bak'):
inifile = os.path.abspath(sabnzbd.DIR_LCLDATA + '/' + DEF_INI_FILE)
if sabnzbd.DARWIN:
copy_old_files(sabnzbd.DIR_LCLDATA)
@@ -1065,7 +1074,7 @@ def main():
# All system data dirs are relative to the place we found the INI file
sabnzbd.DIR_LCLDATA = os.path.dirname(inifile)
if not os.path.exists(inifile) and not os.path.exists(sabnzbd.DIR_LCLDATA):
if not os.path.exists(inifile) and not os.path.exists(inifile + '.bak') and not os.path.exists(sabnzbd.DIR_LCLDATA):
try:
os.makedirs(sabnzbd.DIR_LCLDATA)
except IOError:
@@ -1082,6 +1091,9 @@ def main():
# Set root folders for HTTPS server file paths
sabnzbd.cfg.set_root_folders2()
if no_ipv6:
sabnzbd.cfg.no_ipv6.set(True)
# Determine web host address
cherryhost, cherryport, browserhost, https_port = get_webhost(cherryhost, cherryport, https_port)
enable_https = sabnzbd.cfg.enable_https()
@@ -1200,8 +1212,6 @@ def main():
sabnzbd.cfg.log_backups())
format = '%(asctime)s::%(levelname)s::[%(module)s:%(lineno)d] %(message)s'
if sabnzbd.WIN32:
format += '\r'
rollover_log.setFormatter(logging.Formatter(format))
rollover_log.addFilter(FilterCP3())
sabnzbd.LOGHANDLER = rollover_log
@@ -1341,6 +1351,10 @@ def main():
https_cert = sabnzbd.cfg.https_cert.get_path()
https_key = sabnzbd.cfg.https_key.get_path()
https_chain = sabnzbd.cfg.https_chain.get_path()
if not (sabnzbd.cfg.https_chain() and os.path.exists(https_chain)):
https_chain = None
if enable_https:
# If either the HTTPS certificate or key do not exist, make some self-signed ones.
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
@@ -1359,8 +1373,8 @@ def main():
hosts[1] = '::1'
# The Windows binary requires numeric localhost as primary address
if multilocal and cherryhost == 'localhost' and hosts[1] == '127.0.0.1':
cherryhost = '::1'
if multilocal and cherryhost == 'localhost':
cherryhost = hosts[0]
if enable_https:
if https_port:
@@ -1370,14 +1384,15 @@ def main():
# Extra HTTP port for secondary localhost
attach_server(hosts[1], cherryport)
# Extra HTTPS port for secondary localhost
attach_server(hosts[1], https_port, https_cert, https_key)
attach_server(hosts[1], https_port, https_cert, https_key, https_chain)
cherryport = https_port
elif multilocal:
# Extra HTTPS port for secondary localhost
attach_server(hosts[1], cherryport, https_cert, https_key)
cherrypy.config.update({'server.ssl_certificate' : https_cert,
'server.ssl_private_key' : https_key })
'server.ssl_private_key' : https_key,
'server.ssl_certificate_chain' : https_chain})
elif multilocal:
# Extra HTTP port for secondary localhost
attach_server(hosts[1], cherryport)
@@ -1395,6 +1410,17 @@ def main():
else:
sessions = None
mime_gzip = ('text/html',
'text/plain',
'text/css',
'text/xml',
'text/javascript',
'application/javascript',
'text/x-javascript',
'application/x-javascript',
'text/x-json',
'application/json'
)
cherrypy.config.update({'server.environment': 'production',
'server.socket_host': cherryhost,
'server.socket_port': cherryport,
@@ -1404,7 +1430,7 @@ def main():
'engine.reexec_retry' : 100,
'tools.encode.on' : True,
'tools.gzip.on' : True,
'tools.gzip.mime_types' : ['text/html', 'text/plain', 'text/javascript', 'text/css', 'application/x-javascript'],
'tools.gzip.mime_types' : mime_gzip,
'tools.sessions.on' : bool(sessions),
'tools.sessions.storage_type' : 'file',
'tools.sessions.storage_path' : sessions,
@@ -1495,8 +1521,8 @@ def main():
if sabnzbd.FOUNDATION:
import sabnzbd.osxmenu
sabnzbd.osxmenu.notify("SAB_Launched", None)
growler.send_notification('SABnzbd %s' % (sabnzbd.__version__),
"http://%s:%s/sabnzbd" % (browserhost, cherryport), 'startup')
growler.send_notification('SABnzbd%s' % growler.hostname(),
T('SABnzbd %s started') % sabnzbd.__version__, 'startup')
# Now's the time to check for a new version
check_latest_version()
autorestarted = False
@@ -1565,7 +1591,7 @@ def main():
### 30 sec polling tasks
if timer > 9:
timer = 0
# Keep Windows awake (if needed)
# Keep OS awake (if needed)
sabnzbd.keep_awake()
# Restart scheduler (if needed)
scheduler.restart()

View File

@@ -1,3 +1,4 @@
import logging
import cherrypy
from cherrypy.lib import httpauth
@@ -9,10 +10,10 @@ def check_auth(users, encrypt=None, realm=None):
ah = httpauth.parseAuthorization(cherrypy.request.headers['authorization'])
if ah is None:
raise cherrypy.HTTPError(400, 'Bad Request')
if not encrypt:
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
if callable(users):
try:
# backward compatibility
@@ -20,7 +21,7 @@ def check_auth(users, encrypt=None, realm=None):
if not isinstance(users, dict):
raise ValueError, "Authentication users must be a dictionary"
# fetch the user password
password = users.get(ah["username"], None)
except TypeError:
@@ -29,23 +30,26 @@ def check_auth(users, encrypt=None, realm=None):
else:
if not isinstance(users, dict):
raise ValueError, "Authentication users must be a dictionary"
# fetch the user password
password = users.get(ah["username"], None)
# validate the authorization by re-computing it here
# and compare it with what the user-agent provided
if httpauth.checkResponse(ah, password, method=cherrypy.request.method,
encrypt=encrypt, realm=realm):
cherrypy.request.login = ah["username"]
return True
if ah.get('username') or ah.get('password'):
logging.info('Attempt to login with wrong credentials from %s',
cherrypy.request.headers['Remote-Addr'])
cherrypy.request.login = False
return False
def basic_auth(realm, users, encrypt=None):
"""If auth fails, raise 401 with a basic authentication header.
realm: a string containing the authentication realm.
users: a dict of the form: {username: password} or a callable returning a dict.
encrypt: callable used to encrypt the password returned from the user-agent.
@@ -53,23 +57,23 @@ def basic_auth(realm, users, encrypt=None):
"""
if check_auth(users, encrypt):
return
# inform the user-agent this path is protected
cherrypy.response.headers['www-authenticate'] = httpauth.basicAuth(realm)
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
def digest_auth(realm, users):
"""If auth fails, raise 401 with a digest authentication header.
realm: a string containing the authentication realm.
users: a dict of the form: {username: password} or a callable returning a dict.
"""
if check_auth(users, realm=realm):
return
# inform the user-agent this path is protected
cherrypy.response.headers['www-authenticate'] = httpauth.digestAuth(realm)
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")

View File

@@ -1,332 +1,402 @@
import re
import hashlib
import time
import platform
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'
__version__ = '0.4'
class BaseError(Exception):
pass
def gntp_error(self):
error = GNTPError(self.errorcode, self.errordesc)
return error.encode()
class ParseError(BaseError):
def gntp_error(self):
error = GNTPError(errorcode=500,errordesc='Error parsing the message')
return error.encode()
errorcode = 500
errordesc = 'Error parsing the message'
class AuthError(BaseError):
def gntp_error(self):
error = GNTPError(errorcode=400,errordesc='Error with authorization')
return error.encode()
errorcode = 400
errordesc = 'Error with authorization'
class UnsupportedError(BaseError):
def gntp_error(self):
error = GNTPError(errorcode=500,errordesc='Currently unsupported by gntp.py')
return error.encode()
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):
info = {
'version':'1.0',
'messagetype':None,
'encryptionAlgorithmID':None
}
_requiredHeaders = []
headers = {}
resources = {}
def add_origin_info(self):
self.add_header('Origin-Machine-Name',platform.node())
self.add_header('Origin-Software-Name','gntp.py')
self.add_header('Origin-Software-Version',__version__)
self.add_header('Origin-Platform-Name',platform.system())
self.add_header('Origin-Platform-Version',platform.platform())
"""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 data: GNTP Message
@return: GNTP Message information in a dictionary
'''
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
match = re.match('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', data,re.IGNORECASE)
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 password: Null to clear password
@param encryptAlgo: Supports MD5,SHA1,SHA256,SHA512
@todo: Support other hash functions
'''
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;
self.info['keyHashAlgorithm'] = None
return
if not self.encryptAlgo in hash.keys():
raise UnsupportedError('INVALID HASH "%s"'%self.encryptAlgo)
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
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 value: Value to decode
@return: Hex string
'''
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)
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):
def _decode_binary(self, rawIdentifier, identifier):
rawIdentifier += '\r\n\r\n'
dataLength = int(identifier['Length'])
pointerStart = self.raw.find(rawIdentifier)+len(rawIdentifier)
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)))
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
'''
def _validate_password(self, password):
"""Validate GNTP Message against stored password"""
self.password = password
if password == None: raise Exception()
keyHash = self.info.get('keyHash',None)
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
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
'''
"""Verify required headers"""
for header in self._requiredHeaders:
if not self.headers.get(header,False):
raise ParseError('Missing Notification Header: '+header)
if not self.headers.get(header, False):
raise ParseError('Missing Notification Header: ' + header)
def _format_info(self):
'''
Generate info line for GNTP Message
@return: Info line string
'''
info = u'GNTP/%s %s'%(
"""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'%(
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'%(
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 data:
@return: Dictionary of headers
'''
)
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 = re.match('([\w-]+):(.+)', line)
if not match: continue
key = match.group(1).strip()
val = match.group(2).strip()
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):
def add_header(self, key, value):
if isinstance(value, unicode):
self.headers[key] = value
else:
self.headers[key] = unicode('%s'%value,'utf8','replace')
def decode(self,data,password=None):
'''
Decode GNTP Message
@param data:
'''
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 GNTP Message
@return: GNTP Message ready to be sent
'''
self.validate()
EOL = u'\r\n'
message = self._format_info() + EOL
"""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():
message += u'%s: %s%s'%(k,v,EOL)
message += EOL
return message
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"""
notifications = []
"""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):
'''
@param data: (Optional) See decode()
@param password: (Optional) Password to use while encoding/decoding messages
'''
self.info['messagetype'] = 'REGISTER'
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'REGISTER')
self.notifications = []
if data:
self.decode(data,password)
self.decode(data, password)
else:
self.set_password(password)
self.add_header('Application-Name', 'pygntp')
self.add_header('Notifications-Count', 0)
self.add_origin_info()
def validate(self):
'''
Validate required headers and validate notification headers
'''
'''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)
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 data: Message to decode.
'''
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
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):
if notice.get('Notification-Name', False):
self.notifications.append(notice)
elif notice.get('Identifier',False):
notice['Data'] = self._decode_binary(part,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 name: Notification Name
@param enabled: Default Notification to Enabled
'''
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
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: GNTP Registration Message ready to be sent
'''
self.validate()
EOL = u'\r\n'
message = self._format_info() + EOL
"""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():
message += u'%s: %s%s'%(k,v,EOL)
for k, v in self.headers.iteritems():
buffer.writefmt('%s: %s', k, v)
buffer.writefmt()
#Notifications
if len(self.notifications)>0:
if len(self.notifications) > 0:
for notice in self.notifications:
message += EOL
for k,v in notice.iteritems():
message += u'%s: %s%s'%(k,v,EOL)
message += EOL
return message
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"""
"""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):
'''
@param data: (Optional) See decode()
@param app: (Optional) Set Application-Name
@param name: (Optional) Set Notification-Name
@param title: (Optional) Set Notification Title
@param password: (Optional) Password to use while encoding/decoding messages
'''
self.info['messagetype'] = 'NOTIFY'
def __init__(self, data=None, app=None, name=None, title=None, password=None):
_GNTPBase.__init__(self, 'NOTIFY')
if data:
self.decode(data,password)
self.decode(data, password)
else:
self.set_password(password)
if app:
@@ -335,105 +405,103 @@ class GNTPNotice(_GNTPBase):
self.add_header('Notification-Name', name)
if title:
self.add_header('Notification-Title', title)
self.add_origin_info()
def decode(self,data,password):
'''
Decode existing GNTP Notification message
@param data: Message to decode.
'''
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
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)
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
def encode(self):
'''
Encode a GNTP Notification Message
@return: GNTP Notification Message ready to be sent
'''
self.validate()
EOL = u'\r\n'
message = self._format_info() + EOL
#Headers
for k,v in self.headers.iteritems():
message += u'%s: %s%s'%(k,v,EOL)
message += EOL
return message
self.resources[notice.get('Identifier')] = notice
class GNTPSubscribe(_GNTPBase):
"""Represents a GNTP Subscribe Command"""
def __init__(self,data=None,password=None):
self.info['messagetype'] = 'SUBSCRIBE'
self._requiredHeaders = [
'Subscriber-ID',
'Subscriber-Name',
]
"""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)
self.decode(data, password)
else:
self.set_password(password)
self.add_origin_info()
class GNTPOK(_GNTPBase):
"""Represents a GNTP OK Response"""
"""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):
'''
@param data: (Optional) See _GNTPResponse.decode()
@param action: (Optional) Set type of action the OK Response is for
'''
self.info['messagetype'] = '-OK'
def __init__(self, data=None, action=None):
_GNTPBase.__init__(self, '-OK')
if data:
self.decode(data)
if action:
self.add_header('Response-Action', action)
self.add_origin_info()
class GNTPError(_GNTPBase):
_requiredHeaders = ['Error-Code','Error-Description']
def __init__(self,data=None,errorcode=None,errordesc=None):
'''
@param data: (Optional) See _GNTPResponse.decode()
@param errorcode: (Optional) Error code
@param errordesc: (Optional) Error Description
'''
self.info['messagetype'] = '-ERROR'
"""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)
self.add_origin_info()
def error(self):
return self.headers['Error-Code'],self.headers['Error-Description']
def parse_gntp(data,password=None):
'''
Attempt to parse a message as a GNTP message
@param data: Message to be parsed
@param password: Optional password to be used to verify the message
'''
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',data,re.IGNORECASE)
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)
return GNTPRegister(data, password=password)
elif info['messagetype'] == 'NOTIFY':
return GNTPNotice(data,password=password)
return GNTPNotice(data, password=password)
elif info['messagetype'] == 'SUBSCRIBE':
return GNTPSubscribe(data,password=password)
return GNTPSubscribe(data, password=password)
elif info['messagetype'] == '-OK':
return GNTPOK(data)
elif info['messagetype'] == '-ERROR':

View File

@@ -12,10 +12,55 @@ using GNTP
import gntp
import socket
import logging
import platform
__all__ = [
'mini',
'GrowlNotifier',
]
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
@@ -27,55 +72,35 @@ class GrowlNotifier(object):
:param string hostname: Remote host
:param integer port: Remote port
"""
applicationName = 'Python GNTP'
notifications = []
defaultNotifications = []
applicationIcon = None
passwordHash = 'MD5'
socketTimeout = 3
#GNTP Specific
password = None
hostname = 'localhost'
port = 23053
def __init__(self, applicationName='Python GNTP', notifications=[],
defaultNotifications=None, applicationIcon=None, hostname='localhost',
password=None, port=23053):
def __init__(self, applicationName=None, notifications=None, defaultNotifications=None, applicationIcon=None, hostname=None, password=None, port=None):
if applicationName:
self.applicationName = applicationName
assert self.applicationName, 'An application name is required.'
if notifications:
self.notifications = list(notifications)
assert self.notifications, 'A sequence of one or more notification names is required.'
if defaultNotifications is not None:
self.applicationName = applicationName
self.notifications = list(notifications)
if defaultNotifications:
self.defaultNotifications = list(defaultNotifications)
elif not self.defaultNotifications:
self.defaultNotifications = list(self.notifications)
else:
self.defaultNotifications = self.notifications
self.applicationIcon = applicationIcon
if applicationIcon is not None:
self.applicationIcon = self._checkIcon(applicationIcon)
elif self.applicationIcon is not None:
self.applicationIcon = self._checkIcon(self.applicationIcon)
#GNTP Specific
if password:
self.password = password
if hostname:
self.hostname = hostname
assert self.hostname, 'Requires valid hostname'
if port:
self.port = int(port)
assert isinstance(self.port, int), 'Requires valid port'
self.password = password
self.hostname = hostname
self.port = int(port)
def _checkIcon(self, data):
'''
Check the icon to see if it's valid
@param data:
@todo Consider checking for a valid URL
If it's a simple URL icon, then we return True. If it's a data icon
then we return False
'''
return data
logger.debug('Checking icon')
return data.startswith('http')
def register(self):
"""Send GNTP Registration
@@ -84,23 +109,26 @@ class GrowlNotifier(object):
Before sending notifications to Growl, you need to have
sent a registration message at least once
"""
logger.info('Sending registration to %s:%s', self.hostname, self.port)
logger.debug('Sending registration to %s:%s', self.hostname, self.port)
register = gntp.GNTPRegister()
register.add_header('Application-Name', self.applicationName)
for notification in self.notifications:
enabled = notification in self.defaultNotifications
register.add_notification(notification, enabled)
if self.applicationIcon:
register.add_header('Application-Icon', self.applicationIcon)
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)
if self.password:
register.set_password(self.password, self.passwordHash)
response = self._send('register', register.encode())
if isinstance(response, gntp.GNTPOK):
return True
logger.error('Invalid response %s', response.error())
return response.error()
self.add_origin_info(register)
self.register_hook(register)
return self._send('register', register)
def notify(self, noteType, title, description, icon=None, sticky=False, priority=None):
def notify(self, noteType, title, description, icon=None, sticky=False,
priority=None, callback=None, identifier=None):
"""Send a GNTP notifications
.. warning::
@@ -112,8 +140,13 @@ class GrowlNotifier(object):
:param string icon: Icon URL path
:param boolean sticky: Sticky notification
:param integer priority: Message priority level from -2 to 2
:param string callback: URL callback
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
logger.debug('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
assert noteType in self.notifications
notice = gntp.GNTPNotice()
notice.add_header('Application-Name', self.applicationName)
@@ -126,14 +159,23 @@ class GrowlNotifier(object):
if priority:
notice.add_header('Notification-Priority', priority)
if icon:
notice.add_header('Notification-Icon', self._checkIcon(icon))
if self._checkIcon(icon):
notice.add_header('Notification-Icon', icon)
else:
id = notice.add_resource(icon)
notice.add_header('Notification-Icon', id)
if description:
notice.add_header('Notification-Text', description)
response = self._send('notify', notice.encode())
if isinstance(response, gntp.GNTPOK):
return True
logger.error('Invalid response %s', response.error())
return response.error()
if callback:
notice.add_header('Notification-Callback-Target', callback)
if identifier:
notice.add_header('Notification-Coalescing-ID', identifier)
self.add_origin_info(notice)
self.notify_hook(notice)
return self._send('notify', notice)
def subscribe(self, id, name, port):
"""Send a Subscribe request to a remote machine"""
@@ -143,30 +185,63 @@ class GrowlNotifier(object):
sub.add_header('Subscriber-Port', port)
if self.password:
sub.set_password(self.password, self.passwordHash)
response = self._send('subscribe', sub.encode())
if isinstance(response, gntp.GNTPOK):
return True
logger.error('Invalid response %s', response.error())
return response.error()
def _send(self, type, data):
self.add_origin_info(sub)
self.subscribe_hook(sub)
return self._send('subscribe', sub)
def add_origin_info(self, packet):
"""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-Platform-Name', platform.system())
packet.add_header('Origin-Platform-Version', platform.platform())
def register_hook(self, packet):
pass
def notify_hook(self, packet):
pass
def subscribe_hook(self, packet):
pass
def _send(self, messagetype, packet):
"""Send the GNTP Packet"""
#logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, type, data)
#Less verbose please
logger.debug('To : %s:%s <%s>', self.hostname, self.port, type)
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__)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(self.socketTimeout)
s.connect((self.hostname, self.port))
s.send(data.encode('utf8', 'replace'))
try:
s.settimeout(10)
except:
pass
response = gntp.parse_gntp(s.recv(1024))
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)
s.close()
#logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
#Less verbose please
#Less verbose
logger.debug('From : %s:%s <%s>', self.hostname, self.port, response.__class__)
return 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
return True
logger.error('Invalid response: %s', response.error())
return response.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)
mini('Testing mini notification')

View File

@@ -73,6 +73,7 @@
<table>
<tr>
<th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
<th>&nbsp;</th>
<th>$T('rss-order')</th>
<th>$T('rss-type')</th>
<th>$T('rss-filter')</th>
@@ -86,6 +87,7 @@
<form action="upd_rss_filter" method="get">
<tr class="odd">
<td></td>
<td><input type="checkbox" name="enabled" value="1" checked="checked" /></td>
<td></td>
<td>
<select name="filter_type">
@@ -152,6 +154,10 @@
</td>
<form action="upd_rss_filter" method="get">
<td>
<input type="checkbox" name="enabled" value="1" <!--#if $filter[6] == '1' then 'checked="checked"' else ""#--> />
</td>
<td>
<input type="text" size="3" name="new_index" value=$fnum>
</td>
@@ -208,6 +214,7 @@
<input type="hidden" name="feed" value="$feed"/>
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-save')"/>
<!--#if not $rss[$feed].filter_states[$fnum]#-->&nbsp;&nbsp;$T('Incorrect filter')<!--#end if#-->
</td>
</form>
</tr>

View File

@@ -31,16 +31,14 @@ $T('hour'):<br>
<!--#end for#-->
</select>
<br>$T('sch-frequency'): <br>
<select name="dayofweek">
<option value="*" selected>$T('daily')
<option value="1">$T('monday')
<option value="2">$T('tuesday')
<option value="3">$T('wednesday')
<option value="4">$T('thursday')
<option value="5">$T('friday')
<option value="6">$T('saturday')
<option value="7">$T('sunday')
</select>
<input type="checkbox" name="daysofweek" value="1">$T('monday')<br/>
<input type="checkbox" name="daysofweek" value="2">$T('tuesday')<br/>
<input type="checkbox" name="daysofweek" value="3">$T('wednesday')<br/>
<input type="checkbox" name="daysofweek" value="4">$T('thursday')<br/>
<input type="checkbox" name="daysofweek" value="5">$T('friday')<br/>
<input type="checkbox" name="daysofweek" value="6">$T('saturday')<br/>
<input type="checkbox" name="daysofweek" value="7">$T('sunday')<br/>
<br>$T('sch-action'):<br>
<select name="action">
<!--#for $action in $actions#-->

View File

@@ -1,6 +1,7 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>$mbleft MB $T('queued') - SABnzbd $version</title>
<link rel="stylesheet" type="text/css" href="$statpath/static/stylesheets/default.css"/>
<link rel="stylesheet" type="text/css" href="$statpath/static/stylesheets/defaultcolors.css"/>

View File

@@ -50,7 +50,9 @@
\$(document).ready(function () {
\$('.col2 H3').click(function () { \$(this).parent().next().toggle() });
#if $pane != "Servers"#
\$('.col2 H3').click(function () { \$(this).parent().next().toggle() });
#end if#
\$('.sabnzbd_restart').click(function () {
\$('.sabnzbd_restart').each(function () {
@@ -97,7 +99,7 @@
});
});
</script>
</head>

View File

@@ -127,6 +127,11 @@
<input type="text" name="https_key" id="https_key" value="$https_key" size="50" />
<span class="desc">$T('explain-https_key')</span>
</div>
<div class="field-pair alt">
<label class="config" for="https_chain">$T('opt-https_chain')</label>
<input type="text" name="https_chain" id="https_chain" value="$https_chain" size="50" />
<span class="desc">$T('explain-https_chain')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />

View File

@@ -85,12 +85,26 @@
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair alt">
<label class="config" for="notify_classes">$T('opt-notify_classes')</label>
<select name="notify_classes" multiple="multiple" class="multiple_cats">
<!--#for $item in $notify_list#-->
<option value="$item" <!--#if $item in $notify_classes then 'selected="selected"' else ""#--> >$T($notify_texts[$item])</option>
<!--#end for#-->
</select>
<span class="desc">$T('explain-notify_classes')</span>
</div>
<div class="field-pair <!--#if not $have_ncenter then "disabled" else "" #-->">
<label class="config" for="ncenter_enable">$T('opt-ncenter_enable')</label>
<input type="checkbox" name="ncenter_enable" id="ncenter_enable" value="1" <!--#if int($ncenter_enable) > 0 then 'checked="checked"' else ""#--> <!--#if not $have_ncenter then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ncenter_enable')</span>
</div>
<div class="field-pair alt <!--#if not $have_ntfosd then "disabled" else "" #-->">
<label class="config" for="ntfosd_enable">$T('opt-ntfosd_enable')</label>
<input type="checkbox" name="ntfosd_enable" id="ntfosd_enable" value="1" <!--#if int($ntfosd_enable) > 0 then 'checked="checked"' else ""#--> <!--#if not $have_ntfosd then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ntfosd_enable')</span>
</div>
<div class="field-pair">
<div class="field-pair" >
<label class="config" for="growl_enable">$T('opt-growl_enable')</label>
<input type="checkbox" name="growl_enable" id="growl_enable" value="1" <!--#if int($growl_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-growl_enable')</span>
@@ -163,7 +177,7 @@
success: function (data) {
if (data.status == true) {
\$('#testnotice-result').addClass("success").html('$T('smpl-notesent')');
} else {
} else {
\$('#testnotice-result').addClass("failure").html(data.error);
}
}

View File

@@ -100,7 +100,7 @@
</fieldset>
</div><!-- /col1 -->
</form>
</div>
</div>
<!--#end if#-->
@@ -341,6 +341,7 @@
<td>
<input type="submit" class="Save" value="$T('button-save')" />
<input type="button" class="delFilter" value="$T('button-x')" />
<!--#if not $rss[$feed].filter_states[$fnum]#-->&nbsp;&nbsp;$T('Incorrect filter')<!--#end if#-->
</td>
</tr>
</tbody>
@@ -510,7 +511,7 @@
}).done(function( msg ) {
location.reload();
});
} else {
} else {
return false;
}
});

View File

@@ -34,17 +34,16 @@ else:
</select>
</div>
<div class="field-pair">
<label class="config" for="dayofweek">$T('sch-frequency')</label>
<select name="dayofweek" id="dayofweek">
<option value="*" selected="selected">$T('daily')</option>
<option value="1">$T('monday')</option>
<option value="2">$T('tuesday')</option>
<option value="3">$T('wednesday')</option>
<option value="4">$T('thursday')</option>
<option value="5">$T('friday')</option>
<option value="6">$T('saturday')</option>
<option value="7">$T('sunday')</option>
</select>
<label class="config" for="daysofweek">$T('sch-frequency')</label>
<div class="checkbox-days">
<p><input type="checkbox" name="daysofweek" value="1"><label>$T('monday')</label></p>
<p><input type="checkbox" name="daysofweek" value="2"><label>$T('tuesday')</label></p>
<p><input type="checkbox" name="daysofweek" value="3"><label>$T('wednesday')</label></p>
<p><input type="checkbox" name="daysofweek" value="4"><label>$T('thursday')</label></p>
<p><input type="checkbox" name="daysofweek" value="5"><label>$T('friday')</label></p>
<p><input type="checkbox" name="daysofweek" value="6"><label>$T('saturday')</label></p>
<p><input type="checkbox" name="daysofweek" value="7"><label>$T('sunday')</label></p>
</div>
</div>
<div class="field-pair alt">
<label class="config" for="action">$T('sch-action')</label>

View File

@@ -23,7 +23,7 @@
</div>
<div class="field-pair">
<label class="config" for="host">$T('srv-host')</label>
<input type="url" name="host" id="host" size="40" />
<input type="text" name="host" id="host" size="40" />
</div>
<div class="field-pair alt">
<label class="config" for="port">$T('srv-port')</label>
@@ -86,18 +86,16 @@
<div class="section">
<div class="col2">
<h3>$servers[$server]['name']</h3>
<!--#if 'amounts' in $servers[$server]#-->
<p>
<b>$T('srv-bandwidth'):</b><br/>
$T('total'): $servers[$server]['amounts'][0]<br/>
$T('today'): $servers[$server]['amounts'][3]<br/>
$T('thisWeek'): $servers[$server]['amounts'][2]<br/>
$T('thisMonth'): $servers[$server]['amounts'][1]
<table><tr>
<td><input type="checkbox" class="toggleServerCheckbox" name="q_enable" value="1" <!--#if int($servers[$server]['enable']) != 0 then 'checked="checked"' else ""#--> rel="$server" /></td>
<td>&nbsp;$T('enabled')</td>
</tr></table>
</p>
<input type="button" value="$T('button-clrServer')" class="clrServer" />
<!--#end if#-->
<input type="button" value="$T('showDetails')" class="showserver" />
</div><!-- /col2 -->
<div class="col1">
<div class="col1" style="display:none;">
<fieldset>
<div class="field-pair alt">
<label class="config" for="enable$cur">$T('srv-enable')</label>
@@ -106,7 +104,7 @@
</div>
<div class="field-pair">
<label class="config" for="host$cur">$T('srv-host')</label>
<input type="url" name="host" id="host$cur" value="$servers[$server]['host']" size="40" />
<input type="text" name="host" id="host$cur" value="$servers[$server]['host']" size="40" />
</div>
<div class="field-pair alt">
<label class="config" for="port$cur">$T('srv-port')</label>
@@ -154,6 +152,15 @@
</div>
</fieldset>
</div><!-- /col1 -->
<div class="col2" style="display:block;">
<!--#if 'amounts' in $servers[$server]#-->
<b>$T('srv-bandwidth'):</b><br/>
$T('total'): $servers[$server]['amounts'][0]<br/>
$T('today'): $servers[$server]['amounts'][3]<br/>
$T('thisWeek'): $servers[$server]['amounts'][2]<br/>
$T('thisMonth'): $servers[$server]['amounts'][1]
<!--#end if#-->
</div>
</div><!-- /section -->
</form>
<!--#end for#-->
@@ -162,6 +169,15 @@
<script>
\$(document).ready(function(){
\$('.showserver').click(function () {
\$(this).parent().next().toggle();
\$(this).parent().next().next().toggle();
if (\$(this).attr("value") == "$T('showDetails')") {
\$(this).attr("value", "$T('hideDetails')");
} else {
\$(this).attr("value", "$T('showDetails')");
}
});
\$('#addServerButton').click(function(){
\$('#addServer').hide();
\$('#addServerContent').show();
@@ -188,6 +204,16 @@
\$(this).parents('form:first').attr('action','clrServer').submit();
return false;
});
\$('.toggleServerCheckbox').click(function(){
var whichServer = \$(this).attr("rel");
\$.ajax({
type: "POST",
url: "toggleServer",
data: {server: whichServer, session: "$session" }
}).done(function() {
location.reload();
});
});
});
</script>

View File

@@ -13,7 +13,11 @@
<fieldset>
<div class="field-pair alt">
<label class="config" for="check_new_rel">$T('opt-check_new_rel')</label>
<input type="checkbox" name="check_new_rel" id="check_new_rel" value="1" <!--#if int($check_new_rel) > 0 then 'checked="checked"' else ""#--> />
<select name="check_new_rel" id="check_new_rel">
<option value="0" <!--#if $check_new_rel == 0 then 'selected="selected" class="selected"' else ""#--> >$T('off')</option>
<option value="1" <!--#if $check_new_rel == 1 then 'selected="selected" class="selected"' else ""#--> >$T('on')</option>
<option value="2" <!--#if $check_new_rel == 2 then 'selected="selected" class="selected"' else ""#--> >$T('also-test')</option>
</select>
<span class="desc">$T('explain-check_new_rel')</span>
</div>
<div class="field-pair">

View File

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SABnzbd $version - $T('queued'): $mbleft $T('MB')</title>
<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="${path}rss?mode=history"/>

View File

@@ -10,7 +10,7 @@
</table>
<div class="sabnzbd_logo main_sprite_container sprite_sabnzbdplus_logo"></div>
<p><strong>SABnzbd $T('version'):</strong> $version</p>
<p><small>Copyright (C) 2008-2012, The SABnzbd Team &lt;<a href="mailto:team@sabnzbd.org">team@sabnzbd.org</a>&gt;</small></p>
<p><small>Copyright (C) 2008-2012, The SABnzbd Team &lt;team@sabnzbd.org&gt;</small></p>
<p><small>$T('yourRights')</small></p>
</div>

View File

@@ -174,6 +174,7 @@ $T('explain-RSS')
<table class="rssTable">
<tr>
<th>$T('Plush-rss-delete')</th>
<th>&nbsp;</th>
<th>$T('rss-order')</th>
<th>$T('rss-type')</th>
<th>$T('rss-filter')</th>
@@ -187,6 +188,7 @@ $T('explain-RSS')
<form action="upd_rss_filter" method="get">
<tr class="odd">
<td></td>
<td><input type="checkbox" name="enabled" value="1" checked="checked" /></td>
<td></td>
<td>
<select name="filter_type">
@@ -255,7 +257,10 @@ $T('explain-RSS')
</td>
<form action="upd_rss_filter" method="get">
<td>
<td>
<input type="checkbox" name="enabled" value="1" <!--#if $filter[6] == '1' then 'checked="checked"' else ""#--> />
</td>
<td>
<input type="text" size="3" name="new_index" value=$fnum>
</td>
<td>
@@ -310,6 +315,7 @@ $T('explain-RSS')
<input type="hidden" name="feed" value="$feed"/>
<input type="hidden" name="session" value="$session">
<input type="submit" class="juiButton" value="$T('button-save')"/>
<!--#if not $rss[$feed].filter_states[$fnum]#-->&nbsp;&nbsp;$T('Incorrect filter')<!--#end if#-->
</td>
</form>
</tr>

View File

@@ -36,18 +36,15 @@ else:
</label>
</div>
<div class="field-pair alt">
<label class="nocheck clearfix" for="dayofweek">
<label class="nocheck clearfix" for="daysofweek">
<span class="component-title">$T('sch-frequency')</span>
<select name="dayofweek" id="dayofweek">
<option value="*" selected>$T('daily')</option>
<option value="1">$T('monday')</option>
<option value="2">$T('tuesday')</option>
<option value="3">$T('wednesday')</option>
<option value="4">$T('thursday')</option>
<option value="5">$T('friday')</option>
<option value="6">$T('saturday')</option>
<option value="7">$T('sunday')</option>
</select>
<input type="checkbox" name="daysofweek" value="1">$T('monday')<br/>
<input type="checkbox" name="daysofweek" value="2">$T('tuesday')<br/>
<input type="checkbox" name="daysofweek" value="3">$T('wednesday')<br/>
<input type="checkbox" name="daysofweek" value="4">$T('thursday')<br/>
<input type="checkbox" name="daysofweek" value="5">$T('friday')<br/>
<input type="checkbox" name="daysofweek" value="6">$T('saturday')<br/>
<input type="checkbox" name="daysofweek" value="7">$T('sunday')<br/>
</label>
</div>
<div class="field-pair">

View File

@@ -56,7 +56,7 @@
</td>
<td class="download-title">
<a href="nzb/$slot.nzo_id/" title="$T('status'): $T('post-'+$slot.status)<br/>$T('nzo-age'): $slot.avg_age<br/><!--#if $slot.missing#-->$T('missingArt'): $slot.missing<!--#end if#-->">$slot.filename</a>
<a href="nzb/$slot.nzo_id/" title="$T('status'): $T('post-'+$slot.status)<br/>$T('nzo-age'): $slot.avg_age<br/><!--#if $slot.missing#-->$T('missingArt'): $slot.missing<!--#end if#-->">$slot.filename.replace('.', '.&#8203;').replace('_', '_&#8203;')</a>
</td>
<td>

View File

@@ -40,6 +40,7 @@ jQuery(function($){
$('#addID').click(function(){ // also works when hitting enter because of <form>
if ($('#addID_input').val()!='URL') {
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {
@@ -118,15 +119,19 @@ jQuery(function($){
// fix for touch devices -- toggle visibility
$('.sprite_q_menu_pausefor').bind('touchend', function(e) {
e.preventDefault();
if( $(this).hasClass('sprite_q_menu_pauseforsfHover') ) {
$(this).find("ul").toggle();
if (! $.browser.safari) {
e.preventDefault();
if( $(this).hasClass('sprite_q_menu_pauseforsfHover') ) {
$(this).find("ul").toggle();
}
}
});
$('.sprite_q_queue').bind('touchend', function(e) {
e.preventDefault();
if( $(this).hasClass('sprite_q_queuesfHover') ) {
$(this).find("ul").toggle();
if (! $.browser.safari) {
e.preventDefault();
if( $(this).hasClass('sprite_q_queuesfHover') ) {
$(this).find("ul").toggle();
}
}
});
@@ -154,6 +159,7 @@ jQuery(function($){
else
$('#speed-wrapper .sprite_q_menu_pausefor').removeClass('sprite_q_menu_pausefor_on');
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'config', name:'set_speedlimit', value: str, apikey: $.plush.apikey}
@@ -209,6 +215,7 @@ jQuery(function($){
else
$('.sprite_q_queue').removeClass('sprite_q_queue_on');
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'change_complete_action', value: $(this).val(), apikey: $.plush.apikey}
@@ -230,6 +237,7 @@ jQuery(function($){
value="all";
}
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'delete', value:value, del_files:del_files, apikey: $.plush.apikey},
@@ -254,6 +262,7 @@ jQuery(function($){
case 'sortSizeDesc': sort='size'; dir='desc'; break;
}
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'sort', sort: sort, dir: dir, apikey: $.plush.apikey},
@@ -268,6 +277,7 @@ jQuery(function($){
minutes = prompt($(event.target).attr('title'));
$.plush.SetQueuePauseInfo(true,minutes+':00');
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'config', name:'set_pause', value: minutes, apikey: $.plush.apikey},
@@ -278,6 +288,7 @@ jQuery(function($){
// Get Bookmarks
$('#get_bookmarks_now').click(function() {
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'newzbin', name:'get_bookmarks', apikey: $.plush.apikey},
@@ -288,6 +299,7 @@ jQuery(function($){
// Reset Quota
$('#reset_quota_now').click(function() {
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'reset_quota', apikey: $.plush.apikey},
@@ -298,6 +310,7 @@ jQuery(function($){
// Get RSS
$('#get_rss_now').click(function() {
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'rss_now', apikey: $.plush.apikey},
@@ -308,6 +321,7 @@ jQuery(function($){
// Get Watched folder
$('#get_watched_now').click(function() {
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'watched_now', apikey: $.plush.apikey},
@@ -471,6 +485,7 @@ jQuery(function($){
$('#pause_resume').removeClass('sprite_q_pause_on').addClass('sprite_q_pause');
$('#pause_int').html("");
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'resume', apikey: $.plush.apikey}
@@ -479,6 +494,7 @@ jQuery(function($){
$('#pause_resume').removeClass('sprite_q_pause').addClass('sprite_q_pause_on');
$('#pause_int').html("");
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'pause', apikey: $.plush.apikey}
@@ -522,6 +538,7 @@ jQuery(function($){
if ($(this).hasClass('sprite_ql_grip_resume_on')) {
$(this).toggleClass('sprite_ql_grip_resume_on').toggleClass('sprite_ql_grip_pause_on');
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'pause', value: pid, apikey: $.plush.apikey}
@@ -529,6 +546,7 @@ jQuery(function($){
} else {
$(this).toggleClass('sprite_ql_grip_resume_on').toggleClass('sprite_ql_grip_pause_on');
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'resume', value: pid, apikey: $.plush.apikey}
@@ -573,6 +591,7 @@ jQuery(function($){
var nzbid = $(this).parent().parent().attr('id');
var oldPos = $('#'+nzbid)[0].rowIndex + $.plush.queuecurpage * $.plush.queuePerPage;
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'priority', value: nzbid, value2: $(this).val(), apikey: $.plush.apikey},
@@ -595,6 +614,7 @@ jQuery(function($){
var val = $(this).parent().parent().attr('id');
var cval = $(this).attr('class').split(" ")[0]; // ignore added "hovering" class
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode: cval, value: val, value2: $(this).val(), apikey: $.plush.apikey},
@@ -683,6 +703,7 @@ $.plush.queueprevslots = $.plush.queuenoofslots; // for the next refresh
if (table.tBodies[0].rows[i].id == row.id) {
val2 = (i + $.plush.queuecurpage * $.plush.queuePerPage);
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'switch', value: row.id, value2: val2, apikey: $.plush.apikey},
@@ -768,6 +789,7 @@ $("a","#multiops_inputs").click(function(e){
if ($('#multi_status').val())
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:$('#multi_status').val(), value: nzo_ids, apikey: $.plush.apikey}
@@ -775,6 +797,7 @@ $("a","#multiops_inputs").click(function(e){
if ($('#multi_cat').val())
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode: 'change_cat', value: nzo_ids, value2: $('#multi_cat').val(), apikey: $.plush.apikey}
@@ -782,6 +805,7 @@ $("a","#multiops_inputs").click(function(e){
if ($('#multi_priority').val())
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'priority', value: nzo_ids, value2: $('#multi_priority').val(), apikey: $.plush.apikey}
@@ -789,6 +813,7 @@ $("a","#multiops_inputs").click(function(e){
if ($('#multi_pp').val())
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode: 'change_opts', value: nzo_ids, value2: $('#multi_pp').val(), apikey: $.plush.apikey}
@@ -796,6 +821,7 @@ $("a","#multiops_inputs").click(function(e){
if ($('#multi_script').val())
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode: 'change_script', value: nzo_ids, value2: $('#multi_script').val(), apikey: $.plush.apikey}
@@ -866,6 +892,7 @@ $("a","#multiops_inputs").click(function(e){
value="failed";
}
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'history', name:'delete', value:value, del_files:del_files, apikey: $.plush.apikey},
@@ -955,6 +982,7 @@ $("a","#multiops_inputs").click(function(e){
$.plush.pendingHistoryRefresh = true;
$.colorbox.close();
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:mode, name:'delete', value: delid, del_files: del_files, apikey: $.plush.apikey},
@@ -1081,6 +1109,7 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
// Fetch updated content from queue.tmpl
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "queue/",
data: {start: ( page * $.plush.queuePerPage ), limit: $.plush.queuePerPage},
@@ -1136,6 +1165,7 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "history/",
data: data,

View File

@@ -31,7 +31,7 @@
<!--#for $warn in $warnings#-->
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#-->">
<td>$warn.replace("\n","</td><td>")</td></tr>
<td>$warn.replace("\n","</td><td>", 2)</td></tr>
<!--#end for#-->
</table>
<!--#else#-->

View File

@@ -79,6 +79,7 @@ MochiKit.DOM.addLoadEvent(location = "../../#config-rss");
<thead>
<tr>
<th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
<th>&nbsp;</th>
<th>$T('rss-order')</th>
<th>$T('rss-type')</th>
<th>$T('rss-filter')</th>
@@ -92,6 +93,7 @@ MochiKit.DOM.addLoadEvent(location = "../../#config-rss");
<tbody>
<tr id="$feed+new_filter">
<td></td>
<td><input type="checkbox" name="enabled" value="1" checked="checked" /></td>
<td></td>
<td>
<select name="filter_type">
@@ -157,7 +159,9 @@ MochiKit.DOM.addLoadEvent(location = "../../#config-rss");
<td>
<input type="submit" value="$T('rss-delFilter')" onclick="javascript:lr('config/rss/del_rss_filter','index=$fnum&feed=$feed')" />
</td>
<td>
<input type="checkbox" name="enabled" value="1" <!--#if $filter[6] == '1' then 'checked="checked"' else ""#--> />
</td>
<td>
<input type="text" size="3" name="new_index" value=$fnum>
</td>
@@ -213,6 +217,7 @@ MochiKit.DOM.addLoadEvent(location = "../../#config-rss");
<input type="hidden" name="index" value="$fnum"/>
<input type="hidden" name="feed" value="$feed"/>
<input type="submit" value="$T('button-save')" onclick="javascript:submitconfig('config/rss/upd_rss_filter', this,'$feed+$fnum','1' )"/>
<!--#if not $rss[$feed].filter_states[$fnum]#-->&nbsp;&nbsp;$T('Incorrect filter')<!--#end if#-->
</td>
</tr>

View File

@@ -26,17 +26,14 @@ else:
<!--#end for#-->
</select>
<br class="clear" />
<label class="label">$T('sch-frequency'):</label>
<select name="dayofweek">
<option value="*" selected>$T('daily')
<option value="1">$T('monday')
<option value="2">$T('tuesday')
<option value="3">$T('wednesday')
<option value="4">$T('thursday')
<option value="5">$T('friday')
<option value="6">$T('saturday')
<option value="7">$T('sunday')
</select>
<label class="label" for="daysofweek">$T('sch-frequency'):</label>
<input type="checkbox" name="daysofweek" value="1">$T('monday')<br/>
<input type="checkbox" name="daysofweek" value="2">$T('tuesday')<br/>
<input type="checkbox" name="daysofweek" value="3">$T('wednesday')<br/>
<input type="checkbox" name="daysofweek" value="4">$T('thursday')<br/>
<input type="checkbox" name="daysofweek" value="5">$T('friday')<br/>
<input type="checkbox" name="daysofweek" value="6">$T('saturday')<br/>
<input type="checkbox" name="daysofweek" value="7">$T('sunday')<br/>
<br class="clear" />
<label class="label">$T('sch-action'):</label>
<select name="action">

View File

@@ -808,7 +808,7 @@ function lrb(url, extra, refresh)
{
method:'POST',
sendContent:values,
headers: {"Content-Type":"application/x-www-form-urlencoded"}
headers: {"Content-Type":"application/x-www-form-urlencoded", "Cache-Control": "no-cache"}
});
d.addCallback(function(d)
@@ -840,7 +840,7 @@ function lrb(url, extra, refresh)
{
method:'POST',
sendContent:values,
headers: {"Content-Type":"application/x-www-form-urlencoded"}
headers: {"Content-Type":"application/x-www-form-urlencoded", "Cache-Control": "no-cache"}
});
if (saveelement) {

View File

@@ -8,9 +8,8 @@
<br/>
<div id="tips" class="hidden">
$T('wizard-tip1') <span class="bold">$T('wizard-tip2')</span><br/>
<!--#if len($urls) > 1#--><!--#set $s = 's'#--><!--#else#--><!--#set $s = ''#--><!--#end if#-->
<!--#set $tip3 = $T('wizard-tip3') % $s#-->
$tip3:<br/><br/>
<!--#set $tip3 = $T('wizard-tip3') % ''#-->
$tip3<br/><br/>
<div class="quoteBlock">
<!--#set $i = 0#-->
<!--#for $url in $urls#-->

View File

@@ -31,7 +31,7 @@ $T('wizard-explain-server')
<div id="connections-tip" class="tips">$T('wizard-server-con-explain') $T('wizard-server-con-eg')</div>
<div id="connections-error" class="error-text hidden">$T('wizard-server-number')</div>
<br class="clear" />
<label><span class="label">$T('srv-ssl')</span>
<label for="srv-ssl"><!--#if $have_ssl then $T('srv-ssl') else '<span class="disabled-text">'+$T('srv-ssl')+'</span> (pyopenssl (python-ssl) '+$T('opt-notInstalled')+')'#-->
<input class="validate-text" class="radio" type="checkbox" name="ssl" value="1" <!--#if $have_ssl then '' else 'disabled'#--><!--#if $ssl == 1 then 'checked' else ''#-->></label>
<div class="tips">$T('wizard-server-ssl-explain')</div>
<br class="clear" />

103
make_dmg.py Normal file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python -OO
#
# Copyright 2008-2012 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.
#
import os
import sys
import re
if len(sys.argv) < 2:
print 'Usage: %s <release>' % os.path.split(sys.argv[0])[1]
exit(1)
# Setup file names
release = sys.argv[1]
prod = 'SABnzbd-' + release
fileDmg = prod + '-osx.dmg'
fileOSr = prod + '-osx-src.tar.gz'
fileImg = prod + '.sparseimage'
builds = ('sl', 'lion', 'ml')
build_folders = (
'OS X 10.5 and 10.6 (Leopards)',
'OS X 10.7 (Lion)',
'OS X 10.8 (Mountain Lion)'
)
# Check presense of all builds
build_paths = []
for build in builds:
path = os.path.join(os.environ['HOME'], 'project/osx/%s-%s.cpio' % (prod, build))
if os.path.exists(path):
build_paths.append(path)
else:
print 'Missing build %s' % path
exit(1)
# Create sparseimage from template
os.system("unzip -o osx/image/template.sparseimage.zip")
os.rename('template.sparseimage', fileImg)
# mount sparseimage and modify volume label
os.system("hdiutil mount %s | grep /Volumes/SABnzbd >mount.log" % fileImg)
# Rename the volume
fp = open('mount.log', 'r')
data = fp.read()
fp.close()
os.remove('mount.log')
m = re.search(r'/dev/(\w+)\s+', data)
volume = 'SABnzbd-' + str(release)
os.system('diskutil rename %s %s' % (m.group(1), volume))
authority = os.environ.get('SIGNING_AUTH')
# Unpack build into image and sign if possible
for build in xrange(len(builds)):
vol_path = '/Volumes/%s/%s/' % (volume, build_folders[build])
os.system('ditto -x -z "%s" "%s"' % (build_paths[build], vol_path))
if authority:
app_name = '%s-%s' % (volume, builds[build])
os.system('codesign -f -i "%s" -s "%s" "%s/SABnzbd.app"' % (app_name, authority, vol_path))
# Put README.rtf in root
from_path = '/Volumes/%s/%s/SABnzbd.app/Contents/Resources/Credits.rtf' % (volume, build_folders[0])
to_path = '/Volumes/%s/README.rtf' % volume
os.system('cp "%s" "%s"' % (from_path, to_path))
# Unmount sparseimage
print 'Eject volume'
os.system("hdiutil eject /Volumes/%s/>/dev/null" % volume)
print 'Wait 1 second'
os.system("sleep 1")
# Convert sparseimage to read-only compressed dmg
print 'Create DMG file'
if os.path.exists(fileDmg):
os.remove(fileDmg)
os.system("hdiutil convert %s -format UDBZ -o %s>/dev/null" % (fileImg, fileDmg))
# Remove sparseimage
os.system("rm %s>/dev/null" % fileImg)
print 'Make image internet-enabled'
os.system("hdiutil internet-enable %s" % fileDmg)
print 'Copy GZ file'
os.system('cp ~/project/osx/%s .' % fileOSr)

BIN
osx/image/sabnzbd.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

Binary file not shown.

View File

@@ -34,12 +34,13 @@ except ImportError:
try:
import py2app
from setuptools import setup
OSX_LION = [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 7, 0]
OSX_SL = not OSX_LION and [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 6, 0]
OSX_LEOPARD = not (OSX_LION or OSX_SL)
OSX_ML = [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 8, 0]
OSX_LION = not OSX_ML and [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 7, 0]
OSX_SL = not OSX_LION and not OSX_ML
class WindowsError (): pass
except ImportError:
py2app = None
OSX_ML = OSX_LION = OSX_SL = False
VERSION_FILE = 'sabnzbd/version.py'
VERSION_FILEAPP = 'osx/resources/InfoPlist.strings'
@@ -47,8 +48,8 @@ VERSION_FILEAPP = 'osx/resources/InfoPlist.strings'
my_version = 'unknown'
my_baseline = 'unknown'
def DeleteFiles(name):
''' Delete one file or set of files from wild-card spec '''
def delete_files(name):
""" Delete one file or set of files from wild-card spec """
for f in glob.glob(name):
try:
if os.path.exists(f):
@@ -95,7 +96,7 @@ def PatchVersion(name):
try:
pipe = subprocess.Popen(GitStatus, shell=True, stdout=subprocess.PIPE).stdout
for line in pipe.read().split('\n'):
if 'nothing to commit' in line:
if 'nothing to commit' in line or 'nothing added to commit' in line:
state = ''
break
pipe.close()
@@ -151,8 +152,7 @@ def PairList(src):
lst.append((path, flist))
else:
path, name = os.path.split(item)
items = []
items.append(name)
items = [name]
lst.append((path, items))
return lst
@@ -207,7 +207,7 @@ def Dos2Unix(name):
def Unix2Dos(name):
""" Read file, remove \r, replace \n by \r\n and write back """
base, ext = os.path.splitext(name)
if ext.lower() not in ('.py', '.txt', '.css', '.js', '.tmpl', '.sh', '.cmd'):
if ext.lower() not in ('.py', '.txt', '.css', '.js', '.tmpl', '.sh', '.cmd', '.mkd'):
return
print name
@@ -230,9 +230,9 @@ def Unix2Dos(name):
def rename_file(folder, old, new):
oldpath = "%s/%s" % (folder, old)
newpath = "%s/%s" % (folder, new)
try:
oldpath = "%s/%s" % (folder, old)
newpath = "%s/%s" % (folder, new)
if os.path.exists(newpath):
os.remove(newpath)
os.rename(oldpath, newpath)
@@ -246,6 +246,11 @@ print sys.argv[0]
Git = CheckPath('git')
ZipCmd = CheckPath('zip')
UnZipCmd = CheckPath('unzip')
if os.name != 'nt':
PanDoc = CheckPath('pandoc')
else:
PanDoc = None
if os.name == 'nt':
msg = 'Requires the standard version of NSIS'
NSIS = CheckPath('makensis')
@@ -254,7 +259,7 @@ if os.name == 'nt':
os.system('%s >%s' % (NSIS, log))
if 'Unicode' not in open(log).read():
msg = ''
DeleteFiles(log)
delete_files(log)
if msg:
print msg
exit(1)
@@ -293,11 +298,14 @@ Win32TempName = 'SABnzbd-windows.exe'
fileIns = prod + '-win32-setup.exe'
fileBin = prod + '-win32-bin.zip'
fileSrc = prod + '-src.tar.gz'
fileDmg = prod + '-osx.dmg'
fileDmgLp = prod + '-osx-leopard.dmg'
fileDmg_ml = prod + '-osx-mountainlion.dmg'
fileDmg_lion = prod + '-osx-lion.dmg'
fileDmg_sl = prod + '-osx-snowleopard.dmg'
fileOSr = prod + '-osx-src.tar.gz'
fileImg = prod + '.sparseimage'
if OSX_SL: postfix = 'sl'
if OSX_LION: postfix = 'lion'
if OSX_ML: postfix = 'ml'
PatchVersion(release)
@@ -305,7 +313,7 @@ PatchVersion(release)
# List of data elements, directories end with a '/'
data_files = [
'ABOUT.txt',
'README.txt',
'README.mkd',
'INSTALL.txt',
'GPL2.txt',
'GPL3.txt',
@@ -351,34 +359,19 @@ if target == 'app':
os.system(GitRevertVersion)
exit(1)
if not PanDoc:
print "Sorry, requires pandoc in the $PATH"
os.system(GitRevertVersion)
exit(1)
# Check which Python flavour
apple_py = 'ActiveState' not in sys.copyright
if OSX_LION:
# Check if Leopard build is present
leopard_build = '/project/leopard/%s' % str(my_version)
if not os.path.isdir(leopard_build):
print 'Leopard build not found at %s' % leopard_build
exit(1)
# Create sparseimage from template
os.system("unzip -o osx/image/template.sparseimage.zip")
os.rename('template.sparseimage', fileImg)
# mount sparseimage and modify volume label
os.system("hdiutil mount %s | grep /Volumes/SABnzbd >mount.log" % (fileImg))
# Rename the volume
fp = open('mount.log', 'r')
data = fp.read()
fp.close()
os.remove('mount.log')
m = re.search(r'/dev/(\w+)\s+', data)
volume = 'SABnzbd-' + str(my_version)
os.system('disktool -n %s %s' % (m.group(1), volume))
options['description'] = 'SABnzbd ' + str(my_version)
# Remove previous build result
os.system('rm -rf dist/ build/')
# Create MO files
os.system('python ./tools/make_mo.py')
@@ -418,67 +411,63 @@ if target == 'app':
setup_requires=['py2app'],
)
# copy unrar & par2 binary to avoid striping
# Remove 64bit code
if not OSX_SL:
os.system("mv dist/SABnzbd.app dist/SABnzbd.app.temp")
os.system("ditto --arch i386 --arch ppc dist/SABnzbd.app.temp dist/SABnzbd.app/")
os.system("rm -rf dist/SABnzbd.app.temp")
# copy unrar & par2 binary
os.system("mkdir dist/SABnzbd.app/Contents/Resources/osx>/dev/null")
os.system("mkdir dist/SABnzbd.app/Contents/Resources/osx/par2>/dev/null")
os.system("cp -pR osx/par2/ dist/SABnzbd.app/Contents/Resources/osx/par2>/dev/null")
os.system("mkdir dist/SABnzbd.app/Contents/Resources/osx/unrar>/dev/null")
os.system("cp -pR osx/unrar/license.txt dist/SABnzbd.app/Contents/Resources/osx/unrar/ >/dev/null")
if OSX_LION:
os.system("cp -pR osx/unrar/unrar dist/SABnzbd.app/Contents/Resources/osx/unrar/ >/dev/null")
else:
if OSX_SL:
os.system("cp -pR osx/unrar/unrar-leopard dist/SABnzbd.app/Contents/Resources/osx/unrar/unrar >/dev/null")
else:
os.system("cp -pR osx/unrar/unrar dist/SABnzbd.app/Contents/Resources/osx/unrar/ >/dev/null")
os.system("cp icons/sabnzbd.ico dist/SABnzbd.app/Contents/Resources >/dev/null")
os.system("cp README.rtf dist/SABnzbd.app/Contents/Resources/Credits.rtf >/dev/null")
os.system("pandoc -f markdown -t rtf -s -o dist/SABnzbd.app/Contents/Resources/Credits.rtf README.mkd >/dev/null")
os.system("find dist/SABnzbd.app -name .git | xargs rm -rf")
if OSX_LION:
# Sign the App if possible
authority = os.environ.get('SIGNING_AUTH')
if authority:
os.system('codesign -f -i "%s-lion" -s "%s" dist/SABnzbd.app' % (volume, authority))
os.system('codesign -f -i "%s-leopard" -s "%s" %s/dist/SABnzbd.app' % (volume, authority, leopard_build))
# Remove source files to prevent re-compilation, which would invalidate signing
py_ver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
os.system("find dist/SABnzbd.app/Contents/Resources/lib/python%s/Cheetah -name '*.py' | xargs rm" % py_ver)
os.system("find dist/SABnzbd.app/Contents/Resources/lib/python%s/xml -name '*.py' | xargs rm" % py_ver)
os.system('rm dist/SABnzbd.app/Contents/Resources/site.py')
# Add the SabNotifier app
if OSX_ML and os.path.exists('/project/sabnotifier/SABnzbd.app'):
os.system("cp -pR /project/sabnotifier/SABnzbd.app dist/SABnzbd.app/Contents/Resources/")
# copy app to mounted sparseimage
os.system('cp -r dist/SABnzbd.app "/Volumes/%s/OS X 10.6 and Above/" >/dev/null' % volume)
# Add License files
os.mkdir("dist/SABnzbd.app/Contents/Resources/licenses/")
os.system("cp -p licenses/*.txt dist/SABnzbd.app/Contents/Resources/licenses/")
os.system("cp -p *.txt dist/SABnzbd.app/Contents/Resources/licenses/")
# Copy the Leopard build
os.system('cp -r %s/dist/SABnzbd.app "/Volumes/%s/OS X 10.5 and Below/" >/dev/null' % (leopard_build, volume))
os.system("sleep 5")
# Archive result to share
dest_path = '/Volumes/VMware Shared Folders/osx'
if not os.path.exists(dest_path):
dest_path = '$HOME/project/osx'
cpio_path = os.path.join(dest_path, prod) + '-' + postfix + '.cpio'
print 'Create CPIO file %s' % cpio_path
delete_files(cpio_path)
os.system('ditto -c -z dist/ "%s"' % cpio_path)
if OSX_ML:
print 'Create src %s' % fileOSr
delete_files(fileOSr)
os.system('tar -czf %s --exclude ".git*" --exclude "sab*.zip" --exclude "SAB*.tar.gz" --exclude "*.cmd" --exclude "*.pyc" '
'--exclude "*.sparseimage" --exclude "dist" --exclude "build" --exclude "*.nsi" --exclude "win" --exclude "*.dmg" '
'./ >/dev/null' % (fileOSr) )
# Copy README.txt
os.system("cp README.rtf /Volumes/%s/" % volume)
# Remove site.py to prevent re-compilation (otherwise the OSX Firewall may complain)
os.remove('/Volumes/%s/OS X 10.6 and Above/SABnzbd.app/Contents/Resources/site.py' % volume)
os.remove('/Volumes/%s/OS X 10.5 and Below/SABnzbd.app/Contents/Resources/site.py' % volume)
#Unmount sparseimage
os.system("hdiutil eject /Volumes/%s/>/dev/null" % volume)
os.system("sleep 5")
# Convert sparseimage to read only compressed dmg
if os.path.exists(fileDmg):
os.remove(fileDmg)
os.system("hdiutil convert %s -format UDBZ -o %s>/dev/null" % (fileImg, fileDmg))
# Remove sparseimage
os.system("rm %s>/dev/null" % (fileImg))
# Make image internet-enabled
os.system("hdiutil internet-enable %s" % fileDmg)
else:
dest = '/Volumes/VMware Shared Folders/leopard/%s' % str(my_version)
os.system('rm -rf "%s"' % dest)
os.makedirs('%s/dist' % dest)
os.system('cp -r dist/SABnzbd.app "%s/dist/" >/dev/null' % dest)
'--exclude "*.sparseimage*" --exclude "dist" --exclude "build" --exclude "*.nsi" --exclude "win" --exclude "*.dmg" '
'./ >/dev/null' % os.path.join(dest_path, fileOSr) )
os.system(GitRevertApp + VERSION_FILEAPP)
os.system(GitRevertApp + VERSION_FILE)
elif target in ('binary', 'installer'):
if not py2exe:
print "Sorry, only works on Windows!"
@@ -518,12 +507,12 @@ elif target in ('binary', 'installer'):
for tup in options['data_files']:
for file in tup[1]:
name, ext = os.path.splitext(file)
if ext.lower() in ('.txt', '.cmd'):
if ext.lower() in ('.txt', '.cmd', '.mkd'):
Unix2Dos("dist/%s" % file)
DeleteFiles('dist/Sample-PostProc.sh')
DeleteFiles('dist/PKG-INFO')
delete_files('dist/Sample-PostProc.sh')
delete_files('dist/PKG-INFO')
DeleteFiles('*.ini')
delete_files('*.ini')
############################
# Generate the windowed-app
@@ -560,32 +549,40 @@ elif target in ('binary', 'installer'):
############################
# Remove unwanted system DLL files that Py2Exe copies when running on Win7
DeleteFiles(r'dist\lib\API-MS-Win-*.dll')
DeleteFiles(r'dist\lib\MSWSOCK.DLL')
DeleteFiles(r'dist\lib\POWRPROF.DLL')
delete_files(r'dist\lib\API-MS-Win-*.dll')
delete_files(r'dist\lib\MSWSOCK.DLL')
delete_files(r'dist\lib\POWRPROF.DLL')
delete_files(r'dist\lib\KERNELBASE.dll')
############################
# Remove .git residue
DeleteFiles(r'dist\interfaces\Config\.git')
delete_files(r'dist\interfaces\Config\.git')
############################
# Copy Curl if needed
if not (sys.version_info > (2, 5)):
if sys.version_info < (2, 6):
# Curl for Python 2.5
os.system(r'unzip -o win\curl\curl.zip -d dist\lib')
############################
# Fix icon issue with NZB association
os.system(r'copy dist\icons\nzb.ico dist')
############################
# Rename MKD file
rename_file('dist', 'README.mkd', 'README.txt')
############################
if target == 'installer':
DeleteFiles(fileIns)
delete_files(fileIns)
os.system('makensis.exe /v3 /DSAB_PRODUCT=%s /DSAB_VERSION=%s /DSAB_FILE=%s NSIS_Installer.nsi.tmp' % \
(prod, release, fileIns))
DeleteFiles('NSIS_Installer.nsi.tmp')
delete_files('NSIS_Installer.nsi.tmp')
if not os.path.exists(fileIns):
print 'Fatal error creating %s' % fileIns
exit(1)
DeleteFiles(fileBin)
delete_files(fileBin)
os.rename('dist', prod)
os.system('zip -9 -r -X %s %s' % (fileBin, prod))
time.sleep(1.0)
@@ -652,6 +649,10 @@ else:
shutil.copy2(file, dest)
Dos2Unix(fullname)
############################
# Rename MKD file
rename_file(root, 'README.mkd', 'README.txt')
os.chdir(root)
os.chdir('..')
@@ -659,4 +660,3 @@ else:
CreateTar('srcdist', fileSrc, prod)
os.system(GitRevertVersion)

View File

@@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-04-28 12:01+0000\n"
"PO-Revision-Date: 2012-03-14 04:51+0000\n"
"Last-Translator: Rene <Unknown>\n"
"PO-Revision-Date: 2012-08-03 17:24+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: 2012-04-29 05:17+0000\n"
"X-Generator: Launchpad (build 15149)\n"
"X-Launchpad-Export-Date: 2012-08-04 05:38+0000\n"
"X-Generator: Launchpad (build 15742)\n"
#: email/email.tmpl:1
msgid ""
@@ -65,42 +65,42 @@ msgid ""
"<!--#end if#-->\n"
msgstr ""
"##\n"
"## Default Email template for SABnzbd\n"
"## This a Cheetah template\n"
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
"## Standard Email skabelon til SABnzbd\n"
"## Dette er en Cheetah skabelon\n"
"## Dokumentation: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"## Linjeskift og blanktegn er betydelig!\n"
"##\n"
"## These are the email headers\n"
"## Disse er e-mail-headerne \n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
"Subject: SABnzbd har <!--#if $status then \"hentet\" else \"fejlet\" #--> "
"job $name\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## After this comes the body, the empty line is required!\n"
"## Efter dette kommer body, den tomme linje kræves!\n"
"\n"
"Hej,\n"
"<!--#if $status #-->\n"
"SABnzbd har downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
"\"(newzbin #\" + $msgid + \")\"#-->\n"
"SABnzbd har hentet \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin "
"#\" + $msgid + \")\"#-->\n"
"<!--#else#-->\n"
"SABnzbd har ikke downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
"SABnzbd kunne ikke hente \"$name\" <!--#if $msgid==\"\" then \"\" else "
"\"(newzbin #\" + $msgid + \")\"#-->\n"
"<!--#end if#-->\n"
"Færdig kl. $end_time\n"
"Downloaded $size\n"
"Hentet $size\n"
"\n"
"Resultat af job:\n"
"<!--#for $stage in $stages #-->\n"
"Stage $stage <!--#slurp#-->\n"
"Etape $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"
"Output fra bruger script \"$script\" (Exit code = $script_ret):\n"
"$script_output\n"
"<!--#end if#-->\n"
"<!--#if $status #-->\n"
@@ -138,20 +138,20 @@ msgid ""
"Bye\n"
msgstr ""
"##\n"
"## RSS Email template for SABnzbd\n"
"## This a Cheetah template\n"
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
"## RSS Email skabelon til SABnzbd\n"
"## Dette er Cheetah skabelon\n"
"## Dokumentation: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"## Linjeskift og blanktegn er betydelig!\n"
"##\n"
"## These are the email headers\n"
"## Dette er email headers\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd har tilføjet $antal jobs til køen\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## After this comes the body, the empty line is required!\n"
"## Efter dette kommer body, den tomme linje kræves!\n"
"\n"
"Hej,\n"
"\n"
@@ -189,24 +189,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"
"## Dårlig URL Fetch E-mail skabelon for SABnzbd\n"
"## Dette er en Cheetah skabelon\n"
"## Dokumentation: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"## Linjeskift og blanktegn er betydelig!\n"
"##\n"
"## These are the email headers\n"
"## Dette er email headers\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd failed to fetch an NZB\n"
"Subject: SABnzbd kunne ikke hente en NZB\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## After this comes the body, the empty line is required!\n"
"## Efter dette kommer body, den tomme linje kræves!\n"
"\n"
"Hi,\n"
"Hej,\n"
"\n"
"SABnzbd has failed to retrieve the NZB from $url.\n"
"The error message was: $msg\n"
"SABnzbd kunne ikke hente NZB fra $url.\n"
"Fejl meddelelsen er: $msg\n"
"\n"
"Bye\n"
"Farvel\n"

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -13,71 +13,71 @@ msgstr ""
"Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 7bit\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr ""
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr ""
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr ""
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr ""
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
msgstr ""
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr ""
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr ""
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr ""
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr ""
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr ""
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr ""
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
msgid "This system requires the Microsoft runtime library VC90 to be installed first. Do you want to do that now?"
msgstr ""
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr ""
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr ""
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr ""
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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:449
#: NSIS_Installer.nsi:457
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: 2012-05-01 18:51+0000\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-05-03 03:22+0000\n"
"Last-Translator: Rene <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: 2012-05-04 05:14+0000\n"
"X-Generator: Launchpad (build 15195)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Gå til SABnzbd Wiki"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Vis udgivelsesbemærkninger"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Støtte projektet, donere!"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Luk 'SABnzbd.exe' først"
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
@@ -42,31 +42,31 @@ msgstr ""
"produktbemærkningerne eller gå til http://wiki.sabnzbd.org/introducing-0-7-0 "
"!"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Dette vil afinstallere SABnzbd fra dit system"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Kør ved opstart"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Skrivebords ikon"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "NZB filtilknytning"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Slet program"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Slet instillinger"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
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 ""
"Dette system kræver, at Microsoft runtime biblioteket VC90, der skal "
"installeres først. Ønsker du at gøre det nu?"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Downloading Microsoft runtime installer..."
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Download fejl, prøv igen?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr "Kan ikke installere uden runtime bibliotek, prøv igen?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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 ""
"Du kan ikke overskrive en eksisterende installation. Klik `OK` for at fjerne "
"den tidligere version eller `Annuller` for at annullere denne opgradering."
#: NSIS_Installer.nsi:449
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Dine indstillinger og data vil blive opbevaret."

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: 2012-05-01 18:51+0000\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-05-04 14:06+0000\n"
"Last-Translator: shypike <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: 2012-05-05 05:41+0000\n"
"X-Generator: Launchpad (build 15195)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Gehen Sie auf die SABnzbd Wiki-Seite"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Versionshinweise anzeigen"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Bitte unterstützen Sie das Projekt durch eine Spende!"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Schliessen Sie bitte zuerst \"SABnzbd.exe\"."
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
@@ -42,31 +42,31 @@ msgstr ""
"Versionshinweise oder gehen Sie zu http://wiki.sabnzbd.org/introducing-0-7-0 "
"!"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Dies entfernt SABnzbd von Ihrem System"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Beim Systemstart ausführen"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Desktop-Symbol"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "Mit NZB-Dateien verknüpfen"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Programm löschen"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Einstellungen löschen"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
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:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr ""
"Installationsprogramm für Microsoft-Laufzeitbibliothek wird "
"heruntergeladen..."
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Download-Fehler. Erneut versuchen?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr ""
"Installation ohne Laufzeitbibliothek nicht möglich. Erneut versuchen?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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:449
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Ihre Einstellungen und Daten bleiben erhalten."

View File

@@ -7,63 +7,65 @@ msgid ""
msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-05-01 18:51+0000\n"
"PO-Revision-Date: 2012-05-07 01:37+0000\n"
"Last-Translator: Robert Sanchez <rsanch1@gmail.com>\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-07-30 09:38+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: 2012-05-08 05:38+0000\n"
"X-Generator: Launchpad (build 15204)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Vaya al wiki de SABnzbd"
msgstr "Ir al wiki de SABnzbd"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Mostrar notas de la versión"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Apoya el proyecto, haz una donación!"
msgstr "¡Apoye el proyecto, haga una donación!"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Por favor de cierrar primero \"SABnzbd.exe\""
msgstr "Por favor cierre primero \"SABnzbd.exe\""
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
msgstr ""
" >>>> ATENCION <<<<\\r\\n\\r\\nPor favor, compruebe las "
"notas de version o visite http://wiki.sabnzbd.org/introducing-0-7-0 !"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Esto desinstalará SABnzbd de su sistema"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Ejecutar al inicio"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Icono del escritorio"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "Asociación de archivos NZB"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Eliminar programa"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Eliminar Ajustes"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
msgid ""
"This system requires the Microsoft runtime library VC90 to be installed "
"first. Do you want to do that now?"
@@ -71,20 +73,20 @@ msgstr ""
"Este sistema requiere la ejecución de la biblioteca Microsoft runtime VC90 "
"que debe ser instalada. ¿Quieres hacerlo ahora?"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Descargando el instalador de Microsoft runtime..."
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Error en la descarga, ¿probamos de nuevo?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr ""
"No se puede instalar sin la biblioteca runtime, ¿Lo volvemos a intentar?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
msgid ""
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
"the previous version or `Cancel` to cancel this upgrade."
@@ -92,7 +94,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:449
#: NSIS_Installer.nsi:457
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: 2012-05-01 18:51+0000\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-05-02 15:56+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: 2012-05-03 05:55+0000\n"
"X-Generator: Launchpad (build 15185)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Aller sur le Wiki de SABnzbd"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Afficher les notes de version"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Supportez le projet, faites un don !"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Quittez \"SABnzbd.exe\" avant l'installation, SVP"
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
@@ -42,31 +42,31 @@ msgstr ""
"vérifiez d'abord les notes de version ou consultez "
"http://wiki.sabnzbd.org/introducing-0-7-0 !"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Ceci désinstallera SABnzbd de votre système"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Lancer au démarrage"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Icône sur le Bureau"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "Association des fichiers NZB"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Supprimer le programme"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Supprimer les Paramètres"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
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:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Téléchargement de Microsoft runtime installateur ..."
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Erreur téléchargement, réessayer?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr "Impossible d'installer sans bibliothèque d'exécution, réessayer?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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:449
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Vos paramètres et les données seront conservées."

View File

@@ -7,86 +7,86 @@ msgid ""
msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-05-01 18:51+0000\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2011-06-26 10: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: 2012-05-02 05:52+0000\n"
"X-Generator: Launchpad (build 15177)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr ""
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr ""
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr ""
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr ""
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
msgstr ""
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr ""
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr ""
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr ""
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr ""
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr ""
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr ""
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
msgid ""
"This system requires the Microsoft runtime library VC90 to be installed "
"first. Do you want to do that now?"
msgstr ""
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr ""
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr ""
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr ""
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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:449
#: NSIS_Installer.nsi:457
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: 2012-05-01 18:51+0000\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-05-01 18:56+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: 2012-05-02 05:52+0000\n"
"X-Generator: Launchpad (build 15177)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Ga naar de SABnzbd Wiki"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Toon vrijgave bericht"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Steun het project, Doneer!"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Sluit \"SABnzbd.exe\" eerst af"
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
@@ -41,31 +41,31 @@ msgstr ""
" >>>> WAARSCHUWING <<<<\\\\r\\\\n\\\\r\\\\nLees eerst het "
"vrijgave bericht of ga naar http://wiki.sabnzbd.org/introducing-0-7-0 !"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Dit verwijdert SABnzbd van je systeem"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Opstarten bij systeem start"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Pictogram op bureaublad"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "NZB bestanden koppelen aan SABnzbd"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Verwijder programma"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Verwijder instellingen"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
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 ""
"Op dit systeem moeten eerst de Microsoft runtime bibliotheek VC90 "
"geïnstalleerd worden. Wilt u dat nu doen?"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Downloaden van de Microsoft bibliotheek"
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Download mislukt, opnieuw?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr "Installeren heeft geen zin zonder de bibliotheek, opnieuw?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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 ""
"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:449
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Je instellingen en bestanden blijven behouden."

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: 2012-05-01 18:51+0000\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-05-02 10:07+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: 2012-05-03 05:55+0000\n"
"X-Generator: Launchpad (build 15185)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Idź do wiki SABnzbd"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Pokaż informacje o wydaniu"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Wspomóż projekt!"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Najpierw zamknij SABnzbd.exe"
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
@@ -41,31 +41,31 @@ msgstr ""
" >>>> UWAGA <<<<\\r\\n\\r\\nNajpierw przeczytaj informacje "
"o wydaniu lub odwiedź http://wiki.sabnzbd.org/introducing-0-7-0 !"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "To odinstaluje SABnzbd z systemu"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Uruchom wraz z systemem"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Ikona pulpitu"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "powiązanie pliku NZB"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Usuń program"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Skasuj obecne ustawienia"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
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 ""
"Ten system wymaga najpierw zainstalowania bibliotek Microsoft VC90. Czy "
"chcesz wykonać teraz instalację?"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Pobieranie instalatora bibliotek Microsoft..."
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Problem z pobieraniem, spróbować ponownie?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr "Nie można wykonać instalacji bez bibliotek, spróbować ponownie?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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 ""
"Nie można nadpisać istniejącej instalacji. \\n\\n Naciśnij `OK`, aby usunąć "
"poprzednia wersję lub `Anuluj` aby anulować aktualizację."
#: NSIS_Installer.nsi:449
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Twoje ustawienia i dane zostaną zachowane."

View File

@@ -7,66 +7,65 @@ msgid ""
msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-05-01 18:51+0000\n"
"PO-Revision-Date: 2012-05-04 14:13+0000\n"
"Last-Translator: shypike <Unknown>\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-06-17 03:09+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: 2012-05-05 05:41+0000\n"
"X-Generator: Launchpad (build 15195)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Vá para a Wiki SABnzbd"
msgstr "Vá para a Wiki do SABnzbd"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Mostrar Notas de Lançamento"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Apoie o projeto. Faça uma doação!"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Por favor, feche \"SABnzbd.exe\" primeiro"
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
msgstr ""
" >>>> ATENÇÃO <<<<\\\\r\\\\n\\\\r\\\\nPor favor, verifique "
"primeiro as notas de lançamento ou vá até "
"http://wiki.sabnzbd.org/introducing-0-7-0 !"
" >>>> ATENÇÃO <<<<\\r\\n\\r\\nPor favor, verifique primeiro "
"as notas de lançamento ou vá até http://wiki.sabnzbd.org/introducing-0-7-0 !"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Isso irá desinstalar SABnzbd de seu sistema"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Executar na inicialização"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Ícone na Área de Trabalho"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "Associação com Arquivos NZB"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Excluir o Programa"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Apagar Configurações"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
msgid ""
"This system requires the Microsoft runtime library VC90 to be installed "
"first. Do you want to do that now?"
@@ -74,19 +73,19 @@ msgstr ""
"Este sistema precisa que a biblioteca runtime Microsoft VC90 seja instalada "
"antes. Você quer fazer isso agora?"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Baixando o instalador runtime da Microsoft ..."
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Houve um erro de download. Quer tentar novamente?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr "Não é possível instalar sem a biblioteca runtime. Quer repetir?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
msgid ""
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
"the previous version or `Cancel` to cancel this upgrade."
@@ -94,7 +93,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:449
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Suas configurações e os dados serão preservados."

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: 2012-05-01 18:51+0000\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-05-04 12:45+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: 2012-05-05 05:41+0000\n"
"X-Generator: Launchpad (build 15195)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Dute la Wiki SABnzbd"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Arată Notele de Publicare"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Susţine proiectul, Donează!"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Închideţi mai întâi \"SABnzbd.exe\""
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
@@ -42,31 +42,31 @@ msgstr ""
"întâi notele de publicare sau să vizitaţi "
"http://wiki.sabnzbd.org/introducing-0-7-0 !"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Acest lucru va dezinstala SABnzbd din sistem"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Executare la pornire"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Icoană Desktop"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "Asociere cu Fişierele NZB"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Şterge Program"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Ştergeţi Setări"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
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 ""
"Acest sistem necesită librăria Microsoft VC90 instalată. Dortiți să faceți "
"asta acum ?"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Descărcare rutină instalare Microsoft..."
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Eroare descărcare, încerc din nou?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr "Nu pot instala fără rutină librărie, încerc din nou?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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 ""
"Nu puteți suprascrie instalarea existentă. \\n\\nClick `OK` pentru a elimina "
"versiunea anterioară sau `Anulare` pentru a anula actualizarea."
#: NSIS_Installer.nsi:449
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Setările şi informaţiile vor fi salvate."

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: 2012-05-01 18:51+0000\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2012-05-15 19:42+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: 2012-05-16 05:28+0000\n"
"X-Generator: Launchpad (build 15247)\n"
"X-Launchpad-Export-Date: 2012-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
#: NSIS_Installer.nsi:417
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Gå till SABnzbd Wiki"
#: NSIS_Installer.nsi:419
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Visa releasenoteringar"
#: NSIS_Installer.nsi:421
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Donera och stöd detta projekt!"
#: NSIS_Installer.nsi:423
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Var vänlig stäng \"SABnzbd.exe\" först"
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:433
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
@@ -41,31 +41,31 @@ msgstr ""
" >>>> VARNING <<<<\\r\\n\\r\\nVänligen läs först "
"releasenoteringarna eller gå till http://wiki.sabnzbd.org/introducing-0-7-0 !"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Detta kommer att avinstallera SABnzbd från systemet"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Kör vid uppstart"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Skrivbordsikon"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "NZB Filassosication"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Radera programmet"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Radera inställningar"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:447
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 ""
"Detta system kräver att Microsofts runtimebibliotek VC90 är installerat. "
"Vill du göra detta nu?"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Laddar ned Microsofts runtimeinstaller..."
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Misslyckat nedladdningsförsök, försök igen?"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr "Kan inte installera utan runtimebibliotek, försök igen?"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:455
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 ""
"avinstallera tidigare version eller 'Avbryt' för att avbryta denna "
"uppgradering."
#: NSIS_Installer.nsi:449
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Dina inställningar och ditt data kommer att bevaras."

View File

@@ -31,6 +31,10 @@ import subprocess
import time
import cherrypy
from threading import RLock, Lock, Condition, Thread
try:
import sleepless
except ImportError:
sleepless = None
#------------------------------------------------------------------------
# Determine platform flags
@@ -59,6 +63,10 @@ elif os.name == 'posix':
pass
if platform.machine() == 'i386':
DARWIN_INTEL = True
if DARWIN:
DARWIN_ML = [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 8]
else:
DARWIN_ML = False
#------------------------------------------------------------------------
@@ -752,13 +760,21 @@ def empty_queues():
def keep_awake():
""" If we still have work to do, keep Windows system awake
""" If we still have work to do, keep Windows/OSX system awake
"""
global KERNEL32
if KERNEL32 and not sabnzbd.downloader.Downloader.do.paused:
if (not PostProcessor.do.empty()) or not NzbQueue.do.is_empty():
# set ES_SYSTEM_REQUIRED
KERNEL32.SetThreadExecutionState(ctypes.c_int(0x00000001))
if KERNEL32 or sleepless:
if sabnzbd.cfg.keep_awake():
awake = False
if not sabnzbd.downloader.Downloader.do.paused:
if (not PostProcessor.do.empty()) or not NzbQueue.do.is_empty():
awake = True
if KERNEL32:
# set ES_SYSTEM_REQUIRED
KERNEL32.SetThreadExecutionState(ctypes.c_int(0x00000001))
else:
sleepless.keep_awake(u'SABnzbd is busy downloading and/or post-processing')
if not awake and sleepless:
sleepless.allow_sleep()
def CheckFreeSpace():
@@ -1060,16 +1076,9 @@ def active_primaries():
return sabnzbd.downloader.Downloader.do.active_primaries()
def proxy_postproc(nzo):
sabnzbd.postproc.PostProcessor.do.process(nzo)
def proxy_pre_queue(name, pp, cat, script, priority, size, groups):
return sabnzbd.newsunpack.pre_queue(name, pp, cat, script, priority, size, groups)
def proxy_get_history_size():
history_db = sabnzbd.database.get_history_handle()
return history_db.get_history_size()
def proxy_build_history():
""" Proxy to let nzbqueue call api """
return sabnzbd.api.build_history()

View File

@@ -422,6 +422,8 @@ def _api_history(name, output, kwargs):
limit = kwargs.get('limit')
search = kwargs.get('search')
failed_only = kwargs.get('failed_only')
if not limit:
limit = cfg.history_limit()
if name == 'delete':
special = value.lower()
@@ -461,33 +463,6 @@ def _api_get_files(name, output, kwargs):
else:
return report(output, _MSG_NO_VALUE)
def _api_addurl(names, output, kwargs):
""" API: accepts name, output, pp, script, cat, priority, nzbname """
pp = kwargs.get('pp')
script = kwargs.get('script')
cat = kwargs.get('cat')
priority = kwargs.get('priority')
nzbnames = kwargs.get('nzbname')
if not isinstance(names, list):
names = [names]
if not isinstance(nzbnames, list):
nzbnames = [nzbnames]
for n in xrange(len(names)):
name = names[n]
if n < len(nzbnames):
nzbname = nzbnames[n]
else:
nzbname = ''
if name:
name = name.strip()
sabnzbd.add_url(name, pp, script, cat, priority, nzbname)
if len(names) > 0:
return report(output)
else:
return report(output, _MSG_NO_VALUE)
_RE_NEWZBIN_URL = re.compile(r'/browse/post/(\d+)')
def _api_addid(names, output, kwargs):
@@ -796,7 +771,7 @@ _api_table = {
'fullstatus' : _api_fullstatus,
'history' : _api_history,
'get_files' : _api_get_files,
'addurl' : _api_addurl,
'addurl' : _api_addid,
'addid' : _api_addid,
'pause' : _api_pause,
'resume' : _api_resume,
@@ -1165,7 +1140,11 @@ def build_queue(web_dir=None, root=None, verbose=False, prim=True, webdir='', ve
datestart = datetime.datetime.now()
slot['eta'] = 'unknown'
slot['avg_age'] = calc_age(average_date, bool(trans))
if status == Status.GRABBING:
slot['avg_age'] = '---'
else:
slot['avg_age'] = calc_age(average_date, bool(trans))
slot['verbosity'] = ""
if web_dir:
finished = []
@@ -1252,6 +1231,16 @@ def build_queue(web_dir=None, root=None, verbose=False, prim=True, webdir='', ve
return info, pnfo_list, bytespersec, verbose_list, dictn
#------------------------------------------------------------------------------
def fast_queue():
""" Return paused, bytes_left, bpsnow, time_left """
bytes_left = NzbQueue.do.remaining()
paused = Downloader.do.paused
bpsnow = BPSMeter.do.get_bps()
time_left = calc_timeleft(bytes_left, bpsnow)
return paused, bytes_left, bpsnow, time_left
#------------------------------------------------------------------------------
def qstatus_data():
"""Build up the queue status as a nested object and output as a JSON object
@@ -1279,16 +1268,17 @@ def qstatus_data():
state = "IDLE"
if Downloader.do.paused:
state = "PAUSED"
state = Status.PAUSED
elif qnfo[QNFO_BYTES_LEFT_FIELD] / MEBI > 0:
state = "DOWNLOADING"
state = Status.DOWNLOADING
status = {
"state" : state,
"pp_active" : not PostProcessor.do.empty(),
"paused" : Downloader.do.paused,
"pause_int" : scheduler.pause_int(),
"kbpersec" : BPSMeter.do.get_bps() / KIBI,
"speed" : to_units(BPSMeter.do.get_bps(), dec_limit=1),
"kbpersec" : bpsnow / KIBI,
"speed" : to_units(bpsnow, dec_limit=1),
"mbleft" : qnfo[QNFO_BYTES_LEFT_FIELD] / MEBI,
"mb" : qnfo[QNFO_BYTES_FIELD] / MEBI,
"noofslots" : len(pnfo_list),
@@ -1686,9 +1676,11 @@ def build_history(start=None, limit=None, verbose=False, verbose_list=None, sear
# Aquire the db instance
try:
history_db = cherrypy.thread_data.history_db
close_db = False
except:
# Required for repairs at startup because Cherrypy isn't active yet
history_db = get_history_handle()
close_db = True
# Fetch history items
if not h_limit:
@@ -1754,6 +1746,9 @@ def build_history(start=None, limit=None, verbose=False, verbose_list=None, sear
total_items += full_queue_size
fetched_items = len(items)
if close_db:
history_db.close()
return (items, fetched_items, total_items)
@@ -1945,3 +1940,13 @@ def del_from_section(kwargs):
else:
return False
#------------------------------------------------------------------------------
def history_remove_failed():
""" Remove all failed jobs from history, including files """
logging.info('Scheduled removal of all failed jobs')
history_db = get_history_handle()
del_job_files(history_db.get_failed_paths())
history_db.remove_failed()
history_db.close()
del history_db

View File

@@ -35,7 +35,8 @@ except:
import sabnzbd
from sabnzbd.misc import get_filepath, sanitize_filename, get_unique_path, renamer, \
set_permissions
set_permissions, flag_file
from sabnzbd.constants import QCHECK_FILE
import sabnzbd.cfg as cfg
from sabnzbd.articlecache import ArticleCache
from sabnzbd.postproc import PostProcessor
@@ -60,18 +61,18 @@ class Assembler(Thread):
def stop(self):
self.process(None)
def process(self, nzf):
self.queue.put(nzf)
def process(self, job):
self.queue.put(job)
def run(self):
import sabnzbd.nzbqueue
while 1:
nzo_nzf_tuple = self.queue.get()
if not nzo_nzf_tuple:
job = self.queue.get()
if not job:
logging.info("Shutting down")
break
nzo, nzf = nzo_nzf_tuple
nzo, nzf = job
if nzf:
sabnzbd.CheckFreeSpace()
@@ -87,13 +88,17 @@ class Assembler(Thread):
try:
filepath = _assemble(nzf, filepath, dupe)
except IOError, (errno, strerror):
# 28 == disk full => pause downloader
if errno == 28:
logging.error(Ta('Disk full! Forcing Pause'))
if nzo.deleted:
# Job was deleted, ignore error
pass
else:
logging.error(Ta('Disk error on creating file %s'), latin1(filepath))
# Pause without saving
sabnzbd.downloader.Downloader.do.pause(save=False)
# 28 == disk full => pause downloader
if errno == 28:
logging.error(Ta('Disk full! Forcing Pause'))
else:
logging.error(Ta('Disk error on creating file %s'), latin1(filepath))
# Pause without saving
sabnzbd.downloader.Downloader.do.pause(save=False)
except:
logging.error('Fatal error in Assembler', exc_info = True)
break
@@ -109,6 +114,7 @@ class Assembler(Thread):
if check_encrypted_rar(nzo, filepath):
logging.warning(Ta('WARNING: Paused job "%s" because of encrypted RAR file'), latin1(nzo.final_name))
nzo.pause()
nzf.completed = True
else:
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo.nzo_id, add_to_history=False, cleanup=False)
PostProcessor.do.process(nzo)
@@ -174,7 +180,7 @@ def _assemble(nzf, path, dupe):
fout.flush()
fout.close()
set_permissions(path)
set_permissions(path)
if md5:
nzf.md5sum = md5.digest()
del md5
@@ -200,35 +206,37 @@ def file_has_articles(nzf):
# For a full description of the par2 specification, visit:
# http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html
def GetMD5Hashes(fname):
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 = False
new_encoding = True
table = {}
try:
f = open(fname, 'rb')
except:
return table
if force or not flag_file(os.path.split(fname)[0], QCHECK_FILE):
try:
f = open(fname, 'rb')
except:
return table, new_encoding
try:
header = f.read(8)
while header:
name, hash = ParseFilePacket(f, header)
new_encoding |= is_utf8(name)
if name:
table[name] = hash
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 = {}
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()
f.close()
return table, new_encoding

View File

@@ -63,12 +63,12 @@ def this_month(t):
_DAYS = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
def last_month_day(t=None):
def last_month_day(tm):
""" Return last day of this month """
t = t or time.localtime(t)
year, month = time.localtime(t)[:2]
year, month = tm[:2]
day = _DAYS[month]
if day == 28 and (year % 4) == 0 and (year % 400) == 0:
# This simple formula for leap years is good enough
if day == 28 and (year % 4) == 0:
day = 29
return day
@@ -136,8 +136,28 @@ class BPSMeter(object):
sabnzbd.save_admin(data, BYTES_FILE_NAME)
def defaults(self):
""" Get the latest data from the database and assign to a fake server
"""
logging.debug('Setting default BPS meter values')
import sabnzbd.database
history_db = sabnzbd.database.get_history_handle()
grand, month, week = history_db.get_history_size()
history_db.close()
self.grand_total = {}
self.month_total = {}
self.week_total = {}
self.day_total = {}
if grand: self.grand_total['x'] = grand
if month: self.month_total['x'] = month
if week: self.week_total['x'] = week
self.quota = self.left = cfg.quota_size.get_float()
def read(self):
""" Read admin from disk """
""" Read admin from disk, return True when pause is needed
"""
res = False
quota = self.left = cfg.quota_size.get_float() # Quota for this period
self.have_quota = bool(cfg.quota_size())
data = sabnzbd.load_admin(BYTES_FILE_NAME)
@@ -155,16 +175,14 @@ class BPSMeter(object):
self.quota = self.left = cfg.quota_size.get_float()
res = self.reset_quota()
except:
# Get the latest data from the database and assign to a fake server
logging.debug('Setting default BPS meter values')
grand, month, week = sabnzbd.proxy_get_history_size()
if grand: self.grand_total['x'] = grand
if month: self.month_total['x'] = month
if week: self.week_total['x'] = week
self.quota = self.left = cfg.quota_size.get_float()
res = False
# Force update of counters
self.update()
self.defaults()
# Force update of counters and validate data
try:
for server in self.grand_total:
self.update(server)
except TypeError:
self.defaults()
self.update()
return res
@@ -292,20 +310,21 @@ class BPSMeter(object):
tm = time.localtime(t)
if self.q_period == 'd':
nx = (tm[0], tm[1], tm[2], self.q_hour, self.q_minute, 0, 0, 0, tm[8])
if (tm.tm_hour + tm.tm_min * 60) >= (self.q_hour + self.q_minute * 60):
if (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute):
# If today's moment has passed, it will happen tomorrow
t = time.mktime(nx) + 24 * 3600
tm = time.localtime(t)
elif self.q_period == 'w':
if self.q_day < tm.tm_wday+1 or (self.q_day == tm.tm_wday+1 and (tm.tm_hour + tm.tm_min * 60) >= (self.q_hour + self.q_minute * 60)):
if self.q_day < tm.tm_wday+1 or (self.q_day == tm.tm_wday+1 and (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute)):
tm = time.localtime(next_week(t))
dif = abs(self.q_day - tm.tm_wday - 1)
t = time.mktime(tm) + dif * 24 * 3600
tm = time.localtime(t)
elif self.q_period == 'm':
if self.q_day < tm.tm_mday or (self.q_day == tm.tm_mday and (tm.tm_hour + tm.tm_min * 60) >= (self.q_hour + self.q_minute * 60)):
if self.q_day < tm.tm_mday or (self.q_day == tm.tm_mday and (tm.tm_hour * 60 + tm.tm_min) >= (self.q_hour * 60 + self.q_minute)):
tm = time.localtime(next_month(t))
tm = (tm[0], tm[1], self.q_day, self.q_hour, self.q_minute, 0, 0, 0, tm[8])
day = min(last_month_day(tm), self.q_day)
tm = (tm[0], tm[1], day, self.q_hour, self.q_minute, 0, 0, 0, tm[8])
else:
return
tm = (tm[0], tm[1], tm[2], self.q_hour, self.q_minute, 0, 0, 0, tm[8])
@@ -330,7 +349,12 @@ class BPSMeter(object):
self.have_quota = bool(cfg.quota_size())
if self.have_quota:
quota = cfg.quota_size.get_float()
self.left = quota - (self.quota - self.left)
if self.quota:
# Quota change, recalculate amount left
self.left = quota - (self.quota - self.left)
else:
# If previously no quota, self.left holds this period's usage
self.left = quota - self.left
self.quota = quota
else:
self.quota = self.left = 0L
@@ -360,8 +384,14 @@ class BPSMeter(object):
if m:
self.q_hour = int(m.group(1))
self.q_minute = int(m.group(2))
self.q_day = max(1, self.q_day)
self.q_day = min(7, self.q_day)
if self.q_period == 'w':
self.q_day = max(1, self.q_day)
self.q_day = min(7, self.q_day)
elif self.q_period == 'm':
self.q_day = max(1, self.q_day)
self.q_day = min(31, self.q_day)
else:
self.q_day = 1
self.change_quota(allow_resume=False)
return quota_handler, self.q_hour, self.q_minute
else:

View File

@@ -24,7 +24,7 @@ import sabnzbd
from sabnzbd.constants import DEF_HOST, DEF_PORT_WIN_SSL, DEF_PORT_WIN, DEF_STDINTF, \
DEF_DOWNLOAD_DIR, DEF_NZBBACK_DIR, DEF_PORT_UNIX_SSL, \
NORMAL_PRIORITY, DEF_SCANRATE, DEF_PORT_UNIX, DEF_COMPLETE_DIR, \
DEF_ADMIN_DIR
DEF_ADMIN_DIR, NOTIFY_KEYS
from sabnzbd.config import OptionBool, OptionNumber, OptionPassword, \
OptionDir, OptionStr, OptionList, no_nonsense, \
validate_octal, validate_safedir, validate_dir_exists, \
@@ -65,7 +65,7 @@ else:
# Configuration instances
#
quick_check = OptionBool('misc', 'quick_check', True)
fail_on_crc = OptionBool('misc', 'fail_on_crc', False)
fail_on_crc = OptionBool('misc', 'fail_on_crc', True)
send_group = OptionBool('misc', 'send_group', False)
sfv_check = OptionBool('misc', 'sfv_check', True)
@@ -79,7 +79,7 @@ email_full = OptionBool('misc', 'email_full', False)
email_dir = OptionDir('misc', 'email_dir', create=True)
email_rss = OptionBool('misc', 'email_rss', False)
version_check = OptionBool('misc', 'check_new_rel', True)
version_check = OptionNumber('misc', 'check_new_rel', 1)
autobrowser = OptionBool('misc', 'auto_browser', True)
replace_illegal = OptionBool('misc', 'replace_illegal', True)
pre_script = OptionStr('misc', 'pre_script', 'None')
@@ -92,6 +92,7 @@ enable_tsjoin = OptionBool('misc', 'enable_tsjoin', True)
enable_par_cleanup = OptionBool('misc', 'enable_par_cleanup', True)
never_repair = OptionBool('misc', 'never_repair', False)
ignore_unrar_dates = OptionBool('misc', 'ignore_unrar_dates', False)
overwrite_files = OptionBool('misc', 'overwrite_files', False)
par_option = OptionStr('misc', 'par_option', '', validation=no_nonsense)
nice = OptionStr('misc', 'nice', '', validation=no_nonsense)
@@ -129,6 +130,7 @@ safe_postproc = OptionBool('misc', 'safe_postproc', True)
pause_on_post_processing = OptionBool('misc', 'pause_on_post_processing', False)
ampm = OptionBool('misc', 'ampm', False)
rss_filenames = OptionBool('misc', 'rss_filenames', False)
rss_odd_titles = OptionList('misc', 'rss_odd_titles', ['nzbindex.nl/', 'nzbindex.com/', 'nzbclub.com/'])
schedules = OptionList('misc', 'schedlines')
@@ -151,6 +153,8 @@ date_categories = OptionStr('misc', 'date_categories', ['tv'])
matrix_username = OptionStr('nzbmatrix', 'username')
matrix_apikey = OptionStr('nzbmatrix', 'apikey')
matrix_del_bookmark = OptionBool('nzbmatrix', 'del_bookmark', True)
xxx_username = OptionStr('nzbxxx', 'username')
xxx_apikey = OptionStr('nzbxxx', 'apikey')
configlock = OptionBool('misc', 'config_lock', 0)
@@ -186,7 +190,7 @@ login_realm = OptionStr('misc', 'login_realm', 'SABnzbd')
bandwidth_limit = OptionNumber('misc', 'bandwidth_limit', 0)
refresh_rate = OptionNumber('misc', 'refresh_rate', 0)
rss_rate = OptionNumber('misc', 'rss_rate', 60, 15, 24*60)
cache_limit = OptionStr('misc', 'cache_limit', '200M')
cache_limit = OptionStr('misc', 'cache_limit')
web_dir = OptionStr('misc', 'web_dir', DEF_STDINTF)
web_dir2 = OptionStr('misc', 'web_dir2')
web_color = OptionStr('misc', 'web_color', '')
@@ -203,13 +207,14 @@ log_new = OptionBool('logging', 'log_new', False)
https_cert = OptionDir('misc', 'https_cert', 'server.cert', create=False)
https_key = OptionDir('misc', 'https_key', 'server.key', create=False)
https_chain = OptionDir('misc','https_chain', create=False)
enable_https = OptionBool('misc', 'enable_https', False)
language = OptionStr('misc', 'language', 'en')
ssl_type = OptionStr('misc', 'ssl_type', 'v23')
unpack_check = OptionBool('misc', 'unpack_check', True)
no_penalties = OptionBool('misc', 'no_penalties', False)
random_server_ip = OptionBool('misc', 'random_server_ip', True)
randomize_server_ip = OptionBool('misc', 'randomize_server_ip', False)
# Internal options, not saved in INI file
debug_delay = OptionNumber('misc', 'debug_delay', 0, add=False)
@@ -221,11 +226,14 @@ api_warnings = OptionBool('misc', 'api_warnings', True)
max_art_tries = OptionNumber('misc', 'max_art_tries', 3, 2)
max_art_opt = OptionBool('misc', 'max_art_opt', False)
use_pickle = OptionBool('misc', 'use_pickle', False)
no_ipv6 = OptionBool('misc', 'no_ipv6', False)
growl_server = OptionStr('growl', 'growl_server')
growl_password = OptionPassword('growl', 'growl_password')
growl_enable = OptionBool('growl', 'growl_enable', True)
growl_enable = OptionBool('growl', 'growl_enable', not sabnzbd.DARWIN_ML)
ntfosd_enable = OptionBool('growl', 'ntfosd_enable', not sabnzbd.WIN32 and not sabnzbd.DARWIN)
ncenter_enable = OptionBool('growl', 'ncenter_enable', sabnzbd.DARWIN)
notify_classes = OptionList('growl', 'notify_classes', NOTIFY_KEYS)
quota_size = OptionStr('misc', 'quota_size')
quota_day = OptionStr('misc', 'quota_day')
@@ -234,10 +242,14 @@ quota_period = OptionStr('misc', 'quota_period', 'm')
osx_menu = OptionBool('misc', 'osx_menu', True)
osx_speed = OptionBool('misc', 'osx_speed', True)
keep_awake = OptionBool('misc', 'keep_awake', True)
win_menu = OptionBool('misc', 'win_menu', True)
uniconfig = OptionBool('misc', 'uniconfig', True)
allow_incomplete_nzb = OptionBool('misc', 'allow_incomplete_nzb', False)
marker_file = OptionStr('misc', 'nomedia_marker', '')
wait_ext_drive = OptionNumber('misc', 'wait_ext_drive', 5, 1, 60)
history_limit = OptionNumber('misc', 'history_limit', 50, 0)
show_sysload = OptionNumber('misc', 'show_sysload', 2, 0, 2)
#------------------------------------------------------------------------------
# Set root folders for Folder config-items
@@ -257,3 +269,4 @@ def set_root_folders(home, lcldata):
def set_root_folders2():
https_cert.set_root(admin_dir.get_path())
https_key.set_root(admin_dir.get_path())
https_chain.set_root(admin_dir.get_path())

View File

@@ -22,6 +22,7 @@ sabnzbd.config - Configuration Support
import os
import logging
import threading
import shutil
import sabnzbd.misc
from sabnzbd.constants import CONFIG_VERSION, NORMAL_PRIORITY, DEFAULT_PRIORITY
from sabnzbd.utils import listquote
@@ -138,7 +139,7 @@ class OptionNumber(Option):
else:
value = float(value)
except ValueError:
value = 0
value = self._Option__default_val
if self.__validation:
error, val = self.__validation(value)
self._Option__set(val)
@@ -633,11 +634,26 @@ def delete(section, keyword):
@synchronized(SAVE_CONFIG_LOCK)
def read_config(path):
""" Read the complete INI file and check its version number
if OK, pass values to config-database
"""
return _read_config(path)
def _read_config(path, try_backup=False):
""" Read the complete INI file and check its version number
if OK, pass values to config-database
"""
global CFG, database, modified
if try_backup or not os.path.exists(path):
# Not found, try backup
try:
shutil.copyfile(path + '.bak', path)
try_backup = True
except IOError:
pass
if not os.path.exists(path):
# No file found, create default INI file
try:
@@ -659,7 +675,10 @@ def read_config(path):
except (KeyError, ValueError):
CFG['__version__'] = CONFIG_VERSION
except configobj.ConfigObjError, strerror:
return False, '"%s" is not a valid configuration file<br>Error message: %s' % (path, strerror)
if try_backup:
return False, '"%s" is not a valid configuration file<br>Error message: %s' % (path, strerror)
else:
return _read_config(path, True)
CFG['__version__'] = CONFIG_VERSION
@@ -731,46 +750,32 @@ def save_config(force=False):
else:
CFG[sec][kw] = value
res = False
filename = CFG.filename
bakname = filename + '.bak'
# Check if file is writable
if not sabnzbd.misc.is_writable(filename):
logging.error(Ta('Cannot write to INI file %s'), filename)
return res
# copy current file to backup
try:
# Check if file is writable
if not sabnzbd.misc.is_writable(filename):
logging.error(Ta('Cannot write to INI file %s'), filename)
modified = False
return False
shutil.copyfile(filename, bakname)
except:
# Something wrong with the backup,
logging.warning(Ta('Cannot create backup file for %s'), bakname)
logging.info("Traceback: ", exc_info = True)
# Read current content
f = open(filename)
data = f.read()
f.close()
tmpname = filename + '.tmp'
bakname = filename + '.bak'
# Write new file
f = open(tmpname, 'w')
f.write(data)
f.close()
# Update temp file content
CFG.filename = tmpname
# Write new config file
try:
CFG.write()
# Rename to backup
if os.path.isfile(bakname):
os.remove(bakname)
os.rename(filename, bakname)
# Rename temp file, overwriting old one
os.rename(tmpname, filename)
modified = False
res = True
except:
logging.error(Ta('Cannot create backup file for %s'), filename)
logging.error(Ta('Cannot write to INI file %s'), filename)
logging.info("Traceback: ", exc_info = True)
res = False
CFG.filename = filename
return res

View File

@@ -63,6 +63,7 @@ TERM_FLAG_FILE = 'running.sab'
FUTURE_Q_FOLDER = 'future'
JOB_ADMIN = '__ADMIN__'
VERIFIED_FILE = '__verified__'
QCHECK_FILE = '__skip_qcheck__'
ATTRIB_FILE = 'SABnzbd_attrib'
REPAIR_REQUEST = 'repair-all.sab'
@@ -139,7 +140,7 @@ date_match = [r'(\d{4})\W(\d{1,2})\W(\d{1,2})', #2008-10-16
year_match = r'[\W]([1|2]\d{3})([^\w]|$)' # Something '(YYYY)' or '.YYYY.' or ' YYYY '
sample_match = r'((^|[\W_])sample\d*[\W_])|(-s\.)' # something-sample.avi something-s.avi
sample_match = r'((^|[\W_])sample\d*[\W_])|(-s\.\w+$)' # something-sample.avi something-s.avi
class Status():
COMPLETED = 'Completed'
@@ -156,3 +157,5 @@ class Status():
REPAIRING = 'Repairing'
RUNNING = 'Running'
VERIFYING = 'Verifying'
NOTIFY_KEYS = ('startup', 'download', 'pp', 'complete', 'other')

View File

@@ -37,6 +37,7 @@ from sabnzbd.articlecache import ArticleCache
import sabnzbd.downloader
import sabnzbd.cfg as cfg
from sabnzbd.encoding import name_fixer
from sabnzbd.misc import match_str
#-------------------------------------------------------------------------------
@@ -85,18 +86,19 @@ class Decoder(Thread):
data = None
register = True # Finish article
found = True # Article found (only relevant for precheck)
found = False # Proper article found
if lines:
logme = None
try:
if nzo.precheck and '223' in lines[0]:
raise IndexError
if nzo.precheck:
raise BadYenc
register = True
logging.debug("Decoding %s", article)
data = decode(article, lines)
nzf.article_count += 1
found = True
except IOError, e:
logme = Ta('Decoding %s failed') % article
logging.info(logme)
@@ -120,25 +122,45 @@ class Decoder(Thread):
register = False
logme = None
except BadYenc, e:
logme = Ta('Badly formed yEnc article in %s') % article
logging.info(logme)
except BadYenc:
# Handles precheck and badly formed articles
killed = False
found = False
if nzo.precheck and lines and lines[0].startswith('223 '):
# STAT was used, so we only get a status code
found = True
else:
# Examine headers (for precheck) or body (for download)
# And look for DMCA clues (while skipping "X-" headers)
for line in lines:
if not line.startswith('X-') and match_str(line.lower(), ('dmca', 'removed', 'cancel', 'blocked')):
logging.info('Article removed from server (%s)', article)
killed = True
break
if nzo.precheck:
if found or not killed:
# Pre-check, proper article found, just register
logging.debug('Server has article %s', article)
register = True
elif not killed and not found:
logme = Ta('Badly formed yEnc article in %s') % article
logging.info(logme)
if cfg.fail_on_crc():
if not found:
new_server_found = self.__search_new_server(article)
if new_server_found:
register = False
logme = None
except IndexError:
# Pre-check, article found, just register
register = True
except:
logme = Ta('Unknown Error while decoding %s') % article
logging.info(logme)
logging.info("Traceback: ", exc_info = True)
pass
new_server_found = self.__search_new_server(article)
if new_server_found:
register = False
logme = None
if logme:
article.nzf.nzo.inc_log('bad_art_log', logme)
@@ -211,19 +233,22 @@ def decode(article, data):
#Deal with non-yencoded posts
if not ybegin:
found = False
for i in xrange(10):
if data[i].startswith('begin '):
nzf.filename = name_fixer(data[i].split(None, 2)[2])
nzf.type = 'uu'
found = True
break
if found:
for n in xrange(i+1):
data.pop(0)
if data[-1] == 'end':
data.pop()
if data[-1] == '`':
try:
for i in xrange(10):
if data[i].startswith('begin '):
nzf.filename = name_fixer(data[i].split(None, 2)[2])
nzf.type = 'uu'
found = True
break
if found:
for n in xrange(i+1):
data.pop(0)
if data[-1] == 'end':
data.pop()
if data[-1] == '`':
data.pop()
except IndexError:
raise BadYenc()
decoded_data = '\r\n'.join(data)

View File

@@ -289,7 +289,7 @@ class DirScanner(threading.Thread):
for filename in files:
path = os.path.join(folder, filename)
if os.path.isdir(path) or path in self.ignored:
if os.path.isdir(path) or path in self.ignored or filename[0] == '.':
continue
ext = os.path.splitext(path)[1].lower()

View File

@@ -86,7 +86,7 @@ class Server(object):
def hostip(self):
""" Return a random entry from the possible IPs
"""
if cfg.random_server_ip() and self.info and len(self.info) > 1:
if cfg.randomize_server_ip() and self.info and len(self.info) > 1:
rnd = random.randint(0, len(self.info)-1)
ip = self.info[rnd][4][0]
logging.debug('For server %s, using IP %s' % (self.host, ip))
@@ -531,7 +531,10 @@ class Downloader(Thread):
if server.active:
server.errormsg = Ta('Cannot connect to server %s [%s]') % ('', display_msg)
logging.warning(Ta('Cannot connect to server %s [%s]'), '%s:%s' % (server.host, server.port), msg)
penalty = _PENALTY_502
if clues_pay(msg):
penalty = _PENALTY_PERM
else:
penalty = _PENALTY_502
block = True
else:
# Unknown error, just keep trying
@@ -818,3 +821,13 @@ def clues_too_many_ip(text):
if clue in text:
return True
return False
def clues_pay(text):
""" Check for messages about payments
"""
text = text.lower()
for clue in ('credits', 'paym', 'expired'):
if clue in text:
return True
return False

View File

@@ -24,11 +24,13 @@ import os.path
import logging
import socket
import time
import subprocess
from threading import Thread
import sabnzbd
import sabnzbd.cfg
from sabnzbd.encoding import unicoder, latin1
from sabnzbd.constants import NOTIFY_KEYS
from gntp import GNTPRegister
from gntp.notifier import GrowlNotifier
try:
@@ -51,33 +53,36 @@ except:
#------------------------------------------------------------------------------
# Define translatable message table
TT = lambda x:x
_NOTIFICATION = {
NOTIFICATION = {
'startup' : TT('Startup/Shutdown'), #: Message class for Growl server
'download' : TT('Added NZB'), #: Message class for Growl server
'pp' : TT('Post-processing started'), #: Message class for Growl server
'complete' : TT('Job finished'), #: Message class for Growl server
'other' : TT('Other Messages') #: Message class for Growl server
}
_KEYS = ('startup', 'download', 'pp', 'complete', 'other')
#------------------------------------------------------------------------------
# Setup platform dependent Growl support
#
_GROWL_ICON = None # Platform-dependant icon path
_GROWL = None # Instance of the Notifier after registration
_GROWL_REG = False # Succesful registration
#------------------------------------------------------------------------------
def get_icon(host):
if host is None:
icon = os.path.join(os.path.join(sabnzbd.DIR_PROG, 'icons'), 'sabnzbd.ico')
if not os.path.isfile(icon):
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico')
if not os.path.isfile(icon):
icon = None
def get_icon():
icon = os.path.join(os.path.join(sabnzbd.DIR_PROG, 'icons'), 'sabnzbd.ico')
if not os.path.isfile(icon):
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico')
if os.path.isfile(icon):
if sabnzbd.WIN32 or sabnzbd.DARWIN:
fp = open(icon, 'rb')
icon = fp.read()
fp.close
else:
# Due to a bug in GNTP, need this work-around for Linux/Unix
icon = 'http://sabnzbdplus.sourceforge.net/version/sabnzbd.ico'
else:
icon = 'http://sabnzbdplus.sourceforge.net/version/sabnzbd.ico'
icon = None
return icon
@@ -101,24 +106,24 @@ def send_notification(title , msg, gtype, wait=False):
""" Send Notification message
Return '' when OK, otherwise an error string
"""
msg1 = ''
msg2 = ''
if sabnzbd.cfg.growl_enable():
if _HAVE_CLASSIC_GROWL and not sabnzbd.cfg.growl_server():
return send_local_growl(title, msg, gtype)
else:
if wait:
msg1 = send_growl(title, msg, gtype)
res = []
if gtype in sabnzbd.cfg.notify_classes() or wait:
if sabnzbd.DARWIN_ML and sabnzbd.cfg.ncenter_enable():
res.append(send_notification_center(title, msg, gtype))
if sabnzbd.cfg.growl_enable():
if _HAVE_CLASSIC_GROWL and not sabnzbd.cfg.growl_server():
return send_local_growl(title, msg, gtype)
else:
msg1 = 'ok'
Thread(target=send_growl, args=(title, msg, gtype)).start()
time.sleep(0.5)
if have_ntfosd():
msg2 = send_notify_osd(title, msg)
if msg1 and msg2:
return '%s / %s' % (msg1, msg2)
return msg1 or msg2
if wait:
res.append(send_growl(title, msg, gtype))
else:
res.append('ok')
Thread(target=send_growl, args=(title, msg, gtype)).start()
time.sleep(0.5)
if have_ntfosd():
res.append(send_notify_osd(title, msg))
return ' / '.join([r for r in res if r])
#------------------------------------------------------------------------------
def reset_growl():
@@ -136,10 +141,7 @@ def register_growl():
error = None
host, port = sabnzbd.misc.split_host(sabnzbd.cfg.growl_server())
if host:
sys_name = '@' + sabnzbd.misc.hostname().lower()
else:
sys_name = ''
sys_name = hostname(host)
# Clean up persistent data in GNTP to make re-registration work
GNTPRegister.notifications = []
@@ -147,9 +149,9 @@ def register_growl():
growler = GrowlNotifier(
applicationName = 'SABnzbd%s' % sys_name,
applicationIcon = get_icon(host or None),
notifications = [Tx(_NOTIFICATION[key]) for key in _KEYS],
hostname = host or None,
applicationIcon = get_icon(),
notifications = [Tx(NOTIFICATION[key]) for key in NOTIFY_KEYS],
hostname = host or 'localhost',
port = port or 23053,
password = sabnzbd.cfg.growl_password() or None
)
@@ -172,6 +174,7 @@ def register_growl():
except:
error = 'Unknown Growl registration error'
logging.debug(error)
logging.info("Traceback: ", exc_info = True)
del growler
ret = None
@@ -191,16 +194,14 @@ def send_growl(title , msg, gtype):
if _GROWL:
assert isinstance(_GROWL, GrowlNotifier)
_GROWL_REG = True
if not isinstance(msg, str): msg = str(msg)
if not isinstance(msg, str) and not isinstance(msg, unicode):
msg = str(msg)
logging.debug('Send to Growl: %s %s %s', gtype, latin1(title), latin1(msg))
try:
ret = _GROWL.notify(
noteType = Tx(_NOTIFICATION.get(gtype, 'other')),
noteType = Tx(NOTIFICATION.get(gtype, 'other')),
title = title,
description = unicoder(msg),
#icon = options.icon,
#sticky = options.sticky,
#priority = options.priority
)
if ret is None or isinstance(ret, bool):
return None
@@ -237,7 +238,7 @@ if _HAVE_CLASSIC_GROWL:
""" Send to local Growl server, OSX-only """
global _local_growl
if not _local_growl:
notes = [Tx(_NOTIFICATION[key]) for key in _KEYS]
notes = [Tx(NOTIFICATION[key]) for key in NOTIFY_KEYS]
_local_growl = Growl.GrowlNotifier(
applicationName = 'SABnzbd',
applicationIcon = _OSX_ICON,
@@ -245,7 +246,7 @@ if _HAVE_CLASSIC_GROWL:
defaultNotifications = notes
)
_local_growl.register()
_local_growl.notify(Tx(_NOTIFICATION.get(gtype, 'other')), title, msg)
_local_growl.notify(Tx(NOTIFICATION.get(gtype, 'other')), title, msg)
return None
@@ -276,3 +277,47 @@ if _HAVE_NTFOSD:
return error
else:
return 'Not enabled'
def ncenter_path():
""" Return path of Notification Center tool, if it exists """
tool = os.path.normpath(os.path.join(sabnzbd.DIR_PROG, '../Resources/SABnzbd.app/Contents/MacOS/SABnzbd'))
if os.path.exists(tool):
return tool
else:
return None
def send_notification_center(title, msg, gtype):
""" Send message to Mountain Lion's Notification Center """
tool = ncenter_path()
if tool:
try:
command = [tool, '-title', title, '-message', msg, '-group', Tx(NOTIFICATION.get(gtype, 'other'))]
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
output = proc.stdout.read()
proc.wait()
if 'Notification delivered' in output:
output = ''
except:
logging.info('Cannot run notifier "%s"', tool)
logging.debug("Traceback: ", exc_info = True)
output = 'Notifier tool crashed'
else:
output = 'Notifier app not found'
return output.strip('*\n ')
#------------------------------------------------------------------------------
def hostname(host=True):
""" Return host's pretty name """
if sabnzbd.WIN32:
sys_name = os.environ.get('computername', 'unknown')
else:
try:
sys_name = os.uname()[1]
except:
sys_name = 'unknown'
if host:
return '@%s' % sys_name.lower()
else:
return ''

View File

@@ -1188,12 +1188,15 @@ SPECIAL_BOOL_LIST = \
'queue_complete_pers', 'api_warnings', 'allow_64bit_tools', 'par2_multicore',
'never_repair', 'allow_streaming', 'ignore_unrar_dates', 'rss_filenames',
'osx_menu', 'osx_speed', 'win_menu', 'uniconfig', 'use_pickle', 'allow_incomplete_nzb',
'random_server_ip'
'randomize_server_ip', 'no_ipv6', 'keep_awake', 'overwrite_files'
)
SPECIAL_VALUE_LIST = \
( 'size_limit', 'folder_max_length', 'fsys_type', 'movie_rename_limit', 'nomedia_marker',
'req_completion_rate'
'req_completion_rate', 'wait_ext_drive', 'history_limit', 'show_sysload'
)
SPECIAL_LIST_LIST = \
( 'rss_odd_titles',
)
class ConfigSpecial(object):
def __init__(self, web_dir, root, prim):
@@ -1212,6 +1215,7 @@ class ConfigSpecial(object):
conf['switches'] = [ (kw, config.get_config('misc', kw)(), config.get_config('misc', kw).default()) for kw in SPECIAL_BOOL_LIST]
conf['entries'] = [ (kw, config.get_config('misc', kw)(), config.get_config('misc', kw).default()) for kw in SPECIAL_VALUE_LIST]
conf['entries'].extend( [ (kw, config.get_config('misc', kw).get_string(), '') for kw in SPECIAL_LIST_LIST] )
template = Template(file=os.path.join(self.__web_dir, 'config_special.tmpl'),
filter=FILTER, searchList=[conf], compilerSettings=DIRECTIVES)
@@ -1222,7 +1226,7 @@ class ConfigSpecial(object):
msg = check_session(kwargs)
if msg: return msg
for kw in SPECIAL_BOOL_LIST + SPECIAL_VALUE_LIST:
for kw in SPECIAL_BOOL_LIST + SPECIAL_VALUE_LIST + SPECIAL_LIST_LIST:
item = config.get_config('misc', kw)
value = kwargs.get(kw)
msg = item.set(value)
@@ -1237,7 +1241,7 @@ class ConfigSpecial(object):
GENERAL_LIST = (
'host', 'port', 'username', 'password', 'disable_api_key',
'refresh_rate', 'cache_limit',
'enable_https', 'https_port', 'https_cert', 'https_key'
'enable_https', 'https_port', 'https_cert', 'https_key', 'https_chain'
)
class ConfigGeneral(object):
@@ -1335,6 +1339,7 @@ class ConfigGeneral(object):
conf['https_port'] = cfg.https_port()
conf['https_cert'] = cfg.https_cert()
conf['https_key'] = cfg.https_key()
conf['https_chain'] = cfg.https_chain()
conf['enable_https'] = cfg.enable_https()
conf['username'] = cfg.username()
conf['password'] = cfg.password.get_stars()
@@ -1343,6 +1348,7 @@ class ConfigGeneral(object):
conf['cache_limit'] = cfg.cache_limit()
conf['cleanup_list'] = cfg.cleanup_list.get_string()
conf['nzb_key'] = cfg.nzb_key()
conf['my_lcldata'] = cfg.admin_dir.get_path()
template = Template(file=os.path.join(self.__web_dir, 'config_general.tmpl'),
filter=FILTER, searchList=[conf], compilerSettings=DIRECTIVES)
@@ -1506,6 +1512,18 @@ class ConfigServer(object):
BPSMeter.do.clear_server(server)
raise dcRaiser(self.__root, kwargs)
@cherrypy.expose
def toggleServer(self, **kwargs):
msg = check_session(kwargs)
if msg: return msg
server = kwargs.get('server')
if server:
svr = config.get_config('servers', server)
svr.enable.set(not svr.enable())
config.save_config()
Downloader.do.update_server(server, server)
raise dcRaiser(self.__root, kwargs)
#------------------------------------------------------------------------------
def unique_svr_name(server):
@@ -1616,6 +1634,7 @@ class ConfigRss(object):
rss[feed] = feeds[feed].get_dict()
filters = feeds[feed].filters()
rss[feed]['filters'] = filters
rss[feed]['filter_states'] = [bool(sabnzbd.rss.convert_filter(f[4])) for f in filters]
rss[feed]['filtercount'] = len(filters)
rss[feed]['pick_cat'] = pick_cat
@@ -1872,7 +1891,7 @@ class ConfigRss(object):
#------------------------------------------------------------------------------
_SCHED_ACTIONS = ('resume', 'pause', 'pause_all', 'shutdown', 'restart', 'speedlimit',
'pause_post', 'resume_post', 'scan_folder', 'rss_scan')
'pause_post', 'resume_post', 'scan_folder', 'rss_scan', 'remove_failed')
class ConfigScheduling(object):
def __init__(self, web_dir, root, prim):
@@ -1901,7 +1920,7 @@ class ConfigScheduling(object):
actions = []
actions.extend(_SCHED_ACTIONS)
days = get_days()
day_names = get_days()
conf['schedlines'] = []
snum = 1
conf['taskinfo'] = []
@@ -1909,7 +1928,7 @@ class ConfigScheduling(object):
line = ev[3]
conf['schedlines'].append(line)
try:
m, h, day, action = line.split(' ', 3)
m, h, day_numbers, action = line.split(' ', 3)
except:
continue
action = action.strip()
@@ -1929,7 +1948,17 @@ class ConfigScheduling(object):
act = ''
if act in ('enable_server', 'disable_server'):
action = Ttemplate("sch-" + act) + ' ' + server
item = (snum, '%02d' % int(h), '%02d' % int(m), days.get(day, '**'), '%s %s' % (action, value))
if day_numbers == "1234567":
days_of_week = "Daily"
elif day_numbers == "12345":
days_of_week = "Weekdays"
elif day_numbers == "67":
days_of_week = "Weekends"
else:
days_of_week = ", ".join([day_names.get(i, "**") for i in day_numbers])
item = (snum, '%02d' % int(h), '%02d' % int(m), days_of_week, '%s %s' % (action, value))
conf['taskinfo'].append(item)
snum += 1
@@ -1954,7 +1983,7 @@ class ConfigScheduling(object):
minute = kwargs.get('minute')
hour = kwargs.get('hour')
dayofweek = kwargs.get('dayofweek')
days_of_week = ''.join([str(x) for x in kwargs.get('daysofweek', '')])
action = kwargs.get('action')
arguments = kwargs.get('arguments')
@@ -1964,7 +1993,7 @@ class ConfigScheduling(object):
elif arguments in ('off','disable'):
arguments = '0'
if minute and hour and dayofweek and action:
if minute and hour and days_of_week and action:
if action == 'speedlimit':
if not (arguments and arguments.isdigit()):
action = '0'
@@ -1983,7 +2012,7 @@ class ConfigScheduling(object):
if action:
sched = cfg.schedules()
sched.append('%s %s %s %s %s' %
(minute, hour, dayofweek, action, arguments))
(minute, hour, days_of_week, action, arguments))
cfg.schedules.set(sched)
config.save_config()
@@ -2475,10 +2504,15 @@ def GetRssLog(feed):
# Sort in the order the jobs came from the feed
names.sort(lambda x, y: jobs[x].get('order', 0) - jobs[y].get('order', 0))
done = [xml_name(jobs[job]['title']) for job in names if jobs[job]['status'] == 'D']
good = [make_item(jobs[job]) for job in names if jobs[job]['status'][0] == 'G']
bad = [make_item(jobs[job]) for job in names if jobs[job]['status'][0] == 'B']
# Sort in reverse order of time stamp for 'Done'
dnames = [job for job in jobs.keys() if jobs[job]['status'] == 'D']
dnames.sort(lambda x, y: jobs[y].get('timestamp', 0) - jobs[x].get('timestamp', 0))
done = [xml_name(jobs[job]['title']) for job in dnames]
return done, good, bad
def ShowRssLog(feed, all):
@@ -2562,7 +2596,7 @@ LIST_EMAIL = (
'email_server', 'email_to', 'email_from',
'email_account', 'email_pwd', 'email_dir', 'email_rss'
)
LIST_GROWL = ('growl_enable', 'growl_server', 'growl_password', 'ntfosd_enable')
LIST_GROWL = ('growl_enable', 'growl_server', 'growl_password', 'ntfosd_enable', 'ncenter_enable')
class ConfigNotify(object):
def __init__(self, web_dir, root, prim):
@@ -2582,11 +2616,15 @@ class ConfigNotify(object):
conf['lastmail'] = self.__lastmail
conf['have_growl'] = True
conf['have_ntfosd'] = sabnzbd.growler.have_ntfosd()
conf['have_ncenter'] = sabnzbd.DARWIN_ML and bool(sabnzbd.growler.ncenter_path())
for kw in LIST_EMAIL:
conf[kw] = config.get_config('misc', kw).get_string()
for kw in LIST_GROWL:
conf[kw] = config.get_config('growl', kw).get_string()
conf['notify_list'] = NOTIFY_KEYS
conf['notify_classes'] = cfg.notify_classes.get_string()
conf['notify_texts'] = sabnzbd.growler.NOTIFICATION
template = Template(file=os.path.join(self.__web_dir, 'config_notify.tmpl'),
filter=FILTER, searchList=[conf], compilerSettings=DIRECTIVES)
@@ -2605,6 +2643,7 @@ class ConfigNotify(object):
msg = config.get_config('growl', kw).set(platform_encode(kwargs.get(kw)))
if msg:
return badParameterResponse(T('Incorrect value for %s: %s') % (kw, unicoder(msg)))
cfg.notify_classes.set(kwargs.get('notify_classes', ''))
config.save_config()
self.__lastmail = None

View File

@@ -39,7 +39,7 @@ except:
import sabnzbd
from sabnzbd.decorators import synchronized
from sabnzbd.constants import DEFAULT_PRIORITY, FUTURE_Q_FOLDER, JOB_ADMIN, GIGI, VERIFIED_FILE, Status
from sabnzbd.constants import DEFAULT_PRIORITY, FUTURE_Q_FOLDER, JOB_ADMIN, GIGI, VERIFIED_FILE, Status, MEBI
import sabnzbd.config as config
import sabnzbd.cfg as cfg
from sabnzbd.encoding import unicoder, latin1
@@ -259,10 +259,10 @@ def sanitize_foldername(name):
#------------------------------------------------------------------------------
def verified_flag_file(path, create=False):
def flag_file(path, flag, create=False):
""" Create verify flag file or return True if it already exists """
path = os.path.join(path, JOB_ADMIN)
path = os.path.join(path, VERIFIED_FILE)
path = os.path.join(path, flag)
if create:
try:
f = open(path, 'w')
@@ -317,6 +317,8 @@ def real_path(loc, path):
When 'path' is absolute, return normalized path
A path starting with ~ will be located in the user's Home folder
"""
# The Windows part is a bit convoluted because
# os.path.join() doesn't behave the same for all Python versions
if path:
path = path.strip()
else:
@@ -325,9 +327,15 @@ def real_path(loc, path):
if not sabnzbd.WIN32 and path.startswith('~/'):
path = path.replace('~', sabnzbd.DIR_HOME, 1)
if sabnzbd.WIN32:
if path[0].isalpha() and len(path) > 1 and path[1] == ':':
if len(path) == 2 or path[2] not in '\\/':
path = path.replace('/', '\\')
if len(path) > 1 and path[0].isalpha() and path[1] == ':':
if len(path) == 2 or path[2] != '\\':
path = path.replace(':', ':\\', 1)
elif path.startswith('\\\\'):
pass
elif path.startswith('\\'):
if len(loc) > 1 and loc[0].isalpha() and loc[1] == ':':
path = loc[:2] + path
else:
path = os.path.join(loc, path)
elif path[0] != '/':
@@ -582,11 +590,20 @@ def check_latest_version():
logging.debug("Checked for a new release, cur= %s, latest= %s (on %s)", current, latest, url)
if latest_test and cfg.version_check() > 1:
# User always wants to see the latest test release
latest = latest_test
url = url_beta
if testver and current < latest:
# This is a test version, but user has't seen the
# "Final" of this one yet, so show the Final
sabnzbd.NEW_VERSION = "%s;%s" % (latest_label, url)
elif current < latest:
# This one is behind, show latest final
sabnzbd.NEW_VERSION = "%s;%s" % (latest_label, url)
elif testver and current < latest_test:
# This is a test version beyond the latest Final, so show latest Alpha/Beta/RC
sabnzbd.NEW_VERSION = "%s;%s" % (latest_testlabel, url_beta)
@@ -699,29 +716,25 @@ def split_host(srv):
return (host, port)
#------------------------------------------------------------------------------
def hostname():
""" Return host's pretty name """
if sabnzbd.WIN32:
return os.environ.get('computername', 'unknown')
try:
return os.uname()[1]
except:
return 'unknown'
#------------------------------------------------------------------------------
def check_mount(path):
""" Return False if volume isn't mounted on Linux or OSX
Retry 6 times with an interval of 1 sec.
"""
if sabnzbd.DARWIN:
m = re.search(r'^(/Volumes/[^/]+)/', path, re.I)
elif not sabnzbd.WIN32:
m = re.search(r'^(/(?:mnt|media)/[^/]+)/', path)
elif sabnzbd.WIN32:
m = re.search(r'^([a-z]:\\)', path, re.I)
else:
m = None
return (not m) or os.path.exists(m.group(1))
m = re.search(r'^(/(?:mnt|media)/[^/]+)/', path)
if m:
for n in xrange(cfg.wait_ext_drive() or 1):
if os.path.exists(m.group(1)):
return True
logging.debug('Waiting for %s to come online', m.group(1))
time.sleep(1)
return not m
#------------------------------------------------------------------------------
# Locked directory operations
@@ -773,13 +786,23 @@ def create_dirs(dirpath):
@synchronized(DIR_LOCK)
def move_to_path(path, new_path, unique=True):
""" Move a file to a new path, optionally give unique filename """
if unique:
new_path = get_unique_path(new_path, create_dir=False)
def move_to_path(path, new_path):
""" Move a file to a new path, optionally give unique filename
Return (ok, new_path)
"""
ok = True
overwrite = cfg.overwrite_files()
if overwrite and os.path.exists(new_path):
try:
os.remove(new_path)
except:
overwrite = False
if not overwrite:
new_path = get_unique_filename(new_path)
if new_path:
logging.debug("Moving. Old path:%s new path:%s unique?:%s",
path,new_path, unique)
logging.debug("Moving. Old path:%s new path:%s overwrite?:%s",
path, new_path, overwrite)
try:
# First try cheap rename
renamer(path, new_path)
@@ -794,8 +817,8 @@ def move_to_path(path, new_path, unique=True):
if not (cfg.marker_file() and cfg.marker_file() in path):
logging.error(Ta('Failed moving %s to %s'), path, new_path)
logging.info("Traceback: ", exc_info = True)
new_path = None
return new_path
ok = False
return ok, new_path
@synchronized(DIR_LOCK)
@@ -814,6 +837,10 @@ def cleanup_empty_directories(path):
pass
if not repeat:
break
try:
remove_dir(path)
except:
pass
@synchronized(DIR_LOCK)
@@ -932,8 +959,9 @@ def bad_fetch(nzo, url, msg='', retry=False, content=False):
if isinstance(url, int) or url.isdigit():
url = 'Newzbin #%s' % url
growler.send_notification(T('URL Fetching failed; %s') % '', '%s\n%s' % (msg, url), 'other')
#import sabnzbd.emailer
sabnzbd.emailer.badfetch_mail(msg, url)
if cfg.email_endjob() > 0:
#import sabnzbd.emailer
sabnzbd.emailer.badfetch_mail(msg, url)
from sabnzbd.nzbqueue import NzbQueue
assert isinstance(NzbQueue.do, NzbQueue)
@@ -971,15 +999,37 @@ def get_filename(path):
except:
return ''
def memory_usage():
try:
# Probably only works on Linux because it uses /proc/<pid>/statm
t = open('/proc/%d/statm' % os.getpid())
v = t.read().split()
t.close()
virt = int(_PAGE_SIZE * int(v[0]) / MEBI)
res = int(_PAGE_SIZE * int(v[1]) / MEBI)
return "V=%sM R=%sM" % (virt, res)
except:
return None
try:
_PAGE_SIZE = os.sysconf("SC_PAGE_SIZE")
except:
_PAGE_SIZE = 0
_HAVE_STATM = _PAGE_SIZE and memory_usage()
def loadavg():
""" Return 1, 5 and 15 minute load average of host or "" if not supported
"""
if sabnzbd.WIN32 or sabnzbd.DARWIN:
return ""
try:
return "%.2f | %.2f | %.2f" % os.getloadavg()
except:
return ""
p = ''
if not sabnzbd.WIN32 and not sabnzbd.DARWIN:
opt = cfg.show_sysload()
if opt:
p = '%.2f | %.2f | %.2f' % os.getloadavg()
if opt > 1 and _HAVE_STATM:
p = '%s | %s' % (p, memory_usage())
return p
def format_time_string(seconds, days=0):

View File

@@ -31,10 +31,11 @@ import sabnzbd
from sabnzbd.encoding import TRANS, UNTRANS, unicode2local, name_fixer, \
reliable_unpack_names, unicoder, latin1, platform_encode
from sabnzbd.utils.rarfile import RarFile, is_rarfile
from sabnzbd.misc import format_time_string, find_on_path, make_script_path, int_conv
from sabnzbd.misc import format_time_string, find_on_path, make_script_path, int_conv, \
flag_file
from sabnzbd.tvsort import SeriesSorter
import sabnzbd.cfg as cfg
from constants import Status
from constants import Status, QCHECK_FILE
if sabnzbd.WIN32:
try:
@@ -124,7 +125,7 @@ def find_programs(curdir):
def external_processing(extern_proc, complete_dir, filename, msgid, nicename, cat, group, status):
""" Run a user postproc script, return console output and exit value
"""
command = [str(extern_proc), str(complete_dir), str(filename), \
command = [str(extern_proc), str(complete_dir), str(filename),
str(nicename), str(msgid), str(cat), str(group), str(status)]
if extern_proc.endswith('.py') and (sabnzbd.WIN32 or not os.access(extern_proc, os.X_OK)):
@@ -132,7 +133,7 @@ def external_processing(extern_proc, complete_dir, filename, msgid, nicename, ca
stup, need_shell, command, creationflags = build_command(command)
env = fix_env()
logging.info('Running external script %s(%s, %s, %s, %s, %s, %s, %s)', \
logging.info('Running external script %s(%s, %s, %s, %s, %s, %s, %s)',
extern_proc, complete_dir, filename, nicename, msgid, cat, group, status)
try:
@@ -257,8 +258,8 @@ def match_ts(file):
def clean_up_joinables(names):
''' Remove joinable files and their .1 backups
'''
""" Remove joinable files and their .1 backups
"""
for name in names:
if os.path.exists(name):
logging.debug("Deleting %s", name)
@@ -275,8 +276,8 @@ def clean_up_joinables(names):
pass
def get_seq_number(name):
''' Return sequence number if name as an int
'''
""" Return sequence number if name as an int
"""
head, tail = os.path.splitext(name)
if tail == '.ts':
match, set, num = match_ts(name)
@@ -296,7 +297,8 @@ def file_join(nzo, workdir, workdir_complete, delete, joinables):
# Create matching sets from the list of files
joinable_sets = {}
set = match = num = None
joinable_set = None
set = num = None
for joinable in joinables:
head, tail = os.path.splitext(joinable)
if tail == '.ts':
@@ -353,7 +355,7 @@ def file_join(nzo, workdir, workdir_complete, delete, joinables):
# Finish up
joined_file.flush()
joined_file.close()
newfiles.append(joinable_set)
newfiles.append(filename)
if seq_error:
msg = T('Incomplete sequence of joinable files')
@@ -383,6 +385,7 @@ def rar_unpack(nzo, workdir, workdir_complete, delete, one_folder, rars):
When 'one_folder' is set, all files will be in a single folder
"""
extracted_files = []
success = False
rar_sets = {}
for rar in rars:
@@ -437,7 +440,7 @@ def rar_unpack(nzo, workdir, workdir_complete, delete, one_folder, rars):
except OSError:
logging.warning(Ta('Deleting %s failed!'), latin1(rar))
brokenrar = '%s.1' % (rar)
brokenrar = '%s.1' % rar
if os.path.exists(brokenrar):
logging.info("Deleting %s", brokenrar)
@@ -456,6 +459,8 @@ def rar_extract(rarfile, numrars, one_folder, nzo, setname, extraction_path):
"""
fail = 0
new_files = None
rars = []
if nzo.password:
passwords = [nzo.password]
else:
@@ -523,17 +528,23 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
action = 'e'
else:
action = 'x'
if cfg.overwrite_files():
overwrite = '-o+' # Enable overwrite
rename = '-o+' # Dummy
else:
overwrite = '-o-' # Disable overwrite
rename = '-or' # Auto renaming
if sabnzbd.WIN32:
# Use all flags
command = ['%s' % RAR_COMMAND, action, '-idp', '-o-', '-or', '-ai', password,
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, rename, '-ai', password,
'%s' % rarfile, '%s/' % extraction_path]
elif RAR_PROBLEM:
# Use only oldest options (specifically no "-or")
command = ['%s' % RAR_COMMAND, action, '-idp', '-o-', password,
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, password,
'%s' % rarfile, '%s/' % extraction_path]
else:
# Don't use "-ai" (not needed for non-Windows)
command = ['%s' % RAR_COMMAND, action, '-idp', '-o-', '-or', password,
command = ['%s' % RAR_COMMAND, action, '-idp', overwrite, rename, password,
'%s' % rarfile, '%s/' % extraction_path]
if cfg.ignore_unrar_dates():
@@ -550,7 +561,7 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
if p.stdin:
p.stdin.close()
nzo.set_action_line(T('Unpacking'), '00/%02d' % (numrars))
nzo.set_action_line(T('Unpacking'), '00/%02d' % numrars)
# Loop over the output from rar!
curr = 0
@@ -627,7 +638,7 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
proc.close()
p.wait()
return (fail, (), ())
return fail, (), ()
if proc:
proc.close()
@@ -669,7 +680,7 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
nzo.set_unpack_info('Unpack', '[%s] %s' % (unicoder(setname), msg), set=setname)
logging.info('%s', msg)
return (0, extracted, rarfiles)
return 0, extracted, rarfiles
#------------------------------------------------------------------------------
# (Un)Zip Functions
@@ -714,7 +725,7 @@ def unzip(nzo, workdir, workdir_complete, delete, one_folder, zips):
except OSError:
logging.warning(Ta('Deleting %s failed!'), latin1(_zip))
brokenzip = '%s.1' % (_zip)
brokenzip = '%s.1' % _zip
if os.path.exists(brokenzip):
logging.info("Deleting %s", brokenzip)
@@ -779,6 +790,7 @@ def par2_repair(parfile_nzf, nzo, workdir, setname):
result = True
if not result:
flag_file(workdir, QCHECK_FILE, True)
nzo.status = Status.REPAIRING
result = False
readd = False
@@ -892,7 +904,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
logging.debug('Par2-classic = %s', classic)
import sabnzbd.assembler
if (sabnzbd.assembler.GetMD5Hashes(parfile)[1] and not classic) or not PAR2C_COMMAND:
if (sabnzbd.assembler.GetMD5Hashes(parfile, True)[1] and not classic) or not PAR2C_COMMAND:
if cfg.par_option():
command = [str(PAR2_COMMAND), cmd, str(cfg.par_option().strip()), parfile]
else:
@@ -909,6 +921,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
stup, need_shell, command, creationflags = build_command(command)
logging.debug('Starting par2: %s', command)
lines = []
try:
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
@@ -949,8 +962,15 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
if line == '':
continue
# And off we go
if line.startswith('Invalid option specified'):
if 'Repairing:' not in line:
lines.append(line)
if 'The recovery file does not exist' in line:
logging.info('%s', line)
nzo.set_unpack_info('Repair', unicoder(line), set=setname)
nzo.status = Status.FAILED
elif line.startswith('Invalid option specified'):
msg = T('[%s] PAR2 received incorrect options, check your Config->Switches settings') % unicoder(setname)
nzo.set_unpack_info('Repair', msg, set=setname)
nzo.status = Status.FAILED
@@ -971,7 +991,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
verified = 1
elif line.startswith('Main packet not found'):
## Initialparfile probaly didn't decode properly,
## Initialparfile probably didn't decode properly,
logging.info(Ta('Main packet not found...'))
extrapars = parfile_nzf.extrapars
@@ -981,7 +1001,8 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
## Look for the smallest par2file
block_table = {}
for nzf in extrapars:
block_table[int_conv(nzf.blocks)] = nzf
if not nzf.completed:
block_table[int_conv(nzf.blocks)] = nzf
if block_table:
nzf = block_table[min(block_table.keys())]
@@ -989,6 +1010,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
logging.info("Found new par2file %s", nzf.filename)
nzo.add_parfile(nzf)
extrapars.remove(nzf)
## mark for readd
readd = True
else:
@@ -1050,7 +1072,9 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
extrapar_list = block_table[block_size]
if extrapar_list:
nzo.add_parfile(extrapar_list.pop())
new_nzf = extrapar_list.pop()
nzo.add_parfile(new_nzf)
if new_nzf in extrapars: extrapars.remove(new_nzf)
added_blocks += block_size
else:
@@ -1079,7 +1103,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
elif line.startswith('Repairing:'):
chunks = line.split()
per = float(chunks[-1][:-1])
nzo.set_action_line(T('Repairing'), '%2d%%' % (per))
nzo.set_action_line(T('Repairing'), '%2d%%' % per)
nzo.status = Status.REPAIRING
elif line.startswith('Repair complete'):
@@ -1143,11 +1167,13 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
else:
raise WindowsError(err)
logging.debug('PAR2 output was\n%s', '\n'.join(lines))
if retry_classic:
logging.debug('Retry PAR2-joining with par2-classic')
return PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=True)
else:
return (finished, readd, pars, datafiles, used_joinables)
return finished, readd, pars, datafiles, used_joinables
#-------------------------------------------------------------------------------
@@ -1198,7 +1224,7 @@ def build_command(command):
if need_shell:
command = list2cmdline(command)
return (stup, need_shell, command, creationflags)
return stup, need_shell, command, creationflags
# Sort the various RAR filename formats properly :\
def rar_sort(a, b):
@@ -1254,7 +1280,10 @@ def build_filelists(workdir, workdir_complete, check_rar=True):
zips = [f for f in filelist if ZIP_RE.search(f)]
rars = [f for f in filelist if RAR_RE.search(f) and is_rarfile(f)]
if check_rar:
rars = [f for f in filelist if RAR_RE.search(f) and is_rarfile(f)]
else:
rars = [f for f in filelist if RAR_RE.search(f)]
ts = [f for f in filelist if TS_RE.search(f) and f not in joinables]
@@ -1263,7 +1292,7 @@ def build_filelists(workdir, workdir_complete, check_rar=True):
logging.debug("build_filelists(): rars: %s", rars)
logging.debug("build_filelists(): ts: %s", ts)
return (joinables, zips, rars, ts)
return joinables, zips, rars, ts
def QuickCheck(set, nzo):
@@ -1334,32 +1363,34 @@ def unrar_check(rar):
def sfv_check(sfv_path):
""" Verify files using SFV file,
input: full path of sfv, file are assumed to be relative to sfv
returns: True when all files verified OK
returns: List of failing files or [] when all is OK
"""
failed = []
try:
fp = open(sfv_path, 'r')
except:
logging.info('Cannot open SFV file %s', sfv_path)
return False
failed.append(unicoder(sfv_path))
return failed
root = os.path.split(sfv_path)[0]
status = True
for line in fp:
line = line.strip('\n\r ')
if line[0] != ';':
x = line.rfind(' ')
filename = line[:x].strip()
filename = platform_encode(line[:x].strip())
checksum = line[x:].strip()
path = os.path.join(root, platform_encode(filename))
path = os.path.join(root, filename)
if os.path.exists(path):
if crc_check(path, checksum):
logging.debug('File %s passed SFV check', path)
else:
logging.warning('File %s did not pass SFV check', latin1(path))
status = False
logging.info('File %s did not pass SFV check', latin1(path))
failed.append(unicoder(filename))
else:
logging.warning('File %s mssing in SFV check', latin1(path))
logging.info('File %s missing in SFV check', latin1(path))
failed.append(unicoder(filename))
fp.close()
return status
return failed
def crc_check(path, target_crc):

View File

@@ -27,14 +27,14 @@ import datetime
import sabnzbd
from sabnzbd.trylist import TryList
from sabnzbd.nzbstuff import NzbObject
from sabnzbd.misc import exit_sab, cat_to_opts, verified_flag_file, \
from sabnzbd.misc import exit_sab, cat_to_opts, flag_file, \
get_admin_path, remove_all, globber
from sabnzbd.panic import panic_queue
import sabnzbd.database as database
from sabnzbd.decorators import NZBQUEUE_LOCK, synchronized, synchronized_CV
from sabnzbd.constants import QUEUE_FILE_NAME, QUEUE_VERSION, FUTURE_Q_FOLDER, JOB_ADMIN, \
LOW_PRIORITY, NORMAL_PRIORITY, HIGH_PRIORITY, TOP_PRIORITY, \
REPAIR_PRIORITY, STOP_PRIORITY, \
REPAIR_PRIORITY, STOP_PRIORITY, VERIFIED_FILE, \
PNFO_BYTES_FIELD, PNFO_BYTES_LEFT_FIELD, Status
import sabnzbd.cfg as cfg
from sabnzbd.articlecache import ArticleCache
@@ -150,10 +150,12 @@ class NzbQueue(TryList):
""" Reconstruct admin for a single job folder, optionally with new NZB """
name = os.path.basename(folder)
path = os.path.join(folder, JOB_ADMIN)
if new_nzb is None or not new_nzb.filename:
if verified_flag_file(folder):
filename = ''
else:
if hasattr(new_nzb, 'filename'):
filename = new_nzb.filename
else:
filename = ''
if not filename:
if not flag_file(folder, VERIFIED_FILE):
filename = globber(path, '*.gz')
if len(filename) > 0:
logging.debug('Repair job %s by reparsing stored NZB', latin1(name))
@@ -164,10 +166,38 @@ class NzbQueue(TryList):
self.add(nzo)
else:
remove_all(path, '*.gz')
logging.debug('Repair job %s with new NZB (%s)', latin1(name), latin1(new_nzb.filename))
logging.debug('Repair job %s with new NZB (%s)', latin1(name), latin1(filename))
sabnzbd.add_nzbfile(new_nzb, pp=None, script=None, cat=None, priority=None, nzbname=name, reuse=True)
def send_back(self, nzo):
""" Send back job to queue after successful pre-check """
try:
nzb_path = globber(nzo.workpath, '*.gz')[0]
except:
logging.debug('Failed to find NZB file after pre-check (%s)', nzo.nzo_id)
return
from sabnzbd.dirscanner import ProcessSingleFile
nzo_id = ProcessSingleFile(os.path.split(nzb_path)[1], nzb_path, reuse=True)[1][0]
self.replace_in_q(nzo, nzo_id)
@synchronized(NZBQUEUE_LOCK)
def replace_in_q(self, nzo, nzo_id):
""" Replace nzo by new in at the same spot in the queue, destroy nzo """
try:
new_nzo = self.get_nzo(nzo_id)
pos = self.__nzo_list.index(new_nzo)
targetpos = self.__nzo_list.index(nzo)
self.__nzo_list.pop(pos)
self.__nzo_list[targetpos] = new_nzo
del self.__nzo_table[nzo.nzo_id]
del nzo
except:
logging.error('Failed to restart NZB after pre-check (%s)', nzo.nzo_id)
logging.info("Traceback: ", exc_info = True)
return
@synchronized(NZBQUEUE_LOCK)
def save(self, save_nzo=None):
""" Save queue, all nzo's or just the specified one """
@@ -295,8 +325,7 @@ class NzbQueue(TryList):
# If no files are to be downloaded anymore, send to postproc
if not nzo.files and not nzo.futuretype:
sabnzbd.remove_data(nzo.nzo_id, nzo.workpath)
sabnzbd.proxy_postproc(nzo)
self.end_job(nzo)
return ''
# Reset try_lists
@@ -397,10 +426,10 @@ class NzbQueue(TryList):
if nzf:
post_done = nzo.remove_nzf(nzf)
if post_done:
keep_basic = nzo.finished_files
if keep_basic:
sabnzbd.proxy_postproc(nzo)
self.remove(nzo_id, add_to_history = False, keep_basic=keep_basic)
if nzo.finished_files:
self.end_job(nzo)
else:
self.remove(nzo_id, add_to_history = False, keep_basic=False)
@synchronized(NZBQUEUE_LOCK)
@@ -673,7 +702,7 @@ class NzbQueue(TryList):
nzf = article.nzf
nzo = nzf.nzo
if nzo.deleted or nzf.deleted:
if nzf.deleted:
logging.debug("Discarding article %s, no longer in queue", article.article)
return
@@ -715,7 +744,21 @@ class NzbQueue(TryList):
sabnzbd.downloader.Downloader.do.disconnect()
# Notify assembler to call postprocessor
Assembler.do.process((nzo, None))
if not nzo.deleted:
nzo.deleted = True
if nzo.precheck:
# Check result
enough, ratio = nzo.check_quality()
if enough:
# Enough data present, do real download
workdir = nzo.downpath
self.cleanup_nzo(nzo, keep_basic=True)
self.send_back(nzo)
return
else:
# Not enough data, let postprocessor show it as failed
nzo.save_attribs()
Assembler.do.process((nzo, None))
@synchronized(NZBQUEUE_LOCK)
@@ -733,19 +776,34 @@ class NzbQueue(TryList):
@synchronized(NZBQUEUE_LOCK)
def queue_info(self, for_cli = False):
def queue_info(self, for_cli=False, max_jobs=0):
bytes_left = 0
bytes = 0
pnfo_list = []
n = 0
for nzo in self.__nzo_list:
pnfo = nzo.gather_info(for_cli = for_cli)
if nzo.status != 'Paused':
bytes += pnfo[PNFO_BYTES_FIELD]
bytes_left += pnfo[PNFO_BYTES_LEFT_FIELD]
pnfo_list.append(pnfo)
n += 1
if max_jobs and n >= max_jobs:
break
return (bytes, bytes_left, pnfo_list)
@synchronized(NZBQUEUE_LOCK)
def remaining(self):
""" Return bytes left in the queue by non-paused items
"""
bytes_left = 0
for nzo in self.__nzo_list:
if nzo.status != 'Paused':
bytes_left += nzo.remaining()
return bytes_left
@synchronized(NZBQUEUE_LOCK)
def is_empty(self):
empty = True

View File

@@ -54,7 +54,8 @@ RE_NORMAL = re.compile(r"(.+)(\.nzb)", re.I)
SUBJECT_FN_MATCHER = re.compile(r'"([^"]*)"')
RE_SAMPLE = re.compile(sample_match, re.I)
PROBABLY_PAR2_RE = re.compile(r'(.*)\.vol(\d*)\+(\d*)\.par2', re.I)
REJECT_PAR2_RE = re.compile(r'\.par2\.\d+', re.I) # Reject duplicate par2 files
RE_NORMAL_NAME = re.compile(r'\.\w{2,5}$') # Test reasonably sized extension at the end
################################################################################
# Article #
@@ -154,6 +155,7 @@ NzbFileMapper = (
('import_finished', 'import_finished'),
('md5sum', 'md5sum'),
('valid', 'valid'),
('completed', 'completed')
)
@@ -169,9 +171,7 @@ class NzbFile(TryList):
self.filename = None
self.type = None
match = re.search(SUBJECT_FN_MATCHER, subject)
if match:
self.filename = match.group(1).strip('"')
self.filename = name_extractor(subject)
self.is_par2 = False
self.vol = None
@@ -191,6 +191,7 @@ class NzbFile(TryList):
self.nzo = nzo
self.nzf_id = sabnzbd.get_new_id("nzf", nzo.workpath)
self.deleted = False
self.completed = False
self.valid = False
self.import_finished = False
@@ -272,7 +273,7 @@ class NzbFile(TryList):
@property
def completed(self):
""" Is this file completed? """
return not bool(self.articles)
return self.import_finished and not bool(self.articles)
@property
def lowest_partnum(self):
@@ -348,12 +349,8 @@ class NzbParser(xml.sax.handler.ContentHandler):
self.in_segments = True
elif name == 'file' and self.in_nzb:
subject = attrs.get('subject', '')
match = re.search(SUBJECT_FN_MATCHER, subject)
if match:
self.filename = match.group(1).strip('"').strip()
else:
self.filename = subject.strip()
subject = attrs.get('subject', '').strip()
self.filename = subject
if self.filter and RE_SAMPLE.search(subject):
logging.info('Skipping sample file %s', subject)
@@ -810,6 +807,7 @@ class NzbObject(TryList):
if nzf in self.files:
self.files.remove(nzf)
self.finished_files.append(nzf)
nzf.import_finished = True
nzf.deleted = True
return not bool(self.files)
@@ -818,38 +816,36 @@ class NzbObject(TryList):
nzf.reset_all_try_lists()
self.reset_try_list()
def postpone_pars(self, nzf, head):
""" Move all vol-par files matching 'head' to the extrapars table """
self.partable[head] = nzf
self.extrapars[head] = []
nzf.extrapars = self.extrapars[head]
def postpone_pars(self, nzf, parset):
""" Move all vol-par files matching 'parset' to the extrapars table """
self.partable[parset] = nzf
self.extrapars[parset] = []
nzf.extrapars = self.extrapars[parset]
lparset = parset.lower()
for xnzf in self.files[:]:
name = xnzf.filename
name = xnzf.filename or platform_encode(xnzf.subject)
# Move only when not current NZF and filename was extractable from subject
if name and nzf is not xnzf:
name = name.lower()
if head.lower() in name and '.vol' in name and name.endswith('.par2'):
self.extrapars[head].append(xnzf)
self.files.remove(xnzf)
head, vol, block = analyse_par2(name)
# When only subject is known, it's enough that that 'parset' is in subject
if head and lparset in head.lower():
xnzf.set_par2(parset, vol, block)
self.extrapars[parset].append(xnzf)
if not self.precheck:
self.files.remove(xnzf)
def handle_par2(self, nzf, file_done):
## Special treatment for first part of par2 file
""" Check if file is a par2 and build up par2 collection
"""
fn = nzf.filename
if fn:
# We have a real filename now
fn = fn.strip()
lfn = fn.lower()
if (not nzf.is_par2) and fn and lfn.endswith('.par2'):
par2match = PROBABLY_PAR2_RE.search(fn)
if par2match:
head = par2match.group(1)
vol = par2match.group(2)
block = par2match.group(3)
elif lfn.endswith('.par2'):
head = os.path.splitext(fn)[0]
vol = block = 0
par2match = True
if not nzf.is_par2:
head, vol, block = analyse_par2(fn)
## Is a par2file and repair mode activated
if par2match and (self.repair or cfg.allow_streaming()):
if head and (self.repair or cfg.allow_streaming()):
nzf.set_par2(head, vol, block)
## Already got a parfile for this set?
if head in self.partable:
@@ -867,15 +863,17 @@ class NzbObject(TryList):
## This file either has more blocks,
## or initialparfile is already decoded
else:
if not file_done:
nzf.reset_try_list()
if file_done:
if nzf in self.files: self.files.remove(nzf)
self.extrapars[head].append(nzf)
if nzf not in self.extrapars[head]: self.extrapars[head].append(nzf)
else:
nzf.reset_try_list()
## No par2file in this set yet, set this as
## initialparfile
else:
self.postpone_pars(nzf, head)
## Is not a par2file or nothing todo
## Is not a par2file or nothing to do
else:
pass
## No filename in seg 1? Probably not uu or yenc encoded
@@ -894,7 +892,8 @@ class NzbObject(TryList):
if reset:
self.reset_try_list()
self.handle_par2(nzf, file_done)
if file_done:
self.handle_par2(nzf, file_done)
post_done = False
if not self.files:
@@ -924,6 +923,7 @@ class NzbObject(TryList):
subject = sanitize_filename(latin1(nzf.subject))
if (nzf.filename == filename) or (subject == filename) or (filename in subject):
nzf.filename = filename
nzf.completed = True
self.handle_par2(nzf, file_done=True)
self.remove_nzf(nzf)
nzfs.remove(nzf)
@@ -940,6 +940,7 @@ class NzbObject(TryList):
self.files_table[nzf.nzf_id] = nzf
self.bytes += nzf.bytes
nzf.filename = filename
nzf.completed = True
self.handle_par2(nzf, file_done=True)
self.remove_nzf(nzf)
logging.info('File %s added to job', filename)
@@ -1007,11 +1008,10 @@ class NzbObject(TryList):
self.incomplete = False
def add_parfile(self, parfile):
self.files.append(parfile)
if parfile.extrapars:
if parfile not in self.files:
self.files.append(parfile)
if parfile.extrapars and parfile in parfile.extrapars:
parfile.extrapars.remove(parfile)
else:
logging.debug('PARFILE without EXTRAPARS %s', parfile.filename or parfile.subject)
def remove_parset(self, setname):
self.partable.pop(setname)
@@ -1110,8 +1110,14 @@ class NzbObject(TryList):
if article:
break
# Remove all files for which admin could not be read
for nzf in nzf_remove_list:
nzf.deleted = True
nzf.completed = True
self.files.remove(nzf)
# If cleanup emptied the active files list, end this job
if nzf_remove_list and not self.files:
sabnzbd.NzbQueue.do.end_job(self)
if not article:
# No articles for this server, block for next time
@@ -1228,6 +1234,13 @@ class NzbObject(TryList):
except:
pass
def remaining(self):
""" Return remaining bytes """
bytes_left = 0
for nzf in self.files:
bytes_left += nzf.bytes_left
return bytes_left
def gather_info(self, for_cli = False):
bytes_left_all = 0
@@ -1385,7 +1398,7 @@ def nzf_cmp_date(nzf1, nzf2):
return nzf_cmp_name(nzf1, nzf2, name=False)
RE_RAR = re.compile(r'(\.rar|\.r\d\d)|\.s\d\d|\.t\d\d|\.u\d\d|\.v\d\d', re.I)
RE_RAR = re.compile(r'(\.rar|\.r\d\d|\.s\d\d|\.t\d\d|\.u\d\d|\.v\d\d)$', re.I)
def nzf_cmp_name(nzf1, nzf2, name=True):
# The comparison will sort .par2 files to the top of the queue followed by .rar files,
@@ -1532,3 +1545,33 @@ def set_attrib_file(path, attribs):
f.write('%s\n' % item)
f.close()
def analyse_par2(name):
""" Check if file is a par2-file and determine vol/block
return head, vol, block
head is empty when not a par2 file
"""
head = None
vol = block = 0
if name and not REJECT_PAR2_RE.search(name):
m = PROBABLY_PAR2_RE.search(name)
if m:
head = m.group(1)
vol = m.group(2)
block = m.group(3)
elif name.lower().find('.par2') > 0:
head = os.path.splitext(name)[0]
else:
head = None
return head, vol, block
def name_extractor(subject):
""" Try to extract a file name from a subject line, return `subject` if in doubt
"""
result = subject
for name in re.findall(SUBJECT_FN_MATCHER, subject):
name = name.strip(' "')
if name and RE_NORMAL_NAME.search(name):
result = name
return result

View File

@@ -23,16 +23,17 @@ from Foundation import *
from AppKit import *
from PyObjCTools import AppHelper
from objc import YES, NO, nil
from threading import Thread
import os
import subprocess
from threading import Thread
import cherrypy
import Cheetah.DummyTransaction
import sys
import time
import logging
import logging.handlers
import sabnzbd
import sabnzbd.cfg
@@ -42,6 +43,7 @@ from sabnzbd.misc import get_filename, get_ext, diskfree, to_units
from sabnzbd.panic import launch_a_browser
import sabnzbd.growler as growler
from sabnzbd.api import fast_queue
from sabnzbd.nzbqueue import NzbQueue
import sabnzbd.config as config
import sabnzbd.scheduler as scheduler
@@ -50,6 +52,7 @@ import sabnzbd.dirscanner as dirscanner
from sabnzbd.bpsmeter import BPSMeter
from sabnzbd.newzbin import Bookmarks
from sabnzbd.database import get_history_handle
from sabnzbd.encoding import unicoder
status_icons = {'idle':'../Resources/sab_idle.png','pause':'../Resources/sab_pause.png','clicked':'../Resources/sab_clicked.png'}
start_time = NSDate.date()
@@ -61,6 +64,7 @@ class SABnzbdDelegate(NSObject):
icons = {}
status_bar = None
osx_icon = True
history_db = None
def awakeFromNib(self):
#Status Bar iniatilize
@@ -328,7 +332,7 @@ class SABnzbdDelegate(NSObject):
def queueUpdate(self):
try:
qnfo = NzbQueue.do.queue_info()
qnfo = NzbQueue.do.queue_info(max_jobs=10)
pnfo_list = qnfo[QNFO_PNFO_LIST_FIELD]
bytesleftprogess = 0
@@ -347,7 +351,7 @@ class SABnzbdDelegate(NSObject):
for pnfo in pnfo_list:
if job_nb > 10:
break
filename = pnfo[PNFO_FILENAME_FIELD]
filename = unicoder(pnfo[PNFO_FILENAME_FIELD])
msgid = pnfo[PNFO_MSGID_FIELD]
bytesleft = pnfo[PNFO_BYTES_LEFT_FIELD] / MEBI
bytesleftprogess += pnfo[PNFO_BYTES_LEFT_FIELD]
@@ -374,8 +378,9 @@ class SABnzbdDelegate(NSObject):
def historyUpdate(self):
try:
# Fetch history items
history_db = sabnzbd.database.get_history_handle()
items, fetched_items, total_items = history_db.fetch_history(0,10,None)
if not self.history_db:
self.history_db = sabnzbd.database.get_history_handle()
items, fetched_items, total_items = self.history_db.fetch_history(0,10,None)
self.menu_history = NSMenu.alloc().init()
self.failedAttributes = { NSForegroundColorAttributeName:NSColor.redColor(), NSFontAttributeName:NSFont.menuFontOfSize_(14.0) }
@@ -438,24 +443,23 @@ class SABnzbdDelegate(NSObject):
def stateUpdate(self):
try:
qnfo = NzbQueue.do.queue_info()
bpsnow = BPSMeter.do.get_bps()
if sabnzbd.downloader.Downloader.do.paused:
paused, bytes_left, bpsnow, time_left = fast_queue()
if paused:
self.state = T('Paused')
if sabnzbd.scheduler.pause_int() != "0":
self.setMenuTitle("\n\n%s\n" % (sabnzbd.scheduler.pause_int()))
else:
self.setMenuTitle("")
elif qnfo[QNFO_BYTES_LEFT_FIELD] / MEBI > 0:
elif bytes_left > 0:
self.state = ""
speed = to_units(bpsnow, dec_limit=1) + 'B/s'
speed = to_units(bpsnow, dec_limit=1)
# "10.1 MB/s" doesn't fit, remove space char
if 'M' in speed and len(speed) > 8:
if 'M' in speed and len(speed) > 5:
speed = speed.replace(' ', '')
timeleft = (bpsnow>10 and self.calc_timeleft(qnfo[QNFO_BYTES_LEFT_FIELD],bpsnow)) or "--"
time_left = (bpsnow>10 and time_left) or "------"
statusbarText = "\n\n%s\n%s\n" % (timeleft, speed)
statusbarText = "\n\n%s\n%sB/s\n" % (time_left, speed)
if sabnzbd.SABSTOP:
statusbarText = "..."
@@ -683,8 +687,9 @@ class SABnzbdDelegate(NSObject):
if mode == "queue":
NzbQueue.do.remove_all()
elif mode == "history":
history_db = sabnzbd.database.get_history_handle()
history_db.remove_history()
if not self.history_db:
self.history_db = sabnzbd.database.get_history_handle()
self.history_db.remove_history()
def pauseAction_(self, sender):
minutes = int(sender.representedObject())
@@ -785,4 +790,3 @@ def notify(notificationName, message):
nc = Foundation.NSDistributedNotificationCenter.defaultCenter()
nc.postNotificationName_object_(notificationName, message)
del pool

View File

@@ -236,6 +236,10 @@ def launch_a_browser(url, force=False):
if not force and not cfg.autobrowser() or sabnzbd.DAEMON:
return
if '::1' in url and not '[::1]' in url:
# Get around ideosyncrasy in Python runtime
url = url.replace('::1', '[::1]')
if cfg.enable_https() and not cfg.https_port.get_int():
# Must use https, because http is not available
url = url.replace('http:', 'https:')

View File

@@ -31,12 +31,12 @@ import re
from sabnzbd.newsunpack import unpack_magic, par2_repair, external_processing, sfv_check
from threading import Thread
from sabnzbd.misc import real_path, get_unique_path, create_dirs, move_to_path, \
get_unique_filename, make_script_path, verified_flag_file, \
get_unique_filename, make_script_path, flag_file, \
on_cleanup_list, renamer, remove_dir, remove_all, globber, \
set_permissions
from sabnzbd.tvsort import Sorter
from sabnzbd.constants import REPAIR_PRIORITY, POSTPROC_QUEUE_FILE_NAME, \
POSTPROC_QUEUE_VERSION, sample_match, JOB_ADMIN, Status
from sabnzbd.constants import REPAIR_PRIORITY, TOP_PRIORITY, POSTPROC_QUEUE_FILE_NAME, \
POSTPROC_QUEUE_VERSION, sample_match, JOB_ADMIN, Status, VERIFIED_FILE
from sabnzbd.encoding import TRANS, unicoder
from sabnzbd.newzbin import Bookmarks
import sabnzbd.emailer as emailer
@@ -184,8 +184,8 @@ class PostProcessor(Thread):
sabnzbd.downloader.Downloader.do.wait_for_postproc()
self.__busy = True
if process_job(nzo):
self.remove(nzo)
process_job(nzo)
self.remove(nzo)
check_eoq = True
## Allow download to proceed
@@ -225,19 +225,6 @@ def process_job(nzo):
filename = nzo.final_name
msgid = nzo.msgid
if nzo.precheck:
# Check result
enough, ratio = nzo.check_quality()
if enough:
# Enough data present, do real download
workdir = nzo.downpath
sabnzbd.nzbqueue.NzbQueue.do.cleanup_nzo(nzo, keep_basic=True)
sabnzbd.nzbqueue.NzbQueue.do.repair_job(workdir)
return True
else:
# Not enough data, flag as failed
nzo.save_attribs()
if cfg.allow_streaming() and not (flag_repair or flag_unpack or flag_delete):
# After streaming, force +D
nzo.set_pp(3)
@@ -254,6 +241,7 @@ def process_job(nzo):
# if no files are present (except __admin__), fail the job
if len(globber(workdir)) < 2:
if nzo.precheck:
enough, ratio = nzo.check_quality()
req_ratio = float(cfg.req_completion_rate()) / 100.0
# Make sure that rounded ratio doesn't equal required ratio
# when it is actually below required
@@ -311,7 +299,7 @@ def process_job(nzo):
## TV/Movie/Date Renaming code part 1 - detect and construct paths
file_sorter = Sorter(cat)
complete_dir = file_sorter.detect(dirname, complete_dir)
if file_sorter.is_sortfile():
if file_sorter.sort_file:
one_folder = False
if one_folder:
@@ -357,8 +345,9 @@ def process_job(nzo):
for file_ in files:
path = os.path.join(root, file_)
new_path = path.replace(workdir, tmp_workdir_complete)
new_path = get_unique_filename(new_path)
if not move_to_path(path, new_path, unique=False):
ok, new_path = move_to_path(path, new_path)
newfiles.append(new_path)
if not ok:
nzo.set_unpack_info('Unpack', T('Failed moving %s to %s') % (unicoder(path), unicoder(new_path)))
all_ok = False
break
@@ -409,7 +398,7 @@ def process_job(nzo):
remove_samples(workdir_complete)
## TV/Movie/Date Renaming code part 2 - rename and move files to parent folder
if all_ok and file_sorter.is_sortfile():
if all_ok and file_sorter.sort_file:
if newfiles:
file_sorter.rename(newfiles, workdir_complete)
workdir_complete, ok = file_sorter.move(workdir_complete)
@@ -539,6 +528,7 @@ def process_job(nzo):
def parring(nzo, workdir):
""" Perform par processing. Returns: (par_error, re_add)
"""
assert isinstance(nzo, sabnzbd.nzbstuff.NzbObject)
filename = nzo.final_name
growler.send_notification(T('Post-processing'), nzo.final_name, 'pp')
logging.info('Par2 check starting on %s', filename)
@@ -561,12 +551,12 @@ def parring(nzo, workdir):
need_re_add, res = par2_repair(parfile_nzf, nzo, workdir, set_)
if need_re_add:
re_add = True
else:
par_error = par_error or not res
par_error = par_error or not res
if re_add:
logging.info('Readded %s to queue', filename)
nzo.priority = REPAIR_PRIORITY
if nzo.priority != TOP_PRIORITY:
nzo.priority = REPAIR_PRIORITY
sabnzbd.nzbqueue.add_nzo(nzo)
sabnzbd.downloader.Downloader.do.resume_from_postproc()
@@ -574,7 +564,7 @@ def parring(nzo, workdir):
if (par_error and not re_add) or not repair_sets:
# See if alternative SFV check is possible
if cfg.sfv_check():
if cfg.sfv_check() and not (flag_file(workdir, VERIFIED_FILE) and not repair_sets):
sfvs = globber(workdir, '*.sfv')
else:
sfvs = None
@@ -582,8 +572,12 @@ def parring(nzo, workdir):
par_error = False
nzo.set_unpack_info('Repair', T('Trying SFV verification'))
for sfv in sfvs:
if not sfv_check(sfv):
nzo.set_unpack_info('Repair', T('Some files failed to verify against "%s"') % unicoder(os.path.basename(sfv)))
failed = sfv_check(sfv)
if failed:
msg = T('Some files failed to verify against "%s"') % unicoder(os.path.basename(sfv))
msg += '; '
msg += '; '.join(failed)
nzo.set_unpack_info('Repair', msg)
par_error = True
if not par_error:
nzo.set_unpack_info('Repair', T('Verified successfully using SFV files'))
@@ -592,7 +586,7 @@ def parring(nzo, workdir):
nzo.set_unpack_info('Repair', T('[%s] No par2 sets') % unicoder(filename))
if not par_error:
verified_flag_file(workdir, create=True)
flag_file(workdir, VERIFIED_FILE, create=True)
return par_error, re_add

View File

@@ -31,7 +31,7 @@ from sabnzbd.constants import *
from sabnzbd.decorators import synchronized
import sabnzbd.config as config
import sabnzbd.cfg as cfg
from sabnzbd.misc import cat_convert, sanitize_foldername, wildcard_to_re, cat_to_opts
from sabnzbd.misc import cat_convert, sanitize_foldername, wildcard_to_re, cat_to_opts, match_str
import sabnzbd.emailer as emailer
from sabnzbd.encoding import latin1, unicoder, xml_name
@@ -125,7 +125,7 @@ def convert_filter(text):
try:
return re.compile(txt, re.I)
except:
logging.error(Ta('Could not compile regex: %s'), text)
logging.debug('Could not compile regex: %s', text)
return None
_EXPIRE_SEC = 3*24*3600 # 3 days
@@ -407,7 +407,10 @@ class RSSQueue(object):
result = False
break
else:
found = re.search(regexes[n], title)
if regexes[n]:
found = re.search(regexes[n], title)
else:
found = False
if reTypes[n] == 'M' and not found:
logging.debug("Filter rejected on rule %d", n)
result = False
@@ -653,12 +656,12 @@ def _get_link(uri, entry):
def special_rss_site(url):
""" Return True if url describes an RSS site with odd titles
"""
return cfg.rss_filenames() or 'nzbindex.nl/' in url or 'nzbindex.com/' in url or 'nzbclub.com/' in url
return cfg.rss_filenames() or match_str(url, cfg.rss_odd_titles())
_ENCL_SITES = ('nzbindex.nl', 'nzbindex.com', 'animeusenet.org', 'nzbclub.com')
def encl_sites(url, link):
""" Return True if url or link match sites that use enclosures
""" Return True if url or link matches sites that use enclosures
"""
for site in _ENCL_SITES:
if site in url or (link and site in link):

View File

@@ -29,6 +29,8 @@ import sabnzbd.api as api
import sabnzbd.scheduler as scheduler
from sabnzbd.downloader import Downloader
import sabnzbd.cfg as cfg
from sabnzbd.constants import MEBI
from sabnzbd.misc import to_units
import os
import cherrypy
@@ -71,23 +73,19 @@ class SABTrayThread(SysTrayIconThread):
""" Update menu info, once every 10 calls """
self.counter += 1
if self.counter > 10:
status = api.qstatus_data()
state = status.get('state', "SABnzbd")
self.sabpaused = status.get('paused', False)
self.sabpaused, bytes_left, bpsnow, time_left = api.fast_queue()
mb_left = to_units(bytes_left, dec_limit=1)
speed = to_units(bpsnow, dec_limit=1)
if state == 'IDLE':
self.hover_text = T('Idle')
self.icon = self.sabicons['default']
elif state == 'PAUSED':
if self.sabpaused:
self.hover_text = T('Paused')
self.icon = self.sabicons['pause']
elif state == 'DOWNLOADING':
self.hover_text = "%sB/s %s: %s MB (%s)" % (status.get('speed', "---"), T('Remaining'), str(int(status.get('mbleft', "0"))), status.get('timeleft', "---"))
elif bytes_left > 0:
self.hover_text = "%sB/s %s: %sB (%s)" % (speed, T('Remaining'), mb_left, time_left)
self.icon = self.sabicons['green']
else:
self.hover_text = '??'
self.icon = self.sabicons['pause']
self.hover_text = T('Idle')
self.icon = self.sabicons['default']
self.refresh_icon()
self.counter = 0

View File

@@ -82,7 +82,7 @@ def init():
continue
if d.isdigit():
d = [int(d)]
d = [int(i) for i in d]
else:
d = range(1, 8)
@@ -116,6 +116,8 @@ def init():
elif action_name == 'rss_scan':
action = rss.run_method
rss_planned = True
elif action_name == 'remove_failed':
action = sabnzbd.api.history_remove_failed
else:
logging.warning(Ta('Unknown action: %s'), action_name)
continue

View File

@@ -62,6 +62,7 @@ SKIN_TEXT = {
'sch-resume_post' : TT('Resume post-processing'), #: #: Config->Scheduler
'sch-scan_folder' : TT('Scan watched folder'), #: #: Config->Scheduler
'sch-rss_scan' : TT('Read RSS feeds'), #: #: Config->Scheduler
'sch-remove_failed' : TT('Remove failed jobs'), #: Config->Scheduler
# General texts
'default' : TT('Default'), #: Default value, used in dropdown menus
@@ -298,6 +299,8 @@ SKIN_TEXT = {
'explain-https_cert' : TT('File name or path to HTTPS Certificate.'),
'opt-https_key' : TT('HTTPS Key'),
'explain-https_key' : TT('File name or path to HTTPS Key.'),
'opt-https_chain' : TT('HTTPS Chain Certifcates'),
'explain-https_chain' : TT('File name or path to HTTPS Chain.'),
'tuning' : TT('Tuning'),
'opt-refresh_rate' : TT('Queue auto refresh interval:'),
'explain-refresh_rate' : TT('Refresh interval of the queue web-interface page(sec, 0= none).'),
@@ -415,6 +418,7 @@ SKIN_TEXT = {
'explain-auto_sort' : TT('Automatically sort items by (average) age.'),
'opt-check_new_rel' : TT('Check for New Release'),
'explain-check_new_rel' : TT('Weekly check for new SABnzbd release.'),
'also-test' : TT('Also test releases'), #: Pick list for weekly test for new releases
'opt-replace_spaces' : TT('Replace Spaces in Foldername'),
'explain-replace_spaces' : TT('Replace spaces with underscores in folder names.'),
'opt-replace_dots' : TT('Replace dots in Foldername'),
@@ -550,6 +554,10 @@ SKIN_TEXT = {
'explain-growl_password' : TT('Optional password for Growl server'), #: Don't translate "Growl"
'opt-ntfosd_enable' : TT('Enable NotifyOSD'), #: Don't translate "NotifyOSD"
'explain-ntfosd_enable' : TT('Send notifications to NotifyOSD'), #: Don't translate "NotifyOSD"
'opt-ncenter_enable' : TT('Notification Center'),
'explain-ncenter_enable' : TT('Send notifications to Notification Center'),
'opt-notify_classes' : TT('Notification classes'),
'explain-notify_classes' : TT('Enable classes of messages to be reported (none, one or multiple)'),
'testNotify' : TT('Test Notification'),
# Config->Newzbin

View File

@@ -28,7 +28,7 @@ import re
import sabnzbd
from sabnzbd.misc import move_to_path, cleanup_empty_directories, get_unique_path, \
get_unique_filename, get_ext, renamer, remove_dir, sanitize_foldername
get_unique_filename, get_ext, renamer, sanitize_foldername
from sabnzbd.constants import series_match, date_match, year_match, sample_match
import sabnzbd.cfg as cfg
from sabnzbd.encoding import titler
@@ -37,8 +37,8 @@ RE_SAMPLE = re.compile(sample_match, re.I)
# Do not rename .vob files as they are usually DVD's
EXCLUDED_FILE_EXTS = ('.vob', '.bin')
LOWERCASE = ('the','of','and','at','vs','a','an','but','nor','for','on',\
'so','yet')
LOWERCASE = ('the', 'of', 'and', 'at', 'vs', 'a', 'an', 'but', 'nor', 'for', 'on', \
'so', 'yet')
UPPERCASE = ('III', 'II', 'IV')
REPLACE_AFTER = {
@@ -63,33 +63,38 @@ COUNTRY_REP = ('(US)', '(UK)', '(EU)', '(CA)', '(YU)', '(VE)', '(TR)', '(CH)', \
_RE_ENDEXT = re.compile(r'\.%ext[{}]*$', re.I)
def endswith_ext(path):
m = _RE_ENDEXT.search(path)
return m is not None
""" Return True when path ends with '.%ext'
"""
return _RE_ENDEXT.search(path) is not None
def move_to_parent_folder(workdir):
""" Move content of 'workdir' to 'workdir/..' (except marker file)
""" Move all in 'workdir' into 'workdir/..'
"""
path1 = os.path.abspath(os.path.normpath(os.path.join(workdir, '..'))) #move things to the folder below
# Determine 'folder'/..
workdir = os.path.abspath(os.path.normpath(workdir))
dest = os.path.normpath(os.path.join(workdir, '..'))
# Check for DVD folders and stop if found
for item in os.listdir(workdir):
if item.lower() in ('video_ts', 'audio_ts', 'bdmv'):
return workdir, True
for root, dirs, files in os.walk(workdir):
for _file in files:
path = os.path.join(root, _file)
new_path = path.replace(workdir, path1)
new_path = get_unique_filename(new_path)
if not move_to_path(path, new_path, False):
return path1, False
new_path = path.replace(workdir, dest)
ok, new_path = move_to_path(path, new_path)
if not ok:
return dest, False
cleanup_empty_directories(workdir)
try:
remove_dir(workdir)
except:
pass
return path1, True
return dest, True
class Sorter(object):
""" Generic Sorter class
"""
def __init__(self, cat):
self.sorter = None
self.type = None
@@ -98,22 +103,24 @@ class Sorter(object):
self.ext = ''
def detect(self, dirname, complete_dir):
""" Detect which kind of sort applies
"""
self.sorter = SeriesSorter(dirname, complete_dir, self.cat)
if self.sorter.is_match():
if self.sorter.matched:
complete_dir = self.sorter.get_final_path()
self.type = 'tv'
self.sort_file = True
return complete_dir
self.sorter = DateSorter(dirname, complete_dir, self.cat)
if self.sorter.is_match():
if self.sorter.matched:
complete_dir = self.sorter.get_final_path()
self.type = 'date'
self.sort_file = True
return complete_dir
self.sorter = GenericSorter(dirname, complete_dir, self.cat)
if self.sorter.is_match():
if self.sorter.matched:
complete_dir = self.sorter.get_final_path()
self.type = 'movie'
self.sort_file = True
@@ -123,12 +130,15 @@ class Sorter(object):
return complete_dir
def rename(self, newfiles, workdir_complete):
if self.sorter.should_rename():
""" Rename files of the job
"""
if self.sorter.rename_or_not:
self.sorter.rename(newfiles, workdir_complete)
def rename_with_ext(self, workdir_complete):
""" Special renamer for %ext """
if self.sorter.should_rename() and '%ext' in workdir_complete and self.ext:
""" Special renamer for %ext
"""
if self.sorter.rename_or_not and '%ext' in workdir_complete and self.ext:
# Replace %ext with extension
newpath = workdir_complete.replace('%ext', self.ext)
try:
@@ -167,10 +177,10 @@ class Sorter(object):
ok = False
return workdir_complete, ok
def is_sortfile(self):
return self.sort_file
class SeriesSorter(object):
""" Methods for Series Sorting
"""
def __init__(self, dirname, path, cat):
self.matched = False
@@ -181,6 +191,7 @@ class SeriesSorter(object):
self.cats = cfg.tv_categories()
self.filename_set = ''
self.fname = '' # Value for %fn substitution in folders
self.final_path = ''
self.match_obj = None
self.extras = None
@@ -199,7 +210,7 @@ class SeriesSorter(object):
if force or (cfg.enable_tv_sorting() and cfg.tv_sort_string()):
if force or (not self.cats) or (self.cat and self.cat.lower() in self.cats) or (not self.cat and 'None' in self.cats):
#First check if the show matches TV episode regular expressions. Returns regex match object
self.match_obj, self.extras = check_regexs(self.original_dirname, series_match, double=True)
self.match_obj, self.extras = check_regexs(self.original_dirname, series_match)
if self.match_obj:
logging.debug("Found TV Show - Starting folder sort (%s)", self.original_dirname)
self.matched = True
@@ -211,7 +222,8 @@ class SeriesSorter(object):
def get_final_path(self):
# Collect and construct all the variables such as episode name, show names
""" Collect and construct all the variables such as episode name, show names
"""
if self.get_values():
# Get the final path
path = self.construct_path()
@@ -239,8 +251,8 @@ class SeriesSorter(object):
def get_shownames(self):
''' Get the show name from the match object and format it '''
# Get the formatted title and alternate title formats
self.show_info['show_tname'], self.show_info['show_tname_two'], self.show_info['show_tname_three'] = getTitles(self.match_obj, self.original_dirname, True)
self.show_info['show_name'], self.show_info['show_name_two'], self.show_info['show_name_three'] = getTitles(self.match_obj, self.original_dirname)
self.show_info['show_tname'], self.show_info['show_tname_two'], self.show_info['show_tname_three'] = get_titles(self.match_obj, self.original_dirname, True)
self.show_info['show_name'], self.show_info['show_name_two'], self.show_info['show_name_three'] = get_titles(self.match_obj, self.original_dirname)
def get_seasons(self):
@@ -289,7 +301,7 @@ class SeriesSorter(object):
def get_showdescriptions(self):
''' Get the show descriptions from the match object and format them '''
self.show_info['ep_name'], self.show_info['ep_name_two'], self.show_info['ep_name_three'] = getDescriptions(self.match_obj, self.original_dirname)
self.show_info['ep_name'], self.show_info['ep_name_two'], self.show_info['ep_name_three'] = get_descriptions(self.match_obj, self.original_dirname)
def get_values(self):
@@ -367,10 +379,10 @@ class SeriesSorter(object):
path = path.replace(key, name)
# Lowercase all characters encased in {}
path = toLowercase(path)
path = to_lowercase(path)
# Strip any extra ' ' '.' or '_' around foldernames
path = stripFolders(path)
path = strip_folders(path)
# Split the last part of the path up for the renamer
if extension:
@@ -382,12 +394,10 @@ class SeriesSorter(object):
return os.path.normpath(head)
def should_rename(self):
return self.rename_or_not
def rename(self, files, current_path):
""" Rename for Series
"""
logging.debug("Renaming Series")
renamed = None
largest = (None, None, 0)
def to_filepath(f, current_path):
@@ -419,18 +429,18 @@ class SeriesSorter(object):
self.fname = tmp
newname = "%s%s" % (self.filename_set, self.ext)
# Replace %fn with the original filename
newname = newname.replace('%fn',tmp)
newname = newname.replace('%fn', tmp)
newpath = os.path.join(current_path, newname)
# Replace %ext with extension
newpath = newpath.replace('%ext', self.ext)
if not os.path.exists(newpath):
try:
logging.debug("Rename: %s to %s", filepath,newpath)
renamer(filepath,newpath)
logging.debug("Rename: %s to %s", filepath, newpath)
renamer(filepath, newpath)
except:
logging.error("Failed to rename: %s to %s", current_path, newpath)
logging.info("Traceback: ", exc_info = True)
rename_similar(current_path, file, self.filename_set)
rename_similar(current_path, self.ext, self.filename_set)
else:
logging.debug('Current path already exists, skipping rename, %s', newpath)
else:
@@ -443,6 +453,8 @@ _RE_MULTIPLE = ( \
re.compile(r'\w\W([\w\d])\W', re.I) # blah-1-ok.avi blah-a-ok.avi
)
def check_for_multiple(files):
""" Return list of files that looks like a multi-part post
"""
for regex in _RE_MULTIPLE:
matched_files = check_for_sequence(regex, files)
if matched_files:
@@ -451,6 +463,8 @@ def check_for_multiple(files):
def check_for_sequence(regex, files):
""" Return list of files that looks like a sequence, using 'regex'
"""
matches = {}
prefix = None
# Build up a dictionary of matches
@@ -502,6 +516,8 @@ def check_for_sequence(regex, files):
class GenericSorter(object):
""" Methods for Generic Sorting
"""
def __init__(self, dirname, path, cat):
self.matched = False
@@ -513,6 +529,7 @@ class GenericSorter(object):
self.cat = cat
self.filename_set = ''
self.fname = '' # Value for %fn substitution in folders
self.final_path = ''
self.match_obj = None
@@ -525,7 +542,8 @@ class GenericSorter(object):
def match(self, force=False):
''' Checks the category for a match, if so set self.match to true '''
""" Checks the category for a match, if so set self.match to true
"""
if force or (cfg.enable_movie_sorting() and self.sort_string):
#First check if the show matches TV episode regular expressions. Returns regex match object
if force or (self.cat and self.cat.lower() in self.cats) or (not self.cat and 'None' in self.cats):
@@ -533,14 +551,9 @@ class GenericSorter(object):
self.matched = True
def is_match(self):
''' Returns whether there was a match or not '''
return self.matched
def get_final_path(self):
# Collect and construct all the variables such as episode name, show names
""" Collect and construct all the variables such as episode name, show names
"""
if self.get_values():
# Get the final path
path = self.construct_path()
@@ -551,32 +564,33 @@ class GenericSorter(object):
return os.path.join(self.original_path, self.original_dirname)
def get_values(self):
""" Collect and construct all the values needed for path replacement """
""" Collect and construct all the values needed for path replacement
"""
## - Get Year
dirname = self.original_dirname.replace('_', ' ')
RE_YEAR = re.compile(year_match, re.I)
year_m = RE_YEAR.search(dirname)
if year_m:
# Find the last matched date
# Keep year_m to use in getTitles
# Keep year_m to use in get_titles
year = RE_YEAR.findall(dirname)[-1][0]
self.movie_info['year'] = year
else:
self.movie_info['year'] = ''
## - Get Decades
self.movie_info['decade'], self.movie_info['decade_two'] = getDecades(self.movie_info['year'])
self.movie_info['decade'], self.movie_info['decade_two'] = get_decades(self.movie_info['year'])
## - Get Title
self.movie_info['ttitle'], self.movie_info['ttitle_two'], self.movie_info['ttitle_three'] = getTitles(year_m, self.original_dirname, True)
self.movie_info['title'], self.movie_info['title_two'], self.movie_info['title_three'] = getTitles(year_m, self.original_dirname)
self.movie_info['ttitle'], self.movie_info['ttitle_two'], self.movie_info['ttitle_three'] = get_titles(year_m, self.original_dirname, True)
self.movie_info['title'], self.movie_info['title_two'], self.movie_info['title_three'] = get_titles(year_m, self.original_dirname)
return True
def construct_path(self):
""" Return path reconstructed from orginal and sort expression
"""
sorter = self.sort_string.replace('\\', '/')
mapping = []
@@ -618,10 +632,10 @@ class GenericSorter(object):
# Lowercase all characters encased in {}
path = toLowercase(path)
path = to_lowercase(path)
# Strip any extra ' ' '.' or '_' around foldernames
path = stripFolders(path)
path = strip_folders(path)
# Split the last part of the path up for the renamer
if extension:
@@ -633,10 +647,10 @@ class GenericSorter(object):
return os.path.normpath(head)
def should_rename(self):
return self.rename_or_not
def rename(self, _files, current_path):
""" Rename for Generic files
"""
logging.debug("Renaming Generic file")
def filter_files(_file, current_path):
if is_full_path(_file):
@@ -650,8 +664,7 @@ class GenericSorter(object):
return True
return False
renamed = False
# remove any files below 300MB from this list
# remove any files below the limit from this list
files = [_file for _file in _files if filter_files(_file, current_path)]
length = len(files)
@@ -665,16 +678,16 @@ class GenericSorter(object):
if os.path.exists(filepath):
tmp, ext = os.path.splitext(file)
self.fname = tmp
newname = "%s%s" % (self.filename_set,ext)
newname = newname.replace('%fn',tmp)
newname = "%s%s" % (self.filename_set, ext)
newname = newname.replace('%fn', tmp)
newpath = os.path.join(current_path, newname)
try:
logging.debug("Rename: %s to %s", filepath,newpath)
renamer(filepath,newpath)
logging.debug("Rename: %s to %s", filepath, newpath)
renamer(filepath, newpath)
except:
logging.error(Ta('Failed to rename: %s to %s'), filepath, newpath)
logging.info("Traceback: ", exc_info = True)
rename_similar(current_path, file, self.filename_set)
rename_similar(current_path, ext, self.filename_set)
## Sequence File Handling
# if there is more than one extracted file check for CD1/1/A in the title
@@ -688,21 +701,23 @@ class GenericSorter(object):
tmp, ext = os.path.splitext(file)
self.fname = tmp
name = '%s%s' % (self.filename_set, self.extra)
name = name.replace('%1', str(index)).replace('%fn',tmp)
name = name.replace('%1', str(index)).replace('%fn', tmp)
name = name + ext
newpath = os.path.join(current_path, name)
try:
logging.debug("Rename: %s to %s", filepath,newpath)
renamer(filepath,newpath)
logging.debug("Rename: %s to %s", filepath, newpath)
renamer(filepath, newpath)
except:
logging.error(Ta('Failed to rename: %s to %s'), filepath, newpath)
logging.info("Traceback: ", exc_info = True)
rename_similar(current_path, file, self.filename_set)
rename_similar(current_path, ext, self.filename_set)
else:
logging.debug("Movie files not in sequence %s", _files)
class DateSorter(object):
""" Methods for Date Sorting
"""
def __init__(self, dirname, path, cat):
self.matched = False
@@ -731,7 +746,7 @@ class DateSorter(object):
if force or (cfg.enable_date_sorting() and self.sort_string):
#First check if the show matches TV episode regular expressions. Returns regex match object
if force or (self.cat and self.cat.lower() in self.cats) or (not self.cat and 'None' in self.cats):
self.match_obj, self.date_type = checkForDate(self.original_dirname, date_match)
self.match_obj, self.date_type = check_for_date(self.original_dirname, date_match)
if self.match_obj:
logging.debug("Date Sorting - Starting folder sort (%s)", self.original_dirname)
self.matched = True
@@ -743,7 +758,8 @@ class DateSorter(object):
def get_final_path(self):
# Collect and construct all the variables such as episode name, show names
""" Collect and construct all the variables such as episode name, show names
"""
if self.get_values():
# Get the final path
path = self.construct_path()
@@ -769,25 +785,26 @@ class DateSorter(object):
self.date_info['date_two'] = self.date_info['date'].rjust(2,'0')
## - Get Decades
self.date_info['decade'], self.date_info['decade_two'] = getDecades(self.date_info['year'])
self.date_info['decade'], self.date_info['decade_two'] = get_decades(self.date_info['year'])
## - Get Title
self.date_info['ttitle'], self.date_info['ttitle_two'], self.date_info['ttitle_three'] = getTitles(self.match_obj, self.original_dirname, True)
self.date_info['title'], self.date_info['title_two'], self.date_info['title_three'] = getTitles(self.match_obj, self.original_dirname)
self.date_info['ttitle'], self.date_info['ttitle_two'], self.date_info['ttitle_three'] = get_titles(self.match_obj, self.original_dirname, True)
self.date_info['title'], self.date_info['title_two'], self.date_info['title_three'] = get_titles(self.match_obj, self.original_dirname)
self.date_info['ep_name'], self.date_info['ep_name_two'], self.date_info['ep_name_three'] = getDescriptions(self.match_obj, self.original_dirname)
self.date_info['ep_name'], self.date_info['ep_name_two'], self.date_info['ep_name_three'] = get_descriptions(self.match_obj, self.original_dirname)
return True
def construct_path(self):
""" Return path reconstructed from orginal and sort expression
"""
sorter = self.sort_string.replace('\\', '/')
mapping = []
if endswith_ext(sorter):
extension = True
sorter= sorter.replace(".%ext", '')
sorter = sorter.replace(".%ext", '')
else:
extension = False
@@ -838,10 +855,10 @@ class DateSorter(object):
path = path.replace(key, name)
# Lowercase all characters encased in {}
path = toLowercase(path)
path = to_lowercase(path)
# Strip any extra ' ' '.' or '_' around foldernames
path = stripFolders(path)
path = strip_folders(path)
# Split the last part of the path up for the renamer
if extension:
@@ -853,12 +870,11 @@ class DateSorter(object):
return os.path.normpath(head)
def should_rename(self):
return self.rename_or_not
def rename(self, files, current_path):
""" Renaming Date file
"""
logging.debug("Renaming Date file")
renamed = None
#find the master file to rename
for file in files:
if is_full_path(file):
@@ -868,21 +884,21 @@ class DateSorter(object):
if os.path.exists(filepath):
size = os.stat(filepath).st_size
if size > 130000000:
if size > cfg.movie_rename_limit.get_int():
if 'sample' not in file:
tmp, ext = os.path.splitext(file)
self.fname = tmp
newname = "%s%s" % (self.filename_set,ext)
newname = newname.replace('%fn',tmp)
newname = "%s%s" % (self.filename_set, ext)
newname = newname.replace('%fn', tmp)
newpath = os.path.join(current_path, newname)
if not os.path.exists(newpath):
try:
logging.debug("Rename: %s to %s", filepath,newpath)
renamer(filepath,newpath)
logging.debug("Rename: %s to %s", filepath, newpath)
renamer(filepath, newpath)
except:
logging.error(Ta('Failed to rename: %s to %s'), current_path, newpath)
logging.info("Traceback: ", exc_info = True)
rename_similar(current_path, file, self.filename_set)
rename_similar(current_path, ext, self.filename_set)
break
@@ -909,7 +925,7 @@ def path_subst(path, mapping):
return ''.join(newpath)
def getTitles(match, name, titleing=False):
def get_titles(match, name, titleing=False):
'''
The title will be the part before the match
Clean it up and title() it
@@ -989,7 +1005,7 @@ def replace_word(input, one, two):
input = input.replace(one, two)
return input
def getDescriptions(match, name):
def get_descriptions(match, name):
'''
If present, get a description from the nzb name.
A description has to be after the matched item, seperated either
@@ -1010,7 +1026,9 @@ def getDescriptions(match, name):
return ep_name, ep_name2, ep_name3
def getDecades(year):
def get_decades(year):
""" Return 4 digit and 2 digit decades given 'year'
"""
if year:
try:
decade = year[2:3]+'0'
@@ -1023,14 +1041,18 @@ def getDecades(year):
decade2 = ''
return decade, decade2
def check_for_folder(path):
""" Return True if any folder is found in the tree at 'path'
"""
for root, dirs, files in os.walk(path):
if dirs:
return True
return False
_RE_LOWERCASE = re.compile(r'{([^{]*)}')
def toLowercase(path):
def to_lowercase(path):
''' Lowercases any characters enclosed in {} '''
while True:
m = _RE_LOWERCASE.search(path)
@@ -1043,17 +1065,25 @@ def toLowercase(path):
path = path.replace('}', '')
return path
def stripFolders(folders):
f = folders.strip('/').split('/')
def strip_folders(path):
""" Return 'path' without leading and trailing spaces and underscores in each element
For Windows, also remove leading and trailing dots
"""
f = path.strip('/').split('/')
# For path beginning with a slash, insert empty element to prevent loss
if folders.strip()[0] in '/\\':
if path.strip()[0] in '/\\':
f.insert(0, '')
def strip_all(x):
""" Strip all leading/trailing underscores
also dots for Windows
"""
x = x.strip().strip('_')
if sabnzbd.WIN32:
# Don't want to strip . from folders such as /.sabnzbd/
# OSX and Linux should keep dots, because leading dots are significant
# while Windows cannot handle trailing dots
x = x.strip('.')
x = x.strip()
return x
@@ -1061,41 +1091,44 @@ def stripFolders(folders):
return os.path.normpath('/'.join([strip_all(x) for x in f]))
def rename_similar(path, file, name):
logging.debug('Renaming files similar to: %s to %s', file, name)
file_prefix, ext = os.path.splitext(file)
file_prefix = file_prefix.lower()
for root, dirs, files in os.walk(path):
for _file in files:
fpath = os.path.join(root, _file)
tmp, ext = os.path.splitext(_file)
if tmp.lower() == file_prefix:
newname = "%s%s" % (name,ext)
newname = newname.replace('%fn',tmp)
newpath = os.path.join(path, newname)
if not os.path.exists(newpath):
try:
logging.debug("Rename: %s to %s", fpath,newpath)
renamer(fpath,newpath)
except:
logging.error(Ta('Failed to rename similar file: %s to %s'), path, newpath)
logging.info("Traceback: ", exc_info = True)
def rename_similar(folder, skip_ext, name):
""" Rename all other files in the 'folder' hierarchy after 'name'
and move them to the root of 'folder'.
Files having extension 'skip_ext' will be moved, but not renamed.
"""
logging.debug('Give files in set "%s" matching names.', name)
folder = os.path.normpath(folder)
skip_ext = skip_ext.lower()
for root, dirs, files in os.walk(folder):
for f in files:
path = os.path.join(root, f)
org, ext = os.path.splitext(f)
if ext.lower() == skip_ext:
# Move file, but do not rename
newpath = os.path.join(folder, f)
else:
# Move file and rename
newname = "%s%s" % (name, ext)
newname = newname.replace('%fn', org)
newpath = os.path.join(folder, newname)
if path != newpath:
newpath = get_unique_filename(newpath)
try:
logging.debug("Rename: %s to %s", path, newpath)
renamer(path, newpath)
except:
logging.error(Ta('Failed to rename similar file: %s to %s'), path, newpath)
logging.info("Traceback: ", exc_info=True)
cleanup_empty_directories(folder)
def check_regexs(filename, matchers, double=False):
def check_regexs(filename, matchers):
"""
Regular Expression match for a list of regexes
Returns the MatchObject if a match is made
This version checks for an additional match
"""
#if double:
# matcher, extramatchers = matchers
#else:
# matcher = matchers
# extramatchers = []
extras = []
for expressions in matchers:
expression, extramatchers = expressions
@@ -1104,7 +1137,7 @@ def check_regexs(filename, matchers, double=False):
if match1:
for m in extramatchers:
regex = re.compile(m)
match2 = regex.findall(filename,match1.end())
match2 = regex.findall(filename, match1.end())
if match2:
for match in match2:
if type(match) == type(()) and len(match) > 1:
@@ -1116,12 +1149,11 @@ def check_regexs(filename, matchers, double=False):
return None, None
def checkForDate(filename, matcher):
def check_for_date(filename, matcher):
"""
Regular Expression match for date based files
Returns the MatchObject if a match is made
"""
match2 = None
x = 0
if matcher:
for expression in matcher:
@@ -1133,6 +1165,8 @@ def checkForDate(filename, matcher):
return None, 0
def is_full_path(file):
""" Return True if path is absolute
"""
if file.startswith('\\') or file.startswith('/'):
return True
try:

View File

@@ -49,7 +49,6 @@ class URLGrabber(Thread):
def __init__(self):
Thread.__init__(self)
now = time.time()
self.queue = Queue.Queue()
for tup in NzbQueue.do.get_urls():
url, nzo = tup
@@ -115,7 +114,10 @@ class URLGrabber(Thread):
logging.info('Removing nzbmatrix bookmark %s', matrix_id)
else:
logging.info('Grabbing URL %s', url)
opener = urllib.FancyURLopener({})
if '.nzbsrus.' in url:
opener = urllib.URLopener({})
else:
opener = urllib.FancyURLopener({})
opener.prompt_user_passwd = None
opener.addheaders = []
opener.addheader('User-Agent', 'SABnzbd+/%s' % sabnzbd.version.__version__)
@@ -165,19 +167,20 @@ class URLGrabber(Thread):
misc.bad_fetch(future_nzo, clean_matrix_url(url), msg, retry=True)
continue
category = _MATRIX_MAP.get(category, category)
if del_bookmark:
# No retries of nzbmatrix bookmark removals
continue
else:
msg = ''
retry = True
if del_bookmark:
# No retries of nzbmatrix bookmark removals
continue
# Check if the filepath is specified, if not, check if a retry is allowed.
if not fn:
logging.info('Retry URL %s', url)
self.add(url, future_nzo, 5)
continue
fn, msg, retry, wait = _analyse_others(fn, url)
if not fn:
if retry:
logging.info('Retry URL %s', url)
self.add(url, future_nzo, wait)
else:
misc.bad_fetch(future_nzo, url, msg, retry=True)
continue
if not filename:
filename = os.path.basename(url) + '.nzb'
@@ -228,7 +231,8 @@ class URLGrabber(Thread):
#-------------------------------------------------------------------------------
_RE_NZBMATRIX = re.compile(r'nzbmatrix.com/(.*)[\?&]id=(\d+)', re.I)
_RE_NZBMATRIX = re.compile(r'nzbmatrix\.com/(.*)[\?&]id=(\d+)', re.I)
_RE_NZBXXX = re.compile(r'nzbxxx\.com/(.*)[\?&]id=(\d+)', re.I)
_RE_NZBMATRIX_USER = re.compile(r'&username=([^&=]+)', re.I)
_RE_NZBMATRIX_API = re.compile(r'&apikey=([^&=]+)', re.I)
@@ -237,22 +241,38 @@ def _matrix_url(url):
matrix_id = 0
m = _RE_NZBMATRIX.search(url)
if not m:
mx = _RE_NZBXXX.search(url)
if m:
site = 'nzbmatrix.com'
user = urllib.quote_plus(cfg.matrix_username())
key = urllib.quote_plus(cfg.matrix_apikey())
elif mx:
site = 'nzbxxx.com'
user = urllib.quote_plus(cfg.xxx_username())
key = urllib.quote_plus(cfg.xxx_apikey())
m = mx
if m:
matrix_id = m.group(2)
if not _RE_NZBMATRIX_USER.search(url) or not _RE_NZBMATRIX_API.search(url):
user = urllib.quote_plus(cfg.matrix_username())
key = urllib.quote_plus(cfg.matrix_apikey())
url = '%s://api.nzbmatrix.com/v1.1/download.php?id=%s&username=%s&apikey=%s' % \
(_PROTOCOL, matrix_id, user, key)
url = '%s://api.%s/v1.1/download.php?id=%s&username=%s&apikey=%s' % \
(_PROTOCOL, site, matrix_id, user, key)
return url, matrix_id
def clean_matrix_url(url):
''' Return nzbmatrix url without user credentials '''
site = 'nzbmatrix.com'
m = _RE_NZBMATRIX.search(url)
if not m:
m = _RE_NZBXXX.search(url)
site = 'nzbxxx.com'
if m:
matrix_id = m.group(2)
url = '%s://api.nzbmatrix.com/v1.1/download.php?id=%s' % (_PROTOCOL, matrix_id)
url = '%s://api.%s/v1.1/download.php?id=%s' % (_PROTOCOL, site, matrix_id)
return url
@@ -313,6 +333,50 @@ def _analyse_matrix(fn, matrix_id):
return fn, msg, False, 0
RUS_FATAL = ('DENIED_MISSING_CREDENTIALS', 'DENIED_NO_ACCOUNT',
'DENIED_INVALID_CREDENTIALS', 'INCORRECT_URL',
'NZB_DELETED', 'POST_NUKED', 'FILE_UNAVAILABLE'
)
RUS_15M = ('SQL_ERROR', 'SERVICE_OFFLINE')
RUS_60M = ('MAX_DOWNLOAD_REACHED_UPGRADE_TO_VIP', 'MAX_DOWNLOAD_REACHED')
def _analyse_others(fn, url):
""" Analyse respons of indexer
returns fn|None, error-message|None, retry, wait-seconds
"""
msg = ''
wait = 0
if not fn:
logging.debug('No response from indexer, retry after 60 sec')
return None, msg, True, 60
try:
f = open(fn, 'r')
data = f.read(100)
f.close()
except:
logging.debug('Problem with tempfile %s from indexer, retry after 60 sec', fn)
return None, msg, True, 60
# Check for an error response
if not data:
logging.debug('Received nothing from indexer, retry after 60 sec')
return None, msg, True, 60
if '.nzbsrus.' in url:
# Partial support for nzbsrus.com's API
if misc.match_str(data, RUS_FATAL):
logging.debug('nzbsrus says: %s, abort', data)
return None, data, False, 0
if misc.match_str(data, RUS_15M):
logging.debug('nzbsrus says: %s, wait 15m', data)
return None, data, True, 900
if misc.match_str(data, RUS_60M):
logging.debug('nzbsrus says: %s, wait 60m', data)
return None, data, True, 3600
return fn, msg, False, 0
#------------------------------------------------------------------------------
_MATRIX_MAP = {
'28' : 'anime.all',

View File

@@ -105,7 +105,7 @@ def test_nntp_server(host, port, server=None, username=None, password=None, ssl=
if not username or not password:
nw.nntp.sock.sendall('ARTICLE test\r\n')
nw.nntp.sock.sendall('ARTICLE <test@home>\r\n')
try:
nw.lines = []
nw.recv_chunk(block=True)