Compare commits

...

221 Commits

Author SHA1 Message Date
shypike 1df2943d05 Update text files for 0.7.20 11 years ago
shypike 543eeae124 Make sure that Rating support is suppressed completely when disabled. 11 years ago
shypike 8ed2f7a175 Update some license files. 11 years ago
shypike 074c78ce31 Update text files for 0.7.20RC2 11 years ago
shypike 01b93ce8eb More flexibility in Rating meta data. 11 years ago
shypike ff42913359 Update unrar to 5.11 for (Snow)Leopard on Intel, but not for PPC. 11 years ago
shypike 9b3dcb0bda Merge pull request #184 from sanderjo/0.7.x 11 years ago
shypike a9cfff0ce3 Update text files for 0.7.20RC1 11 years ago
shypike ca4db6b428 Support for the NZB meta field "x-rating-host". 11 years ago
shypike f986f649d2 Let the API-call "Retry" return the new nzo_id of the job. 11 years ago
shypike 48b47535b6 Update copyright year. 11 years ago
shypike 194fbce7ae Added an issue and made some language corrections. 11 years ago
shypike 17ae4d9974 OSX Yosemite: make top-menu icon compatible with "Dark Mode". 11 years ago
shypike cdaf7ae973 Fix problem of testing email server with existing parameters. 11 years ago
shypike 1390a17950 Update unrar for OSX to 5.11 11 years ago
shypike 852b0e3cc2 Update text files for 0.7.19 11 years ago
shypike d3715e0ab9 Support double quotes to delineate parameters in category match lists. 11 years ago
shypike 8d28d8f000 Correct spelling of OSX release names. 11 years ago
shypike 1a3dd15609 When sanitizing names, preserve "." and ".." elements in paths. 11 years ago
shypike ad58dde0e0 The after-unrar-check needs to take the "flat_unpack" option into account. 11 years ago
sanderjo da317d9054 Scheduling logging in proper format (hh:mm), so 08:05 instead of 8:5 11 years ago
shypike 9e344114d6 When a comma is present in a file name, quotes are needed when passed to a user script. 11 years ago
shypike 2cc73c8ad1 Update OSX DMG image. 11 years ago
shypike efe473b4d7 Update OSX signing. 11 years ago
shypike f9e02458e7 Update text files for 0.7.19RC4 11 years ago
shypike 07d8ceb284 Fix problem of a job's destination path getting damaged on Windows. 11 years ago
shypike fc141b6cb6 Update text files for 0.7.19RC3 11 years ago
shypike 4b31bea0e3 Change renaming of duplicate files from file.ext-->file.ext.1 to file.ext-->file.1.ext 11 years ago
shypike 44b1b6ca2e Corrected meta-data name 11 years ago
shypike 8da8b62446 Make OSX MountainLion build compatible with Mavericks. 11 years ago
shypike dc03414659 Do not remove a leading dot in a path element. 11 years ago
shypike be7222de8f Windows UNC paths, used as final destination, were damaged. 11 years ago
shypike 0f64495d25 Update text files for release 0.7.19RC3 11 years ago
shypike 9f82347e72 Update OSX signing method 11 years ago
shypike f22b26ba09 Treat RAR CRC errors like "incorrect password" 11 years ago
shypike f9081afe80 Auto-update rating_host value. 11 years ago
SanderJ 4904ff4bd9 Measure and log Pystone performance, and - if possible - CPU type 11 years ago
shypike 8c2eda4e4b Update text files for 0.7.19RC2 11 years ago
shypike 5591127a0e Expose the rating_host setting to Config->Special. 11 years ago
shypike 823ec4fad8 Add Finnish translation 11 years ago
Johannes 'fish' Ziemke 661dc33912 Add Dockerfile 11 years ago
shypike 28370c826a Merge pull request #183 from sanderjo/0.7.x 11 years ago
sanderjo 12086a247f Extra logging in case of Loading .../.sabnzbd/admin/Rating.sab failed 11 years ago
shypike f53c25ccc5 Update text files for 0.7.19RC2 11 years ago
shypike 04d31bd807 OSX Signing is now only possible on OSX Mavericks, so check this. 11 years ago
shypike 433711bdc6 Prevent folder trimming from removing embedded passwords in filenames. 11 years ago
shypike 376428d12d Fix potential problem with timestamps in RSS. 11 years ago
shypike 8969b7c87e Make sure the final destination path is always sanitized and trimmed. 11 years ago
shypike 92bc48c241 When matching SFV files with RAR-sets, do this case-insensitive. 11 years ago
shypike d85708c5f5 Small code improvement "unwanted extensions". 11 years ago
shypike fa4eaf3e42 Merge pull request #180 from sanderjo/0.7.x 11 years ago
sanderjo 8b788551bb cfg.unwanted_extensions() is a list so check must be cfg.unwanted_extensions() != [] 11 years ago
sanderjo db58025e96 Put the last rar immediately the first rar, so that unwanted extensions will get detected earlier. 11 years ago
shypike 9ec397d6a0 Limit article cache to 1G to prevent a memory size bug in the _yenc module. 11 years ago
shypike dc3fe36d8a Upgrade unrar to version 5.11 (OSX and Windows) 11 years ago
shypike d809b13936 Restore Python 2.5 compatibility in "Ratings" change. 11 years ago
shypike c1607c6bf2 Update text files for 0.7.19RC1 11 years ago
shypike 0ff3c5978c Merge pull request #178 from oznzb-dev/0.7.x 11 years ago
shypike c1d283f299 Sort order of RSS feeds incorrect due to UI using wrong time field. 11 years ago
shypike 04637c2c74 Prevent further pauses by "unwanted extension", once the user has resumed the job after the first stop. 11 years ago
shypike 3a3eeba429 Change renaming of duplicate files from file.ext-->file.ext.1 to file.ext-->file.1.ext 11 years ago
oznzb-dev 1404175ac8 OZnzb filtering 11 years ago
shypike 081010d50b Fix for "Range" selection of queue. 11 years ago
shypike 1591954472 Prevent crash when Windows SysTray function hits PyWin bug. 11 years ago
shypike c2a2b61e15 Sort queue on now visible name instead of original name. 11 years ago
shypike 4b6c469831 Remove special URL handling for nzbclub indexer, no longer needed. 11 years ago
shypike 2d10f879da Update text files for 0.7.18 11 years ago
shypike c2043c6b83 Update translations 11 years ago
shypike b3a637d29c Update main POT file. 11 years ago
shypike 67395f0c19 Improve "unwanted extension" text. 11 years ago
shypike 55f0eccb88 Update text files for 0.7.18RC1 11 years ago
shypike e293a669d2 Allow "nzbname" parameter with just the password (like "/password). 11 years ago
shypike 26b79513c6 Update text files for 0.7.18RC1 11 years ago
shypike fa1ef47341 In debug logging mode, use Google to determine our own IP address (IPv4 and IPv6). 11 years ago
shypike 746be02899 Merge pull request #162 from JessThrysoee/0.7.x 11 years ago
shypike 9a4fdd4582 Log more info about failure to remove item from History. 11 years ago
shypike c76f01b54b Merge pull request #165 from oopoa/bandwidth-api 11 years ago
kcrayon 0849039bb4 rename to server_stats 11 years ago
shypike a79d1173de Merge pull request #163 from Der-Jan/0.7.x 11 years ago
shypike 2eeb6cd9c5 Merge pull request #166 from sanderjo/0.7.x 11 years ago
oopoa 7571f146fa add api for bandwidth statistics 11 years ago
sanderjo 6753ed031d Logs in which rar file unwanted extension 11 years ago
shypike f4dd7d0df4 Don't pass seemingly "joinable" files to par2. No longer needed since we use a wildcard. 11 years ago
shypike e5e9b37bf8 Fix bug that prevented multiple sets in one NZB from joining. 11 years ago
shypike 460841ae36 Merge pull request #164 from sanderjo/0.7.x 11 years ago
sanderjo 565d8bec8a Handle server side 5xx problems 11 years ago
Der-Jan 4bf057b0a9 Set pwd to none when empty 11 years ago
JessThrysoee 081da0a0fc Use default-path in Plush cookies. 11 years ago
shypike 7fe125fd7e Prevent false encryption messages. 11 years ago
shypike 9669580ea7 Prevent false positives for encryption detection. 11 years ago
shypike 27cc3f9be5 Update main POT file. 11 years ago
shypike 57084ab6d1 Additional logging for folder renaming. 11 years ago
shypike 56d0e2957b Update text files for 0.7.18RC1 11 years ago
shypike 08ffaedaca Implement support for X-Failure call-back URL. 11 years ago
shypike 3c3d6dbb4b Merge pull request #160 from sanderjo/0.7.x 11 years ago
sanderjo a66ffc61fd We're now on 0.7.x, and developing 0.8.x 11 years ago
shypike 7e04cb019d Prevent crash in unpacking due to unset variable. 11 years ago
shypike 802c9cfab6 Using priority "Force" will override the duplicate NZB check. 11 years ago
shypike 987a64d3a4 Fix little problem in extension detection and fix some PyLint complaints. 11 years ago
shypike ea90e9e153 Provide UI for detection of unwanted extensions inside RAR archives. 11 years ago
sanderjo ace1ef5498 Implement support to detect unwanted extensions inside RAR archives. 11 years ago
shypike d2926dcbac Prevent pseudo error message when testing "Notification Center". 11 years ago
shypike 5d4f8c2459 Merge branch '0.7.x' of https://github.com/josteink/sabnzbd into 0.7.x 11 years ago
Jostein Kjønigsen e2790cbaa9 Notification: Respect NotifyOSD-preference and allow testing of values from UI. 11 years ago
shypike 1441eea5ae Merge pull request #148 from josteink/0.7.x 11 years ago
Jostein Kjønigsen db4ecfc59c Support testing email based on values in UI instead of stored config. 11 years ago
shypike 4a69dea144 Set new_nzb_on_failure option default off. 11 years ago
shypike 3f790a5048 Update POT file. 11 years ago
shypike 470c1fc13e Install support for X-Failure call-back URL. 11 years ago
shypike 6faeb578a7 Don't trim file names when renaming them (so revert to old behavior). 11 years ago
shypike 44383deb33 Add "pause_pp" to the API. 11 years ago
shypike 80fccd92ec Pause/abort on encryption failed when pre-check was active. 11 years ago
shypike 666e115032 Also remove colons ":" with option sanitize_safe 11 years ago
shypike 048c36c107 Update DMG template again. 11 years ago
shypike de86ba7496 Update text files for 0.7.17Final 11 years ago
shypike a58d070aaf Update OSX template. 11 years ago
shypike 30ed8a2d30 Update OSX DMG template for Mavericks. 11 years ago
shypike c503e3078f Fix error in previous commit (d87162bebd). 11 years ago
shypike 3d74d879a9 Support "retry-after" attribute (for NZBFinder), used for rate limiting of NZB grabs. 11 years ago
shypike ae0268eb3c Update text files for 0.7.17RC2 11 years ago
shypike d6eb1ffbe0 Sanitize names when renaming files and folders. 11 years ago
shypike d88b3a76a5 Prevent race condition in Rating usage from crashing SABnzbd. 11 years ago
shypike a6a68b9b67 Reverse commit 507cbb4d87 11 years ago
shypike 182bce40a7 Update translations 11 years ago
shypike 5072f55d6e Do an extra sanitizing call before a file rename. 11 years ago
shypike dc2d4ffb85 Make RAR/RAR5 detection more robust. 11 years ago
shypike 22f2e0881e Support double quotes in password entry boxes on job detail page. 11 years ago
shypike 614510c00b Prevent embedded password from getting damaged by sanitizing. 11 years ago
shypike ee32bb5419 Extend password boxes on file details page. 11 years ago
shypike 99eb16b504 Provisional RAR5 support. 11 years ago
shypike 2da71a9fa2 Add password fields to File Detail pages of "smpl" and "Classic" skins. 11 years ago
shypike 9cc9e0eaea When checking unrar, prevent creating a zombie process on some systems. 11 years ago
shypike 2d92069a3c Prevent unwanted change of queue order after editing job details. 11 years ago
shypike a0cfc19682 Merge pull request #140 from ecourtenay/0.7.x 11 years ago
Ed Courtenay d87cd33523 Fix trailing slash 11 years ago
shypike 63186c671b Update text files for 0.7.17RC2 11 years ago
shypike 3d3d8f1535 Add Special option ”warn_dupl_jobs” to suppress/enable warning about duplicate jobs. 11 years ago
shypike a90754149b Prevent URLs in the queue from getting ”sanitized”. 11 years ago
shypike ad6a896e46 Support UNC paths in Sort expressions (Windows). 11 years ago
shypike f4e0559894 Remove race-condition in PP-queue exit that prevented shutdown. 11 years ago
shypike e517b67faf Allow "Force" priority to be set in the NZO page (now also for smpl and Classic.) 11 years ago
shypike bbb2b27157 Update text files for 0.7.17RC1 11 years ago
shypike 0b1f9dfe14 Allow "Force" priority to be set in the NZO page. 11 years ago
shypike e7bd7ae0d3 Make sure a manually entered decryption password has no leading spaces. 11 years ago
shypike 8a4579d2ec Allow "Default" category to be selected in Multi-ops. 12 years ago
shypike 0adf5ef93d Fix minor issues in rating system. 11 years ago
shypike fdacbeaa4d Prevent PP queue timeout construction from keeping the CPU awake. 11 years ago
shypike f5ecf3df05 Remove spaces from display of filename/password. 11 years ago
shypike eae644d8f6 Add more logging about password file results. 11 years ago
shypike 406f18d6bb Prevent sanitizing of passwords in "nzbname" argument of API calls. 11 years ago
shypike bf04f950c3 Add Special option "flat_unpack" to remove embedded folders in archives. 11 years ago
shypike e90ff0eeb7 Fix bug in rating system. 11 years ago
shypike 35381555a9 Update text files for 0.7.17Beta3 12 years ago
shypike 4bf956f4ff Upgrade unrar to 5.01 12 years ago
shypike ddfe8653f4 Fix problem with space added to password coming from a file name. 12 years ago
shypike 0066b9f11e Don’t send 8th parameter to user script when empty. 12 years ago
shypike c897b2252d Update text files for 0.7.17Beta2 12 years ago
shypike 2aadcd032d Fix coding errors in OZnzb rating support. 12 years ago
shypike 23e9bf112e Fix problem with running user’s PP script. 12 years ago
shypike 9b577df408 Fix error in ozNZB support. 12 years ago
shypike e6d75b45ae Update text files for 0.7.17Beta1 12 years ago
shypike f68de5df4c Add some basic support for X-DNZB-Failure and X-DNZB-Details headers coming from indexers. 12 years ago
shypike 8bda8efa2f Add provisional support for unrar 5. 12 years ago
shypike 62792f859b Improve scanning of passwords in file names. 12 years ago
shypike 79fa42f90f Update translations 12 years ago
shypike acb3ed2c77 Update translation templates. 12 years ago
shypike 50f411fb0b Add the command line parameter —pidfile to set an explicit PID-file name. 12 years ago
shypike 38c13bc4f0 Merge pull request #127 from oznzb-dev/0.7.x 12 years ago
oznzb-dev 7a7bf0f4e4 Merge branch '0.7.x' of https://github.com/oznzb-dev/sabnzbd into 0.7.x 12 years ago
oznzb-dev 4d1b02fa64 Add backport of OrderedDict to support earlier versions of Python. 12 years ago
oznzb-dev 612e68b5e6 Update three.html 12 years ago
oznzb-dev 9645f947b8 Add modified files related ratings. 12 years ago
oznzb-dev c211969a81 Added support for ratings and integration with OZnzb. 12 years ago
shypike 86916cdf90 Fix another issue with commit 3b3759e81e (NZB-meta data). 12 years ago
shypike fa3e0f941b Always rename files in Sorting, regardless of casing. 12 years ago
shypike 32c524c18d Fix issue with commit 3b3759e81e (NZB-meta data). 12 years ago
shypike 7ba1a4c20f Add Solaris manifest to tar.gz distribution file. 12 years ago
shypike 3b3759e81e Add usage of NZB-meta data and X-headers for Sorting. 12 years ago
shypike ba3aaab3dc Pass extra parameter to OSX Notification Center tool to enable Mavericks support. 12 years ago
shypike 30ec3c430d Show job's ETA when its priority is forced, but queue is paused. 12 years ago
shypike dac568fc35 Another fix for false encryption reports. 12 years ago
shypike 1ee00e12ce Fix crash in API-call "queue-rename" when "value3" is empty or undefined. 12 years ago
shypike 15ae1ae5fd Merge pull request #111 from jim80net/0.7.x 12 years ago
shypike 14f39a21e3 Update text files for 0.7.16 12 years ago
shypike 97ea4ee2eb Special "news_items" wasn't removed completely, leading to UI crash. 12 years ago
Jim80net c68fa9f0c5 Adds solaris manifest 12 years ago
shypike 06576baf5c Update text files for 0.7.15 12 years ago
shypike 7b5fcbe0af For Unix systems, expand wildcards for the par2 tool to prevent problems with some builds of par2cmdline. 12 years ago
shypike 003ee07dee Revert "Use ".admin" instead of "__ADMIN__" as job admin folder to support some non-standard Unix systems." 12 years ago
shypike 654b5e9a24 Remove "news" section in Config skin's main page. 12 years ago
shypike 1b05bc9ed2 Use ".admin" instead of "__ADMIN__" as job admin folder to support some non-standard Unix systems. 12 years ago
shypike dc328c545b Add password entry box to "File Details" page (Plush only). 12 years ago
shypike 823816ddc4 Prevent "special" sub-folders on file servers from being scanned during unpacking. 12 years ago
shypike 8979598f23 Add special option 'sanitize_safe' to remove bad Windows chars on other platforms. 12 years ago
shypike f26bf9b21f Fix false positive encryption alarm for some posts, 12 years ago
shypike 5d3a0cc593 Merge pull request #104 from manandre/rss_guid 12 years ago
manandre 21d445b7a6 Add of GUID field in Queue RSS feed 12 years ago
manandre 9c0df30d34 Add of GUID field in History RSS feed 12 years ago
shypike bc9be3f92b Update text files for 0.7.14 12 years ago
shypike 2dc5c329c9 Fix special case of unjustified encryption warning. 12 years ago
shypike 67817978f4 Missing mini-par2 sometimes prevents the other par2 files from being downloaded. 12 years ago
shypike e2ab8c6ce4 Make sure even invalid RAR files are fed to unrar and handle its reporting. 12 years ago
shypike f33a952536 Update text files for 0.7.13 (again). 12 years ago
shypike cc582b5321 Accept "nzbname" parameter in api-call "add url" even when a ZIP file is retrieved. 12 years ago
shypike bdc526c91b Update text files for 0.7.13 12 years ago
shypike 52039c29b4 Accept partial par2 file when no others are available. 12 years ago
shypike 1dc4175f82 Add "special" option enable_recursion to control recursive unpacking. 12 years ago
shypike 92f70fc177 When post has just one par2-set, use full wildcard so that all files are repair and par candidates. 12 years ago
shypike fd573208bd Fix encryption detection again. 12 years ago
shypike ca9f10c12f Update text files for 0.7.12 12 years ago
shypike 49a72d0902 Update translations 12 years ago
shypike 6aafe3c531 Fix problem in encryption detection. 12 years ago
shypike 9e84696f96 Config and Wizard skins: fix problem with Unicode when using Chrome. 12 years ago
shypike 120c133d7a Implement robots.txt to keep web crawlers out. 12 years ago
shypike cf9713a4b0 Don't try to join a set of just one file (e.g. IMAGE.000) and reduce memory usage when joining large segments. 12 years ago
shypike d12e9889e7 Make encryption detection more careful. 12 years ago
shypike 711a546989 Make name sorting of the queue case-insensitive. 12 years ago
shypike 7f78e6fac1 Save job admin to disk when setting password or changing other attributes. 12 years ago
shypike 72533eefa4 Plush: add "resume pp" entry to pulldown menu, when pause_pp event is scheduled. 12 years ago
shypike d9643d9ea8 Improve RAR detection. 12 years ago
shypike 2de71bb96c Enable "abort if hopeless" for pre-check as well. 12 years ago
  1. 4
      .gitignore
  2. 2
      ABOUT.txt
  3. 161
      CHANGELOG.txt
  4. 3
      COPYRIGHT.txt
  5. 18
      Dockerfile
  6. 4
      INSTALL.txt
  7. 10
      ISSUES.txt
  8. 2
      LICENSE.txt
  9. 4
      PKG-INFO
  10. 2
      README.md
  11. 17
      README.mkd
  12. 57
      SABnzbd.py
  13. 1
      Sample-PostProc.cmd
  14. 1
      Sample-PostProc.sh
  15. 2
      interfaces/Classic/README.TXT
  16. 61
      interfaces/Classic/templates/history.tmpl
  17. 6
      interfaces/Classic/templates/nzo.tmpl
  18. 9
      interfaces/Classic/templates/static/stylesheets/defaultcolors.css
  19. 22
      interfaces/Config/templates/_inc_header_uc.tmpl
  20. 8
      interfaces/Config/templates/config.tmpl
  21. 12
      interfaces/Config/templates/config_notify.tmpl
  22. 162
      interfaces/Config/templates/config_switches.tmpl
  23. 9
      interfaces/Config/templates/staticcfg/css/style.css
  24. 1
      interfaces/Plush/templates/_inc_header.tmpl
  25. 65
      interfaces/Plush/templates/_inc_modals.tmpl
  26. 40
      interfaces/Plush/templates/history.tmpl
  27. 3
      interfaces/Plush/templates/main.tmpl
  28. 4
      interfaces/Plush/templates/nzo.tmpl
  29. 21
      interfaces/Plush/templates/queue.tmpl
  30. 13
      interfaces/Plush/templates/static/javascripts/lib.js
  31. 99
      interfaces/Plush/templates/static/javascripts/plush.js
  32. 74
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/gold.css
  33. BIN
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/sound16.png
  34. BIN
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/thumbdown20.png
  35. BIN
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/thumbup20.png
  36. BIN
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/vision16.png
  37. BIN
      interfaces/Plush/templates/static/stylesheets/rateit/delete.gif
  38. 98
      interfaces/Plush/templates/static/stylesheets/rateit/rateit.css
  39. BIN
      interfaces/Plush/templates/static/stylesheets/rateit/star.gif
  40. 2
      interfaces/smpl/templates/main.tmpl
  41. 6
      interfaces/smpl/templates/nzo.tmpl
  42. 34
      interfaces/wizard/four.html
  43. 1
      interfaces/wizard/inc_top.tmpl
  44. 50
      interfaces/wizard/three.html
  45. 30
      licenses/License-OrderedDict.txt
  46. 3
      licenses/License-PythonParts.txt
  47. 2
      licenses/License-pynewsleecher.txt
  48. 31
      make_dmg.py
  49. BIN
      osx/image/template.sparseimage.zip
  50. BIN
      osx/unrar/unrar
  51. 0
      osx/unrar/unrar-ppc
  52. 26
      package.py
  53. 2
      po/email/SABemail.pot
  54. 6
      po/email/da.po
  55. 6
      po/email/de.po
  56. 6
      po/email/es.po
  57. 212
      po/email/fi.po
  58. 6
      po/email/fr.po
  59. 32
      po/email/nb.po
  60. 32
      po/email/nl.po
  61. 6
      po/email/pl.px
  62. 6
      po/email/pt_BR.po
  63. 6
      po/email/ro.px
  64. 6
      po/email/sv.po
  65. 1888
      po/main/SABnzbd.pot
  66. 1909
      po/main/da.po
  67. 1938
      po/main/de.po
  68. 1899
      po/main/es.po
  69. 4536
      po/main/fi.po
  70. 1915
      po/main/fr.po
  71. 2214
      po/main/nb.po
  72. 1919
      po/main/nl.po
  73. 1915
      po/main/pl.px
  74. 1923
      po/main/pt_BR.po
  75. 1900
      po/main/ro.px
  76. 1915
      po/main/sv.po
  77. 116
      po/nsis/fi.po
  78. 78
      po/nsis/nb.po
  79. 50
      po/nsis/nl.po
  80. 34
      sabnzbd/__init__.py
  81. 105
      sabnzbd/api.py
  82. 10
      sabnzbd/articlecache.py
  83. 105
      sabnzbd/assembler.py
  84. 35
      sabnzbd/cfg.py
  85. 4
      sabnzbd/config.py
  86. 2
      sabnzbd/database.py
  87. 7
      sabnzbd/dirscanner.py
  88. 3
      sabnzbd/downloader.py
  89. 60
      sabnzbd/emailer.py
  90. 5
      sabnzbd/encoding.py
  91. 68
      sabnzbd/growler.py
  92. 87
      sabnzbd/interface.py
  93. 269
      sabnzbd/misc.py
  94. 138
      sabnzbd/newsunpack.py
  95. 34
      sabnzbd/nzbqueue.py
  96. 143
      sabnzbd/nzbstuff.py
  97. 3
      sabnzbd/osxmenu.py
  98. 66
      sabnzbd/postproc.py
  99. 285
      sabnzbd/rating.py
  100. 12
      sabnzbd/rss.py

4
.gitignore

@ -26,3 +26,7 @@ SABnzbd*.dmg
*.keep
*.bak
*.log
# Some people use Emacs as an editor
\#*
.\#*

2
ABOUT.txt

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

161
CHANGELOG.txt

@ -1,4 +1,165 @@
-------------------------------------------------------------------------------
0.7.20 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Make sure that Rating support is suppressed completely when disabled.
-------------------------------------------------------------------------------
0.7.20RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Update unrar for (Snow)Leopard on Intel to 5.11
- Logging of the Pystone performance of the system
-------------------------------------------------------------------------------
0.7.20RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- OSX unrar now really updated to 5.11
- API call "Retry" now returns new job id
- Support for NZB meta field 'x-rating-host'
- Fix email test issue
- Support of OSX Yosemite "Dark Mode"
-------------------------------------------------------------------------------
0.7.19 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Support double quotes to delineate parameters in category match lists.
- When a comma is present in a file name, quotes are needed when passed to a user script
- The after-unrar-check needs to take the "flat_unpack" option into account
- When sanitizing names, preserve "." and ".." elements in paths
-------------------------------------------------------------------------------
0.7.19RC4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix damaging of job's destination path on Windows
-------------------------------------------------------------------------------
0.7.19RC3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Improve password trial when the system uses an older unrar tool
-------------------------------------------------------------------------------
0.7.19RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Make matching of SFV file against RAR-sets case-insensitive
- Make sure final destination path is always sanitized and trimmed
- Improve "check for unwanted extensions"
- Upgrade unrar to version 5.11 (OSX and Windows)
- Limit article cache to 1G to prevent a memory size bug in the _yenc module
- Fix a number of problems with embedded passwords and folder size trimming
- Allow "float" timestamps in RSS feeds
- Expose 'rating_host' to Config->Special
- Add Finnish translation
-------------------------------------------------------------------------------
0.7.19RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- More oznzb filtering
- Fix OSX notification center problem
- Fix sort order of RSS feeds
- Prevent multiple pauses in "unwanted extensions" option
- Change renaming scheme for duplicate files
- Fix sorting of the queue
-------------------------------------------------------------------------------
0.7.18 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Update translations
-------------------------------------------------------------------------------
0.7.18RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Support for X-Failure header
- Support for detecting unwanted extensions inside RAR files
- Using priority Force will override duplicate detection
- Notification: Respect NotifyOSD-preference and allow testing of values from UI
- Prevent pseudo error message when testing "Notification Center"
- Testing email based on values in UI instead of stored config
- Don't trim file names when renaming them (so revert to old behavior)
- Add "pause_pp" to the API
- Pause/abort on encryption failed when pre-check was active
- Also remove colons ":" with option sanitize_safe
- Update DMG template
- Fix potential crash when unpacking due to unset variable
- Fix problem of cookie interference with other apps
- Add API function server_stats
- Support password embedding in file detail page and AddNZB dialog
- Pause/Remove posts when unwanted extensions are detected (like .exe)
- Fix issue with some RAR file sets leading to Windows "87" error
- Handle 5xx RSS feed error messages
-------------------------------------------------------------------------------
0.7.17Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Implement "retry-after" header to support rate-limiting
- Update OSX image to show Mavericks support
-------------------------------------------------------------------------------
0.7.17RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Support UNC paths in Sort expressions (Windows)
- URL in the queue should not show up "sanitized"
- Fix shutdown issue in PP queue
- Allow "Force" to be set as priority in files overview
- Special option "warn_dupl_jobs" to suppress/enable warnings for duplicate jobs
- Fix problem with "sanitize" in "renamer"
- Add (partial) RAR5 support
- Fix some more password-in-filename issues
- Prevent unwanted change of queue order after editing job details
- Add password entry boxes in smpl and Classic skins
- Prevent unrar zombies on some systems
-------------------------------------------------------------------------------
0.7.17RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix bug in rating system
- Fix multiple encryption password issues
- Allow Default category to be picked in Multi-Ops
- Allow "force" prio to be picked in NZO page
- Prevent PP queue timeout construction from keeping the CPU awake.
- Special option "flat_unpack"
-------------------------------------------------------------------------------
0.7.17Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix regression errors in Beta1
-------------------------------------------------------------------------------
0.7.17Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Add command line option --pidfile
- Another fix for false encryption reports
- Fix issue with OSX Mavericks Notification Center
- Add support for 'x-dnzb-propername', 'x-dnzb-episodename', 'x-dnzb-year'
in meta-data of NZB. To be used in TV Sorting
- Add OZnzb features need to be enabled in config ->switches
- Add integration with OZnzb indexer enhanced functionality, allows user access to ratings and reporting directly from SABnzbd interface.
- Add automatic feedback to OZnzb on failed downloads (if enabled)
- Add X-DNZB-Failure and X-DNZB-Details support
- Fix issue with passwords embedded in file names
- Updated translations
-------------------------------------------------------------------------------
0.7.16Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix Config->Special UI crash
-------------------------------------------------------------------------------
0.7.15Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix false encryption alarms for some posts
- Add "password" dialog to Plush's job details page
- Add special "sanitize_safe" to remove bad Windows characters on other platforms
- Remove "news" section from Config skin
- Fix for faulty par2cmdline on some embbeded Unix systems
- Add GUID fields to the History RSS feed.
-------------------------------------------------------------------------------
0.7.14Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Another encryption detection fix (special case)
- Missing mini-par2 sometimes prevents the other par2 files from being downloaded.
- Make sure even invalid RAR files are fed to unrar and handle its reporting.
-------------------------------------------------------------------------------
0.7.13Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Another encryption detection fix
- Special option "enable_recursion" to control recursive unpacking
- When post has just one par2 set, use wildcard so that all files are used
- Accept partial par2 file when only one is available
- Accept "nzbname" parameter in api-call "add url" even when a ZIP file is retrieved.
-------------------------------------------------------------------------------
0.7.12Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix issue in encryption detection
- Don't try to "join" a single X.000 file
- Fix memory overflow caused by very large files to be joined
- Make name sorting of the queue case-insensitive
- Save data to disk after changing job password or other attributes
- Add "resume_pp" entry to Plush pull-down menu when pause_pp event is scheduled
- Deploy "abort when completion not possible" method also in pre-download check
-------------------------------------------------------------------------------
0.7.11Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Bad articles from some servers were accepted as valid data

3
COPYRIGHT.txt

@ -1,5 +1,5 @@
(c) Copyright 2007-2013 by "The SABnzbd-team" <team@sabnzbd.org>
(c) Copyright 2007-2014 by "The SABnzbd-team" <team@sabnzbd.org>
The SABnzbd-team is:
@ -7,6 +7,7 @@ Active team:
ShyPike <shypike@sabnzbd.org>
inpheaux <inpheaux@sabnzbd.org>
zoggy <zoggy@sabnzbd.org>
OZnzb-dev <sabdev@oznzb.com>
Sleeping members
sw1tch <switch@sabnzbd.org>
pairofdimes <pairofdimes@sabnzbd.org>

18
Dockerfile

@ -0,0 +1,18 @@
FROM ubuntu:14.04
MAINTAINER Johannes 'fish' Ziemke <docker@freigeist.org> @discordianfish
RUN echo deb http://archive.ubuntu.com/ubuntu/ trusty multiverse >> \
/etc/apt/sources.list
RUN apt-get -qy update && apt-get -qy install python python-cheetah unrar \
unzip python-yenc par2
RUN useradd sabnzbd -d /sab -m && chown -R sabnzbd:sabnzbd /sab
VOLUME /sab
ADD . /sabnzbd
EXPOSE 8080
USER sabnzbd
ENV HOME /sab
ENTRYPOINT [ "python", "/sabnzbd/SABnzbd.py", "-s", "0.0.0.0:8080" ]

4
INSTALL.txt

@ -1,10 +1,10 @@
SABnzbd 0.7.11
SABnzbd 0.7.20
-------------------------------------------------------------------------------
0) LICENSE
-------------------------------------------------------------------------------
(c) Copyright 2007-2013 by "The SABnzbd-team" <team@sabnzbd.org>
(c) Copyright 2007-2014 by "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

10
ISSUES.txt

@ -59,14 +59,14 @@
prevents the removal.
- Memory usage can sometimes have high peaks. This makes using SABnzbd on very low
memory systems (eg a SAN device) a challenge.
memory systems (e.g. a NAS device or a router) a challenge.
- SABnzbd is not compatible with some software firewall versions.
The Mircosoft Windows Firewall works fine, but remember to tell this
firewall that SABnzbd is allowed to talk to other computers.
- When SABnzbd cannot send nofication emails, check your virus scanner,
firewall or securiry suite. It may be blocking outgoing email.
firewall or security suite. It may be blocking outgoing email.
- When you are using external drives or network shares on OSX or Linux
make sure that the drives are mounted.
@ -75,6 +75,12 @@
On OSX, SABnzbd will not create new folders in /Volumes.
The result will be a failed job that can be retried once the volume has been mounted.
- If you use a mounted drive as "temporary download folder", it must be present when SABnzbd
starts up. If not, SABnzbd will use the default location.
You can make SABnzbd wait for a mount of the "temporary download folder" by setting
Config->Special->wait_for_dfolder to 1.
SABnzbd will appear to hang until the drive is mounted.
- On some operating systems it looks like there is a problem with one of the standard Python libraries.
It is possible that you get errors about saving admin files and even unexplained crashes.
If so, you can enable the option for the alternative library.

2
LICENSE.txt

@ -1,4 +1,4 @@
(c) Copyright 2007-2013 by "The SABnzbd-team" <team@sabnzbd.org>
(c) Copyright 2007-2014 by "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

4
PKG-INFO

@ -1,7 +1,7 @@
Metadata-Version: 1.0
Name: SABnzbd
Version: 0.7.11
Summary: SABnzbd-0.7.11
Version: 0.7.20
Summary: SABnzbd-0.7.20
Home-page: http://sabnzbd.org
Author: The SABnzbd Team
Author-email: team@sabnzbd.org

2
README.md

@ -51,4 +51,4 @@ Our many other commandline options are explained in depth [here](http://wiki.sab
## About Our Repo
We're going to be attempting to follow the [gitflow model](http://nvie.com/posts/a-successful-git-branching-model/), so you can consider "master" to be whatever our present stable release build is (presently 0.6.x) and "develop" to be whatever our next build will be (presently 0.7.x). Once we transition from unstable to stable dev builds we'll create release branches, and encourage you to follow along and help us test.
We're going to be attempting to follow the [gitflow model](http://nvie.com/posts/a-successful-git-branching-model/), so you can consider "master" to be whatever our present stable release build is (presently 0.7.x) and "develop" to be whatever our next build will be (presently 0.8.x). Once we transition from unstable to stable dev builds we'll create release branches, and encourage you to follow along and help us test.

17
README.mkd

@ -1,11 +1,18 @@
Release Notes - SABnzbd 0.7.11
Release Notes - SABnzbd 0.7.20
================================
## Features
- Support of OSX Yosemite "Dark Mode"
- API call "Retry" now returns new job id (supporting nzbdrone)
## Bug fixes
- Obfuscated file name support causes regular multi-set NZBs to verify (much) slower
- Bad articles from some servers are accepted as valid data
- Generic Sort fails to rename files when an extra folder level is present in the RAR files
- OSX unrar now really updated to 5.11 for Lion and higher
- unrar is now updated to 5.11 for Intel systems running (Snow)Leopard
- (Snow)Leopard on PPC still only has unrar 4.01, no new versions from rarlabs
- Fix email test issue
## What's new in 0.7.0
@ -35,7 +42,7 @@ Release Notes - SABnzbd 0.7.11
built-in post-processing options that automatically verify, repair,
extract and clean up posts downloaded from Usenet.
(c) Copyright 2007-2013 by "The SABnzbd-team" \<team@sabnzbd.org\>
(c) Copyright 2007-2014 by "The SABnzbd-team" \<team@sabnzbd.org\>
### IMPORTANT INFORMATION about release 0.7.x

57
SABnzbd.py

@ -1,5 +1,5 @@
#!/usr/bin/python -OO
# Copyright 2008-2013 The SABnzbd-Team <team@sabnzbd.org>
# Copyright 2008-2014 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
@ -240,7 +240,8 @@ def print_help():
print " -d --daemon Use when run as a service"
else:
print " -d --daemon Fork daemon process"
print " --pid <path> Create a PID file in the listed folder (full path)"
print " --pid <path> Create a PID file in the given folder (full path)"
print " --pidfile <path> Create a PID file with the given name (full path)"
print
print " --force Discard web-port timeout (see Wiki!)"
print " -h --help Print this message"
@ -260,7 +261,7 @@ def print_version():
print """
%s-%s
Copyright (C) 2008-2013, The SABnzbd-Team <team@sabnzbd.org>
Copyright (C) 2008-2014, The SABnzbd-Team <team@sabnzbd.org>
SABnzbd comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions. It is licensed under the
@ -843,7 +844,7 @@ def commandline_handler(frozen=True):
'weblogging=', 'server=', 'templates', 'no_ipv6',
'template2', 'browser=', 'config-file=', 'force',
'version', 'https=', 'autorestarted', 'repair', 'repair-all',
'log-all', 'no-login', 'pid=', 'new', 'sessions', 'console',
'log-all', 'no-login', 'pid=', 'new', 'sessions', 'console', 'pidfile=',
# Below Win32 Service options
'password=', 'username=', 'startup=', 'perfmonini=', 'perfmondll=',
'interactive', 'wait=',
@ -915,6 +916,7 @@ def main():
no_login = False
re_argv = [sys.argv[0]]
pid_path = None
pid_file = None
new_instance = False
force_sessions = False
osx_console = False
@ -997,6 +999,10 @@ def main():
pid_path = arg
re_argv.append(opt)
re_argv.append(arg)
elif opt in ('--pidfile',):
pid_file = arg
re_argv.append(opt)
re_argv.append(arg)
elif opt in ('--new',):
new_instance = True
elif opt in ('--sessions',):
@ -1270,6 +1276,43 @@ def main():
logging.info('Python-version = %s', sys.version)
logging.info('Arguments = %s', sabnzbd.CMDLINE)
if sabnzbd.cfg.log_level() > 1:
try:
s_ipv4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s_ipv4.connect(('google.com', 80))
logging.debug('My IPv4 address = %s', s_ipv4.getsockname()[0])
s_ipv4.close()
except:
logging.debug('Could not determine my IPv4 address')
pass
try:
s_ipv6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s_ipv6.connect(('ipv6.google.com', 80))
logging.debug('My IPv6 address = %s', s_ipv6.getsockname()[0])
s_ipv6.close()
except:
logging.debug('Could not determine my IPv6 address')
pass
# measure and log Pystone performance, and - if possible - CPU type
try:
# First try pystone from Python test libary
from test.pystone import pystones
except:
# otherwise use the one provided by SABnzbd
from util.pystone import pystones
pystonetime,pystoneperformance = pystones(1000)
logging.debug('CPU Pystone available performance is %s',int(pystoneperformance))
try:
for myline in open("/proc/cpuinfo"):
if myline.startswith(('model name')):
logging.debug('CPU model name is %s', myline[13:].rstrip() )
break
except:
# probably not on Linux
pass
# OSX 10.5 I/O priority setting
if sabnzbd.DARWIN:
logging.info('[osx] IO priority setting')
@ -1318,6 +1361,8 @@ def main():
sabnzbd.WEB_COLOR2 = CheckColor(sabnzbd.cfg.web_color2(), web_dir2)
sabnzbd.cfg.web_color2.set(sabnzbd.WEB_COLOR2)
logging.debug('Unwanted extensions are ... %s',sabnzbd.cfg.unwanted_extensions())
if fork and not sabnzbd.WIN32:
daemonize()
@ -1545,8 +1590,8 @@ def main():
# Write URL directly to registry
set_connection_info(api_url)
if pid_path:
sabnzbd.pid_file(pid_path, cherryport)
if pid_path or pid_file:
sabnzbd.pid_file(pid_path, pid_file, cherryport)
# Start all SABnzbd tasks
logging.info('Starting %s-%s', sabnzbd.MY_NAME, sabnzbd.__version__)

1
Sample-PostProc.cmd

@ -11,6 +11,7 @@ echo The fourth parameter (newzbin #) = %4
echo The fifth parameter (category) = %5
echo The sixth parameter (group) = %6
echo The seventh parameter (status) = %7
echo The eigth parameter (failure_url)= %8
echo.

1
Sample-PostProc.sh

@ -11,6 +11,7 @@ echo "The fourth parameter (newzbin-id) =" "$4"
echo "The fifth parameter (category) =" "$5"
echo "The sixth parameter (group) =" "$6"
echo "The seventh parameter (status) =" "$7"
echo "The eigth parameter (failure_url) =" "$8"
echo

2
interfaces/Classic/README.TXT

@ -1,5 +1,5 @@
#
# Copyright 2008-2013 The SABnzbd-Team <team@sabnzbd.org>
# Copyright 2008-2014 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

61
interfaces/Classic/templates/history.tmpl

@ -31,7 +31,15 @@ $T('thisWeek'): $week_size&nbsp;&nbsp;|&nbsp;&nbsp;$T('thisMonth'): $month_size
<% from sabnzbd.misc import time_format %>
<!--#if $lines#-->
<table id="historyTable">
<tr><th></th><th>$T('completed')</th><th>$T('name')</th><th>$T('size')</th><th>$T('status')</th><th></th></tr>
<tr>
<th></th>
<th>$T('completed')</th>
<th>$T('name')</th>
<th>$T('size')</th>
<th>$T('status')</th>
<!--#if $rating_enable#--><th>Rating</th><!--#end if#-->
<th></th>
</tr>
<!--#set $odd = False#-->
<!--#for $line in $lines #-->
<%
@ -44,7 +52,20 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_
</a></td>
<td>$compl</td>
<td>$line.name<!--#if $line.action_line#--> - $line.action_line<!--#else if $line.fail_message#--> - <span class="fail_message">$line.fail_message</span><!--#end if#--></td>
<td>$line.size</td><td>$Tx('post-'+$line.status)</td>
<td>$line.size</td>
<td>$Tx('post-'+$line.status)</td>
<!--#if $rating_enable#-->
<!--#if $line.has_rating#-->
<td><div class="rating_overall">$T('video')&nbsp;$line.rating_avg_video $T('audio')&nbsp;$line.rating_avg_audio</div>
<form method="GET" action="./show_edit_rating">
<input type="hidden" name="job" value="$line.nzo_id">
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('report')">
</form></td>
<!--#else#-->
<td></td>
<!--#end if#-->
<!--#end if#-->
<td>
<!--#if not $line.loaded#-->
<!--#if $line.retry#-->
@ -66,6 +87,41 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_
<!--#end if#-->
</td>
</tr>
<!--#if $line.edit_rating#-->
<!--#set $oddLine = not False#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#-->"><td></td><td></td>
<td colspan="3">
<form action="action_edit_rating" method="post" enctype="multipart/form-data">
<input type="hidden" value="$line.nzo_id" name="job">
<input type="hidden" value="$session" name="session" >
<div class="rating_item">$T('video')&nbsp;
<select name="video">
<!--#if not $line.rating_user_video#--><option>-</option><!--#end if#-->
<!--#for $val in $range(1, 11)#--><option <!--#if $line.rating_user_video==$val#-->selected<!--#end if#--> >$val</option><!--#end for#-->
</select>
</div>
<div class="rating_item">$T('audio')&nbsp;
<select name="audio">
<!--#if not $line.rating_user_audio#--><option>-</option><!--#end if#-->
<!--#for $val in $range(1, 11)#--><option <!--#if $line.rating_user_audio==$val#-->selected<!--#end if#--> >$val</option><!--#end for#-->
</select>
</div>
<div class="rating_item">
<input type="radio" name="rating_flag" value="spam">&nbsp;$T('spam')
<input type="radio" name="rating_flag" value="encrypted">&nbsp;$T('encrypted')
<input type="radio" name="rating_flag" value="expired">&nbsp;$T('expired')
<input type="text" name="expired_host" style="margin-left:10px" value="<$T('host')>">
</div>
<div class="rating_item">
<input type="submit" name="send" value="$T('send')">
<input type="submit" name="cancel" value="$T('cancel')">
</div>
</form>
</td>
<td></td>
<td></td>
</tr>
<!--#end if#-->
<!--#if $line.show_details#-->
<!--#set $oddLine = not False#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#-->"><td></td><td></td>
@ -91,6 +147,7 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_
</dl>
</td>
<td></td>
<!--#if $rating_enable#--><td></td><!--#end if#-->
</tr>
<!--#end if#-->
<!--#end for#-->

6
interfaces/Classic/templates/nzo.tmpl

@ -73,7 +73,10 @@
<h3>$T('nzoDetails')</h3>
<form action="save" method="post">
<label class="label">$T('nzoName'):</label><br />
<input type="text" name="name" style="width:400px" size="80" value="$slot.filename">
<input type="text" name="name" style="width:400px" size="80" value="$slot.filename_clean">
<br />
<label class="label">$T('srv-password'):</label><br />
<input type="text" name="password" style="width:200px" size="100" value="$slot.password">
<br />
<label class="label">$T('pp'):</label><br />
<select name="pp">
@ -88,6 +91,7 @@
<label class="label">$T('priority'):</label><br />
<select name="priority">
<option value="-100" <!--#if $slot.priority == "-100" then "selected" else ""#-->>$T('default')</option>
<option value="2" <!--#if $slot.priority == "2" then "selected" else ""#-->>$T('pr-force')</option>
<option value="1" <!--#if $slot.priority == "1" then "selected" else ""#-->>$T('pr-high')</option>
<option value="0" <!--#if $slot.priority == "0" then "selected" else ""#-->>$T('pr-normal')</option>
<option value="-1" <!--#if $slot.priority == "-1" then "selected" else ""#-->>$T('pr-low')</option>

9
interfaces/Classic/templates/static/stylesheets/defaultcolors.css

@ -136,3 +136,12 @@ color:black;
.feedEnabled{color:green;}
.feedDisabled{color:red;}
.rating_overall {
margin:0px 5px 3px 0px;
}
.rating_item {
float:left;
margin:5px 15px 5px 5px;
}

22
interfaces/Config/templates/_inc_header_uc.tmpl

@ -1,7 +1,7 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SABnzbd $version - $T('queued'): $mbleft $T('MB')</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -99,6 +99,26 @@
});
});
/*
* takes the inputs-elements found in the current selector
* and extracts the values into the provided data-object.
*/
\$.fn.extractFormDataTo = function(target)
{
var inputs = \$("input[type != 'submit'][type != 'button']", this);
// could use .serializeArray() but that omits unchecked items
inputs.each(function (i,elem) {
if (elem.type == "checkbox") {
target[elem.name] = elem.checked;
} else {
target[elem.name] = elem.value;
}
});
return this;
}
</script>

8
interfaces/Config/templates/config.tmpl

@ -17,18 +17,14 @@
<tr class="alt"><td class="infoTableHeader">$T('menu-forums') </td><td class="infoTableCell"><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td></tr>
<tr><td class="infoTableHeader">$T('source') </td><td class="infoTableCell"><a href="https://github.com/sabnzbd/sabnzbd" target="_blank">https://github.com/sabnzbd/sabnzbd</a></td></tr>
<tr class="alt"><td class="infoTableHeader">$T('menu-irc') </td><td class="infoTableCell"><a href="irc://irc.synirc.net/#sabnzbd"><i>#sabnzbd</i> on <i>irc.synirc.net</i></a> $T('or') (<a href="http://sabnzbd.org/live-chat/" target="_blank">webchat</a>)</td></tr>
<tr><td class="infoTableHeader">$T('oznzb')</td><td class="infoTableCell"><a href="https://www.oznzb.com/register" target="_blank">https://www.oznzb.com/register</a></td></tr>
</tbody>
</table>
</div>
<div class="padding alt">
<h5 class="copyright">Copyright &copy; 2008-2013 The SABnzbd Team &lt;<span style="color: #0000ff;">team@sabnzbd.org</span>&gt;</h5>
<h5 class="copyright">Copyright &copy; 2008-2014 The SABnzbd Team &lt;<span style="color: #0000ff;">team@sabnzbd.org</span>&gt;</h5>
<p class="copyright"><small>$T('yourRights')</small></p>
</div>
<!--#if $news_items#-->
<div class="padding">
<iframe frameborder=0 width=100% src="http://sabnzbdplus.sourceforge.net/version/news.html"></iframe>
</div>
<!--#end if#-->
</div>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

12
interfaces/Config/templates/config_notify.tmpl

@ -40,7 +40,7 @@
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="section" id="email">
<div class="col2">
<h3>$T('emailAccount')</h3>
</div><!-- /col2 -->
@ -79,7 +79,7 @@
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="section" id="growl">
<div class="col2">
<h3>$T('growlSettings')</h3>
</div><!-- /col2 -->
@ -139,10 +139,12 @@
\$('#email_dir').fileBrowser({ title: 'Select $T('opt-email_dir')' });
\$('#test_email').click(function () {
if (confirm(\$('#test_email').attr('rel'))) {
var data = { mode: 'test_email', apikey: '$session', output: 'json' };
\$("#email").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: {mode: 'test_email', apikey: '$session', output: 'json' },
data: data,
beforeSend: function () {
\$('#test_email').attr("disabled", "disabled");
\$('#testmail-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
@ -162,10 +164,12 @@
}
});
\$('#test_notification').click(function () {
var data = { mode: 'test_notif', apikey: '$session', output: 'json' };
\$("#growl").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: {mode: 'test_notif', apikey: '$session', output: 'json' },
data: data,
beforeSend: function () {
\$('#test_notification').attr("disabled", "disabled");
\$('#testnotice-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');

162
interfaces/Config/templates/config_switches.tmpl

@ -117,6 +117,20 @@
<span class="desc">$T('explain-pause_on_pwrar')</span>
</div>
<div class="field-pair alt">
<label class="config" for="action_on_unwanted_extensions">$T('opt-action_on_unwanted_extensions')</label>
<select name="action_on_unwanted_extensions" id="action_on_unwanted_extensions">
<option value="0" <!--#if int($action_on_unwanted_extensions) == 0 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($action_on_unwanted_extensions) == 1 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="2" <!--#if int($action_on_unwanted_extensions) == 2 then 'selected="selected" class="selected"' else ""#--> >$T('abort')</option>
</select>
<span class="desc">$T('explain-action_on_unwanted_extensions')</span>
</div>
<div class="field-pair">
<label class="config" for="unwanted_extensions">$T('opt-unwanted_extensions')</label>
<input type="text" name="unwanted_extensions" id="unwanted_extensions" value="$unwanted_extensions" size="50"/>
<span class="desc">$T('explain-unwanted_extensions')</span>
</div>
<div class="field-pair alt">
<label class="config" for="top_only">$T('opt-top_only')</label>
<input type="checkbox" name="top_only" id="top_only" value="1" <!--#if int($top_only) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-top_only')</span>
@ -305,6 +319,134 @@
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-indexing')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="rating_enable">$T('opt-rating_enable')</label>
<input type="checkbox" name="rating_enable" id="rating_enable" value="1" <!--#if int($rating_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_enable')</span>
</div>
<div class="field-pair alt">
<label class="config" for="rating_api_key">$T('opt-rating_api_key')</label>
<input type="text" name="rating_api_key" id="rating_api_key" value="$rating_api_key" size="35" />
<span class="desc">$T('explain-rating_api_key')</span>
</div>
<div class="field-pair">
<label class="config" for="rating_feedback">$T('opt-rating_feedback')</label>
<input type="checkbox" name="rating_feedback" id="rating_feedback" value="1" <!--#if int($rating_feedback) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_feedback')</span>
</div>
<div class="field-pair alt">
<label class="config" for="rating_filter_enable">$T('opt-rating_filter_enable')</label>
<input type="checkbox" name="rating_filter_enable" id="rating_filter_enable" value="1" <!--#if int($rating_filter_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_filter_enable')</span>
</div>
<div class="field-pair" id="rating_filter_abort">
<label class="config" for="rating_filter_abort">$T('opt-rating_filter_abort_if')</label>
<div class="rating-filter">
<p>
<label for="rating_filter_abort_video">$T('opt-rating_filter_video')</label>
<select name="rating_filter_abort_video" id="rating_filter_abort_video">
<option value="0" <!--#if $rating_filter_abort_video == 0 then 'selected="selected" class="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_abort_video == $val then 'selected="selected" class="selected"' else ""#--> >$val $T('orLess')</option><!--#end for#-->
</select>
</p>
<p>
<label for="rating_filter_abort_audio">$T('opt-rating_filter_audio')</label>
<select name="rating_filter_abort_audio" id="rating_filter_abort_audio">
<option value="0" <!--#if $rating_filter_abort_audio == 0 then 'selected="selected" class="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_abort_audio == $val then 'selected="selected" class="selected"' else ""#--> >$val $T('orLess')</option><!--#end for#-->
</select>
</p>
<p>
<span>
<input type="checkbox" value="1" id="rating_filter_abort_encrypted" name="rating_filter_abort_encrypted" <!--#if int($rating_filter_abort_encrypted) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_encrypted">$T('opt-rating_filter_passworded')</label>
</span>
<span>
<input type="checkbox" value="1" id="rating_filter_abort_encrypted_confirm" name="rating_filter_abort_encrypted_confirm" <!--#if int($rating_filter_abort_encrypted_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_encrypted_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
<p>
<span>
<input type="checkbox" value="1" id="rating_filter_abort_spam" name="rating_filter_abort_spam" <!--#if int($rating_filter_abort_spam) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_spam">$T('opt-rating_filter_spam')</label>
</span>
<span>
<input type="checkbox" value="1" id="rating_filter_abort_spam_confirm" name="rating_filter_abort_spam_confirm" <!--#if int($rating_filter_abort_spam_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_spam_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
<p>
<input type="checkbox" value="1" id="rating_filter_abort_downvoted" name="rating_filter_abort_downvoted" <!--#if int($rating_filter_abort_downvoted) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_downvoted">$T('opt-rating_filter_downvoted')</label>
</p>
<p>
<label for="rating_filter_abort_keywords">$T('opt-rating_filter_keywords')</label>
<input type="text" name="rating_filter_abort_keywords" id="rating_filter_abort_keywords" value="$rating_filter_abort_keywords" size="35"/>
<span class="desc">$T('explain-rating_filter_keywords')</span>
</p>
</div>
</div>
<div class="field-pair alt" id="rating_filter_pause">
<label class="config" for="rating_filter_pause">$T('opt-rating_filter_pause_if')</label>
<div class="rating-filter">
<p>
<label for="rating_filter_pause_video">$T('opt-rating_filter_video')</label>
<select name="rating_filter_pause_video" id="rating_filter_pause_video">
<option value="0" <!--#if $rating_filter_pause_video == 0 then 'selected="selected" class="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_pause_video == $val then 'selected="selected" class="selected"' else ""#--> >$val $T('orLess')</option><!--#end for#-->
</select>
</p>
<p>
<label for="rating_filter_pause_audio">$T('opt-rating_filter_audio')</label>
<select name="rating_filter_pause_audio" id="rating_filter_pause_audio">
<option value="0" <!--#if $rating_filter_pause_audio == 0 then 'selected="selected" class="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_pause_audio == $val then 'selected="selected" class="selected"' else ""#--> >$val $T('orLess') </option><!--#end for#-->
</select>
</p>
<p>
<span>
<input type="checkbox" value="1" id="rating_filter_pause_encrypted" name="rating_filter_pause_encrypted" <!--#if int($rating_filter_pause_encrypted) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_encrypted">$T('opt-rating_filter_passworded')</label>
</span>
<span>
<input type="checkbox" value="1" id="rating_filter_pause_encrypted_confirm" name="rating_filter_pause_encrypted_confirm" <!--#if int($rating_filter_pause_encrypted_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_encrypted_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
<p>
<span>
<input type="checkbox" value="1" id="rating_filter_pause_spam" name="rating_filter_pause_spam" <!--#if int($rating_filter_pause_spam) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_spam">$T('opt-rating_filter_spam')</label>
</span>
<span>
<input type="checkbox" value="1" id="rating_filter_pause_spam_confirm" name="rating_filter_pause_spam_confirm" <!--#if int($rating_filter_pause_spam_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_spam_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
<p>
<input type="checkbox" value="1" id="rating_filter_pause_downvoted" name="rating_filter_pause_downvoted" <!--#if int($rating_filter_pause_downvoted) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_downvoted">$T('opt-rating_filter_downvoted')</label>
</p>
<p>
<label for="rating_filter_pause_keywords">$T('opt-rating_filter_keywords')</label>
<input type="text" name="rating_filter_pause_keywords" id="rating_filter_pause_keywords" value="$rating_filter_pause_keywords" size="35"/>
<span class="desc">$T('explain-rating_filter_keywords')</span>
</p>
</div>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="padding alt">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
@ -312,4 +454,24 @@
</form>
</div><!-- /colmask -->
<script>
\$(document).ready(function() {
if (!\$('#rating_filter_enable').is(":checked")) {
\$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide();
}
\$('#rating_filter_enable').change(function () {
if (\$(this).is(":checked")) {
\$("#rating_filter_abort").show();
\$("#rating_filter_pause").show();
}
else {
\$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide();
}
});
});
</script>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

9
interfaces/Config/templates/staticcfg/css/style.css

@ -13,3 +13,12 @@
.checkbox-days p{margin: 0 0 5px 0}
.checkbox-days input {vertical-align:middle; margin-top: -1px;}
.checkbox-days label{padding-left: 20px}
.rating-filter {float: left}
.rating-filter p {margin: 0 0 5px 0}
.rating-filter select {vertical-align:middle}
.rating-filter input {vertical-align:middle; margin-top:-1px}
.rating-filter label {display:inline-block; padding-left:0px; width:100px}
.rating-filter input[type="checkbox"] {display:inline}
.rating-filter input[type="checkbox"] + label {padding-left:20px; width:auto}
.rating-filter p > span:first-child {float:left; width:130px}
.rating-filter .desc {display:block; margin:0px; padding-left:103px}

1
interfaces/Plush/templates/_inc_header.tmpl

@ -14,6 +14,7 @@
<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="${path}rss?mode=history"/>
<link rel="stylesheet" type="text/css" href="${path}static/stylesheets/jqueryui/overcast/jquery-ui-1.8.15.custom.css?$version"/>
<link rel="stylesheet" type="text/css" href="${path}static/stylesheets/rateit/rateit.css"/>
#if $color_scheme#
<link rel="shortcut icon" type="image/ico" href="${path}static/stylesheets/colorschemes/$color_scheme/images/sabnzbdplus.ico"/>
<link rel="stylesheet" type="text/css" href="${path}static/stylesheets/colorschemes/$color_scheme/${color_scheme}.css?$version"/>

65
interfaces/Plush/templates/_inc_modals.tmpl

@ -1,3 +1,21 @@
<script type="text/javascript">
function expired_host_changed(self) {
var host = document.getElementsByName('expired_host')[0];
host.value = self.value;
host.readOnly = self.value.length > 0;
}
function flag_modal_submit(self) {
var radios = document.getElementsByName('rating_flag');
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
document.getElementById('noopt').setAttribute('style', 'display:none;size:1');
document.getElementById('submitbtn').click();
return;
}
}
document.getElementById('noopt').removeAttribute('style');
}
</script>
<!-- modals -->
<div style='display:none'>
@ -10,7 +28,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-2013, The SABnzbd Team &lt;team@sabnzbd.org&gt;</small></p>
<p><small>Copyright (C) 2008-2014, The SABnzbd Team &lt;team@sabnzbd.org&gt;</small></p>
<p><small>$T('yourRights')</small></p>
</div>
@ -142,6 +160,51 @@ $T('Plush-containerWidth'):
<input type="submit" id="delete_nzb_modal_remove_files" value="$T('removeNZB-Files')" class="juiButton" />
</div>
<div id="flag_modal">
<input type="hidden" id="flag_modal_job" />
<div class="rating_flag_radio"><input type="radio" name="rating_flag" value="spam">&nbsp;$T('spam')</div>
<div class="rating_flag_radio"><input type="radio" name="rating_flag" value="encrypted">&nbsp;$T('encrypted')</div>
<div class="rating_flag_radio">
<input type="radio" name="rating_flag" value="expired">&nbsp;$T('expired')
<div class="rating_modal_extra">
<div class="rating_modal_expired">$T('host')&nbsp;&nbsp;<input type="text" name="expired_host" value="www.altopia.com" readonly></div>
<select name="common_host" onchange="expired_host_changed(this)">
<option value='www.altopia.com' selected>Altopia</option>
<option value='www.astraweb.com'>Astraweb</option>
<option value='www.euroaccess.ln'>EuroAccess</option>
<option value='www.forteinc.com'>Forte Agent</option>
<option value='www.giganews.com'>Giganews</option>
<option value='www.highwinds.com'>Highwinds</option>
<option value='www.newsdemon.com'>Newsdemon</option>
<option value='www.newsgroupdirect.com'>NewsGroupDirect</option>
<option value='www.newshosting.com'>NewsHosting</option>
<option value='www.readnews.com'>Readnews</option>
<option value='www.supernews.com'>SuperNews</option>
<option value='www.thundernews.com'>ThunderNews</option>
<option value='www.tweaknews.eu'>Tweaknews</option>
<option value='www.usenetserver.com'>UsenetServer</option>
<option value='www.xentech.net'>XenTech</option>
<option value='www.xsnews.nl'>XSnews</option>
<option value=''>$T('other')</option>
</select>
</div>
</div>
<div class="rating_flag_radio">
<input type="radio" name="rating_flag" value="other">&nbsp;$T('otherProblem')
<div class="rating_modal_extra"><input style="width:99%" type="text" name="other"></div>
</div>
<div class="rating_flag_radio">
<input type="radio" name="rating_flag" value="comment">&nbsp;$T('comment')
<div class="rating_modal_extra"><input style="width:99%" type="text" name="comment"></div>
</div>
<br/>
<div class="center">
<input id="submitbtn" type="submit" style="display:none;size:1"/>
<input value="Send" class="juiButton" onclick="flag_modal_submit(this)"/>
<label id="noopt" class="rating_modal_noopt" style="display:none;size:1">No option selected</label>
</div>
</div>
#end if#
</div>

40
interfaces/Plush/templates/history.tmpl

@ -24,7 +24,7 @@
<td class="nzb_status_col">
&nbsp;<div class="nzb_status <!--#if $line.action_line or $line.status=="Queued"#-->Loaded<!--#else if $line.status=="Failed"#-->main_sprite_container sprite_hv_error<!--#else#-->main_sprite_container sprite_hv_star<!--#end if#-->">&nbsp;</div>
</td>
<td class="historyTitle">
<td class="historyTitle" <!--#if $rating_enable#-->style="width:35%"<!--#end if#-->>
<a href="scriptlog?name=$line.nzo_id" class="modal-detail" rel="details">$line.name</a>
<div style="display:none">
@ -106,6 +106,44 @@
<!--#end if#-->
</td>
<!--#if $rating_enable#-->
<!--#if $line.has_rating#-->
<td>
<div class="rating_stars_block_r">
<div class="rating_stars">
<div class="rating_icon_vision"></div><span class="avg_rate" value="$line.rating_avg_video"></span>
<input class="user_combo" type="hidden" value="$line.rating_user_video">
<select class="user_combo video" style="background:transparent">
<!--#if not $line.rating_user_video#--><option>-</option><!--#end if#-->
<!--#for $val in $range(1, 11)#--><option>$val</option><!--#end for#-->
</select>
</div>
<div class="rating_stars">
<div class="rating_icon_sound"></div><span class="avg_rate" value="$line.rating_avg_audio"></span>
<input class="user_combo" type="hidden" value="$line.rating_user_audio">
<select class="user_combo audio" style="background:transparent">
<!--#if not $line.rating_user_audio#--><option>-</option><!--#end if#-->
<!--#for $val in $range(1, 11)#--><option>$val</option><!--#end for#-->
</select>
</div>
</div>
</td>
<td>
<div class="rating_vote_block">
<div class="rating_icon_thumbup user_vote up"></div>
<!--#if $line.rating_user_vote==1#--><b><!--#end if#-->$line.rating_avg_vote_up<!--#if $line.rating_user_vote==1#--></b><!--#end if#-->
<div class="rating_icon_thumbdown user_vote down"></div>
<!--#if $line.rating_user_vote==2#--><b><!--#end if#-->$line.rating_avg_vote_down<!--#if $line.rating_user_vote==2#--></b><!--#end if#-->
</div>
<div class="rating_flag">
<a href="#" class="show_flags">$T('report')</a>
</div>
</td>
<!--#else#-->
<td></td><td></td>
<!--#end if#-->
<!--#end if#-->
<td class="options nowrap">
<!--#if not $line.loaded#-->
<% d = datetime.datetime.fromtimestamp(float(line['completed'])) %>

3
interfaces/Plush/templates/main.tmpl

@ -28,6 +28,7 @@
<!--#if $have_quota#--><li><a id="reset_quota_now" class="pointer">$T('link-resetQuota')</a></li><!--#end if#-->
<!--#if $have_rss_defined#--><li><a id="get_rss_now" class="pointer">$T('button-rssNow')</a></li><!--#end if#-->
<!--#if $have_watched_dir#--><li><a id="get_watched_now" class="pointer">$T('sch-scan_folder')</a></li><!--#end if#-->
<!--#if $pp_pause_event#--><li><a id="resume_pp" class="pointer">$T('sch-resume_post')</a></li><!--#end if#-->
<li><a id="topmenu_toggle" class="pointer">$T('Plush-topMenu')</a></li>
<li><a id="multiops_toggle" class="pointer">$T('Plush-multiOperations')</a></li>
<li>
@ -128,7 +129,7 @@
<select id="multi_cat"><optgroup label="$T('category')">
<option value="">$T('category')</option>
<!--#for $ct in $cat_list#-->
<!--#if $ct != "Default"#--><option value="$ct">$Tspec($ct)</option><!--#end if#-->
<option value="$ct">$Tspec($ct)</option>
<!--#end for#-->
</optgroup></select>
<!--#end if#-->

4
interfaces/Plush/templates/nzo.tmpl

@ -6,7 +6,8 @@
<form action="save" method="post" class="nzo_save_form">
<input type="hidden" name="session" value="$session">
<input type="text" name="name" size="70" value="$slot.filename" />
<input type="text" name="name" size="70" value="$slot.filename_clean" />
<input type="text" name="password" style="width:200px" size="100" value="$slot.password" placeholder="$T('srv-password')"/>
<div>
<select name="index"><optgroup label="$T('order')">
@ -23,6 +24,7 @@
<!--#end if#-->
<select name="priority"><optgroup label="$T('priority')">
<option value="-100" <!--#if $slot.priority == "-100" then "selected" else ""#-->>$T('default')</option>
<option value="2" <!--#if $slot.priority == "2" then "selected" else ""#-->>$T('pr-force')</option>
<option value="1" <!--#if $slot.priority == "1" then "selected" else ""#-->>$T('pr-high')</option>
<option value="0" <!--#if $slot.priority == "0" then "selected" else ""#-->>$T('pr-normal')</option>
<option value="-1" <!--#if $slot.priority == "-1" then "selected" else ""#-->>$T('pr-low')</option>

21
interfaces/Plush/templates/queue.tmpl

@ -55,10 +55,27 @@
<% # <!--#else if $slot.status == "Downloading"#-->main_sprite_container sprite_ql_grip_active %>
</td>
<td class="download-title">
<td class="download-title" <!--#if $rating_enable#-->style="width:35%"<!--#end if#-->>
<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>
<!--#if $rating_enable#-->
<!--#if $slot.has_rating#-->
<td>
<div class="rating_stars_block_c">
<div class="rating_stars">
<div class="rating_icon_vision"></div><span class="avg_rate" value="$slot.rating_avg_video"></span>
</div>
<div class="rating_stars">
<div class="rating_icon_sound"></div><span class="avg_rate" value="$slot.rating_avg_audio"></span>
</div>
</div>
</td>
<!--#else#-->
<td></td>
<!--#end if#-->
<!--#end if#-->
<td>
<div class="main_sprite_container sprite_progressbar_bg">
<div class="main_sprite_container sprite_progress_done" style="background-position: -<!--#if $slot.mb == "0.00" then "120" else int(120 - 120.0 / 100.0 * int(100 - float($slot.mbleft) / float($slot.mb) * 100))#-->px -401px">
@ -69,7 +86,7 @@
</td>
<td class="eta nowrap">
<!--#if not $paused and $slot.status not in ("Paused", "Checking")#-->
<!--#if (not $paused and $slot.status not in ("Paused", "Checking")) or $slot.priority == 'Force'#-->
<span title="$slot.eta">$slot.timeleft&nbsp;$T('Plush-left')</span>
<!--#else#-->
$T('post-'+$slot.status)

13
interfaces/Plush/templates/static/javascripts/lib.js

File diff suppressed because one or more lines are too long

99
interfaces/Plush/templates/static/javascripts/plush.js

@ -169,33 +169,33 @@ jQuery(function($){
// Refresh rate
$("#refreshRate-option").val($.plush.refreshRate).change( function() {
$.plush.refreshRate = $("#refreshRate-option").val();
$.cookie('plushRefreshRate', $.plush.refreshRate, { expires: 365, path: '/' });
$.cookie('plushRefreshRate', $.plush.refreshRate, { expires: 365 });
$.plush.Refresh();
});
// Container width
$("#containerWidth-option").val($.plush.containerWidth).change( function() {
$.plush.containerWidth = $("#containerWidth-option").val();
$.cookie('plushContainerWidth', $.plush.containerWidth, { expires: 365, path: '/' });
$.cookie('plushContainerWidth', $.plush.containerWidth, { expires: 365 });
$('#master-width').css('width',$.plush.containerWidth);
}).trigger('change');
// Confirm Queue Deletions toggle
$("#confirmDeleteQueue").prop('checked', $.plush.confirmDeleteQueue ).change( function() {
$.plush.confirmDeleteQueue = $("#confirmDeleteQueue").prop('checked');
$.cookie('plushConfirmDeleteQueue', $.plush.confirmDeleteQueue ? 1 : 0, { expires: 365, path: '/' });
$.cookie('plushConfirmDeleteQueue', $.plush.confirmDeleteQueue ? 1 : 0, { expires: 365 });
});
// Confirm History Deletions toggle
$("#confirmDeleteHistory").prop('checked', $.plush.confirmDeleteHistory ).change( function() {
$.plush.confirmDeleteHistory = $("#confirmDeleteHistory").prop('checked');
$.cookie('plushConfirmDeleteHistory', $.plush.confirmDeleteHistory ? 1 : 0, { expires: 365, path: '/' });
$.cookie('plushConfirmDeleteHistory', $.plush.confirmDeleteHistory ? 1 : 0, { expires: 365 });
});
// Block Refreshes on Hover toggle
$("#blockRefresh").prop('checked', $.plush.blockRefresh ).change( function() {
$.plush.blockRefresh = $("#blockRefresh").prop('checked');
$.cookie('plushBlockRefresh', $.plush.blockRefresh ? 1 : 0, { expires: 365, path: '/' });
$.cookie('plushBlockRefresh', $.plush.blockRefresh ? 1 : 0, { expires: 365 });
});
// Sabnzbd restart
@ -329,6 +329,17 @@ jQuery(function($){
});
});
// Resume Post Processing
$('#resume_pp').click(function() {
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'resume_pp', apikey: $.plush.apikey},
success: $.plush.RefreshQueue
});
});
$('#multiops_toggle').click(function(){
if( $('#multiops_bar').is(':visible') ) { // hide
$('#multiops_bar').hide();
@ -341,7 +352,7 @@ jQuery(function($){
$.plush.multiOpsChecks = new Array();
$('<input type="checkbox" class="multiops" />').appendTo('#queue tr td.nzb_status_col');
}
$.cookie('plushMultiOps', $.plush.multiOps ? 1 : 0, { expires: 365, path: '/' });
$.cookie('plushMultiOps', $.plush.multiOps ? 1 : 0, { expires: 365 });
});
if ($.plush.multiOps)
$('#multiops_toggle').trigger('click');
@ -354,7 +365,7 @@ jQuery(function($){
$('#topmenu_bar').show();
$.plush.noTopMenu = false;
}
$.cookie('plushNoTopMenu', $.plush.noTopMenu ? 1 : 0, { expires: 365, path: '/' });
$.cookie('plushNoTopMenu', $.plush.noTopMenu ? 1 : 0, { expires: 365 });
});
if ($.plush.noTopMenu)
$('#topmenu_toggle').trigger('click');
@ -510,7 +521,7 @@ jQuery(function($){
$("#queue-pagination-perpage").change(function(event){
$.plush.queuecurpage = Math.floor($.plush.queuecurpage * $.plush.queuePerPage / $(event.target).val() );
$.plush.queuePerPage = $(event.target).val();
$.cookie('plushQueuePerPage', $.plush.queuePerPage, { expires: 365, path: '/' });
$.cookie('plushQueuePerPage', $.plush.queuePerPage, { expires: 365 });
$.plush.queueforcerepagination = true;
$.plush.RefreshQueue();
});
@ -739,9 +750,9 @@ $.plush.queueprevslots = $.plush.queuenoofslots; // for the next refresh
});
var last1, last2;
$("#multiops_select_range").click(function(){
if (last1 && last2 && last1 < last2)
if (last1 >= 0 && last2 >= 0 && last1 < last2)
$("INPUT[type='checkbox']","#queueTable").slice(last1,last2).prop('checked', true).trigger('change');
else if (last1 && last2)
else if (last1 >= 0 && last2 >= 0)
$("INPUT[type='checkbox']","#queueTable").slice(last2,last1).prop('checked', true).trigger('change');
});
$("#multiops_select_invert").click(function(){
@ -754,7 +765,7 @@ $.plush.queueprevslots = $.plush.queuenoofslots; // for the next refresh
});
$("#queue").delegate('.multiops','change',function(event) {
// range event interaction
if (last1) last2 = last1;
if (last1 >= 0) last2 = last1;
last1 = $(event.target).parent()[0].rowIndex ? $(event.target).parent()[0].rowIndex : $(event.target).parent().parent()[0].rowIndex;
// checkbox state persistence
@ -937,7 +948,7 @@ $("a","#multiops_inputs").click(function(e){
$("#history-pagination-perpage").change(function(event){
$.plush.histcurpage = Math.floor($.plush.histcurpage * $.plush.histPerPage / $(event.target).val() );
$.plush.histPerPage = $(event.target).val();
$.cookie('plushHistPerPage', $.plush.histPerPage, { expires: 365, path: '/' });
$.cookie('plushHistPerPage', $.plush.histPerPage, { expires: 365 });
$.plush.histforcerepagination = true;
if ($.plush.histPerPage=="1")
$("#history-pagination").html(''); // pagination rebuild not triggered on blank history (disabled)
@ -1022,7 +1033,7 @@ $("a","#multiops_inputs").click(function(e){
// show all / show failed
$('#failed_only').change(function(){
$.plush.failedOnly = $("#failed_only").val();
$.cookie('plushFailedOnly', $.plush.failedOnly, { expires: 365, path: '/' });
$.cookie('plushFailedOnly', $.plush.failedOnly, { expires: 365 });
$.plush.RefreshHistory();
}).val($.plush.failedOnly);
@ -1036,6 +1047,12 @@ $("a","#multiops_inputs").click(function(e){
title:function(){return $(this).text();},
innerWidth:"80%", innerHeight:"300px", initialWidth:"80%", initialHeight:"300px", speed:0, opacity:0.7 });
// modal for reporting issues
$("#historyTable .modal-report").colorbox({ inline:true,
href: function(){return "#report-"+$(this).parent().parent().parent().attr('id');},
title:function(){return $(this).text();},
innerWidth:"250px", innerHeight:"110px", initialWidth:"250px", initialHeight:"110px", speed:0, opacity:0.7 });
// Build pagination only when needed
if ($.plush.histPerPage=="1") // disabled history
$("#history-pagination").html(''); // remove pages if history empty
@ -1062,6 +1079,55 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
}); // end livequery
$('.user_combo').livequery('change', function(){
var nzo_id = $(this).parent().parent().parent().parent().attr('id');
var videoAudio = $(this).hasClass('video') ? 'video' : 'audio';
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'rating', value: nzo_id, type: videoAudio, setting: $(this).val(), apikey: $.plush.apikey},
success: $.plush.RefreshHistory
});
});
$('.user_vote').livequery('click', function(){
var nzo_id = $(this).parent().parent().parent().attr('id');
var upDown = $(this).hasClass('up') ? 'up' : 'down';
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'rating', value: nzo_id, type: 'vote', setting: upDown, apikey: $.plush.apikey},
success: $.plush.RefreshHistory
});
});
$('#history .show_flags').live('click', function(){
$('#flag_modal_job').val( $(this).parent().parent().parent().attr('id') );
$.colorbox({ inline:true, href:"#flag_modal", title:$(this).text(),
innerWidth:"500px", innerHeight:"185px", initialWidth:"500px", initialHeight:"185px", speed:0, opacity:0.7
});
return false;
});
$('#flag_modal input:submit').click(function(){
var nzo_id = $('#flag_modal_job').val();
var flag = $('input[name=rating_flag]:checked', '#flag_modal').val();
var expired_host = $('input[name=expired_host]', '#flag_modal').val();
var other = $('input[name=other]', '#flag_modal').val();
var comment = $('input[name=comment]', '#flag_modal').val();
var _detail = (flag == 'comment') ? comment : ((flag == 'other') ? other : expired_host);
$.colorbox.close();
$.plush.modalOpen=false;
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'rating', value: nzo_id, type: 'flag', setting: flag, detail: _detail, apikey: $.plush.apikey},
success: $.plush.RefreshHistory
});
});
}, // end $.plush.InitHistory()
@ -1121,6 +1187,8 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
$('.left_stats .initial-loading').hide();
$('#queue').html(result); // Replace queue contents with queue.tmpl
$('#queue .avg_rate').rateit({readonly: true, resetable: false, step: 0.5});
$('#queue .avg_rate').each(function() { $(this).rateit('value', $(this).attr('value') / 2); });
if ($.plush.multiOps) // add checkboxes
$('<input type="checkbox" class="multiops" />').appendTo('#queue tr td.nzb_status_col');
@ -1176,6 +1244,11 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
}
$('.left_stats .initial-loading').hide();
$('#history').html(result); // Replace history contents with history.tmpl
$('#history .avg_rate').rateit({readonly: true, resetable: false, step: 0.5});
$('#history .avg_rate').each(function() { $(this).rateit('value', $(this).attr('value') / 2); });
$('#history .user_combo option').filter(function() {
return $(this).attr('value') == $(this).parent().parent().find('input.user_combo').attr('value');
}).attr('selected', true);
$('#history-pagination span').removeClass('loading'); // Remove spinner graphic from pagination
}
});

74
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/gold.css

@ -1073,7 +1073,81 @@ tr:hover .history_added { color: black; }
.pointer { cursor: pointer; }
/* ---------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------
Ratings
------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------- */
.rating_stars_block_r {
text-align: right;
}
.rating_stars_block_c {
text-align: center;
}
.rating_vote_block {
float: left;
}
.rating_stars {
display: inline-block;
margin-right: 10px;
}
.rating_flag {
margin: 4px 10px 0px 85px;
}
.rating_flag_radio {
margin: 5px 15px 5px 5px;
}
.rating_modal_extra {
float: right;
width: 330px
}
.rating_modal_expired {
float: right;
}
.rating_modal_noopt {
color: red;
margin-left: 10px;
}
.rating_icon_vision {
width: 16px;
height: 16px;
display: inline-block;
margin-right: 5px;
background: url('images/vision16.png') no-repeat top center;
}
.rating_icon_sound {
width: 16px;
height: 16px;
display: inline-block;
margin-right: 5px;
background: url('images/sound16.png') no-repeat top center;
}
.rating_icon_thumbup {
width: 20px;
height: 20px;
display: inline-block;
background: url('images/thumbup20.png') no-repeat top center;
}
.rating_icon_thumbdown {
width: 20px;
height: 20px;
display: inline-block;
background: url('images/thumbdown20.png') no-repeat top center;
}
/* ---------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------

BIN
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/sound16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

BIN
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/thumbdown20.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

BIN
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/thumbup20.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

BIN
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/vision16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

BIN
interfaces/Plush/templates/static/stylesheets/rateit/delete.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

98
interfaces/Plush/templates/static/stylesheets/rateit/rateit.css

@ -0,0 +1,98 @@
.rateit {
display: -moz-inline-box;
display: inline-block;
position: relative;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}
.rateit .rateit-range
{
position: relative;
display: -moz-inline-box;
display: inline-block;
background: url(star.gif);
height: 16px;
outline: none;
}
.rateit .rateit-range * {
display:block;
}
/* for IE 6 */
* html .rateit, * html .rateit .rateit-range
{
display: inline;
}
/* for IE 7 */
* + html .rateit, * + html .rateit .rateit-range
{
display: inline;
}
.rateit .rateit-hover, .rateit .rateit-selected
{
position: absolute;
left: 0px;
}
.rateit .rateit-hover-rtl, .rateit .rateit-selected-rtl
{
left: auto;
right: 0px;
}
.rateit .rateit-hover
{
background: url(star.gif) left -32px;
}
.rateit .rateit-hover-rtl
{
background-position: right -32px;
}
.rateit .rateit-selected
{
background: url(star.gif) left -48px;
}
.rateit .rateit-selected-rtl
{
background-position: right -48px;
}
.rateit .rateit-preset
{
background: url(star.gif) left -16px;
}
.rateit .rateit-preset-rtl
{
background: url(star.gif) left -16px;
}
.rateit button.rateit-reset
{
background: url(delete.gif) 0 0;
width: 16px;
height: 16px;
display: -moz-inline-box;
display: inline-block;
float: left;
outline: none;
border:none;
padding: 0;
}
.rateit button.rateit-reset:hover, .rateit button.rateit-reset:focus
{
background-position: 0 -16px;
}

BIN
interfaces/Plush/templates/static/stylesheets/rateit/star.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

2
interfaces/smpl/templates/main.tmpl

@ -1129,7 +1129,7 @@ function loadingJSON(){
<li><a class="config" href="$prefix/config/general/" onclick="lr('config/general/','', 0, 0);">$T('cmenu-general')</a></li>
<li><a class="config" href="$prefix/config/folders/" onclick="lr('config/folders/','', 0, 0);">$T('cmenu-folders')</a> </li>
<li><a class="config" href="$prefix/config/switches/" onclick="lr('config/switches/','', 0, 0);">$T('cmenu-switches')</a> </li>
<li><a class="config" href="$prefix/config/server" onclick="lr('config/server/','', 0, 0);">$T('cmenu-servers')</a> </li>
<li><a class="config" href="$prefix/config/server/" onclick="lr('config/server/','', 0, 0);">$T('cmenu-servers')</a> </li>
<li><a class="config" href="$prefix/config/scheduling/" onclick="lr('config/scheduling/','', 0, 0);">$T('cmenu-scheduling')</a> </li>
<li><a class="config" href="$prefix/config/rss/" onclick="lr('config/rss/','', 0, 0);">$T('cmenu-rss')</a> </li>
<li><a class="config" href="$prefix/config/notify/" onclick="lr('config/notify/','', 0, 0);">$T('cmenu-notif')</a></li>

6
interfaces/smpl/templates/nzo.tmpl

@ -3,7 +3,10 @@
<h3>$T('nzoDetails')</h3>
<form id="nzbEditForm" class="cmxform">
<label class="label">$T('nzoName'):</label>
<input type="text" name="name" style="width:400px" size="80" value="$slot.filename">
<input type="text" name="name" style="width:400px" size="80" value="$slot.filename_clean">
<br class="clear" />
<label class="label">$T('srv-password'):</label>
<input type="text" name="password" style="width:200px" size="100" value="$slot.password">
<br class="clear" />
<label class="label">$T('pp'):</label>
<select name="pp">
@ -19,6 +22,7 @@
<select name="priority">
<optgroup label="$T('priority')">
<option value="-100" <!--#if $slot.priority == "-100" then "selected" else ""#-->>$T('default')</option>
<option value="2" <!--#if $slot.priority == "2" then "selected" else ""#-->>$T('pr-force')</option>
<option value="1" <!--#if $slot.priority == "1" then "selected" else ""#-->>$T('pr-high')</option>
<option value="0" <!--#if $slot.priority == "0" then "selected" else ""#-->>$T('pr-normal')</option>
<option value="-1" <!--#if $slot.priority == "-1" then "selected" else ""#-->>$T('pr-low')</option>

34
interfaces/wizard/four.html

@ -0,0 +1,34 @@
<!--#include $webdir + "/inc_top.tmpl"#-->
<script type="text/javascript" src="static/javascript/jquery.js"></script>
<script type="text/javascript" src="static/javascript/restart.js"></script>
<br/><br/>
<h4 id="restarting" class="align-center">$T('wizard-restarting')</h4>
<h4 id="complete" class="align-center success hidden">$T('wizard-complete')</h4>
<br />
<br/>
<div id="tips" class="hidden">
$T('wizard-tip1') <span class="bold">$T('wizard-tip2')</span><br/>
<!--#set $tip3 = $T('wizard-tip3') % ''#-->
$tip3<br/><br/>
<div class="quoteBlock">
<!--#set $i = 0#-->
<!--#for $url in $urls#-->
<!--#set $i = $i+1#-->
<a href="$url">$url</a><!--#if $i != len($urls)#--><br /><!--#end if#-->
<!--#end for#-->
</div><br/>
$T('wizard-tip4')
<br/><br/>
$T('wizard-tip-wiki') <a href="$helpuri">wiki</a>
</div>
</div>
<hr /><br/>
<div class="full-width">
<table class="full-width">
<tr class="align-center">
<td><input type="hidden" name="session" id="apikey" value="$session"><input class="bigbutton disabled" type="button" onclick="document.location ='$access_url'" value="$T('wizard-goto')" disabled="disabled"/></td>
</tr>
</table>
</div>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

1
interfaces/wizard/inc_top.tmpl

@ -1,5 +1,6 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>$T('wizard-quickstart')</title>
<link rel="stylesheet" type="text/css" href="static/style.css"/>
<link rel="shortcut icon" href="static/images/favicon.ico" />

50
interfaces/wizard/three.html

@ -1,34 +1,38 @@
<!--#include $webdir + "/inc_top.tmpl"#-->
<script type="text/javascript" src="static/javascript/jquery.js"></script>
<script type="text/javascript" src="static/javascript/restart.js"></script>
<br/><br/>
<h4 id="restarting" class="align-center">$T('wizard-restarting')</h4>
<h4 id="complete" class="align-center success hidden">$T('wizard-complete')</h4>
<br />
<br/>
<div id="tips" class="hidden">
$T('wizard-tip1') <span class="bold">$T('wizard-tip2')</span><br/>
<!--#set $tip3 = $T('wizard-tip3') % ''#-->
$tip3<br/><br/>
<div class="quoteBlock">
<!--#set $i = 0#-->
<!--#for $url in $urls#-->
<!--#set $i = $i+1#-->
<a href="$url">$url</a><!--#if $i != len($urls)#--><br /><!--#end if#-->
<!--#end for#-->
</div><br/>
$T('wizard-tip4')
<br/><br/>
$T('wizard-tip-wiki') <a href="$helpuri">wiki</a>
<form action="./four" method="post" autocomplete="off">
<div class="indented bigger">
<h3>Indexer</h3>
<div>$T('explain-rating_enable')</div>
<div>$T('wizard-create-account')<a href="https://www.oznzb.com/register" target="_blank">https://www.oznzb.com/register</a>.</div>
<br class="clear" />
<input type="checkbox" name="rating_enable" id="rating_enable" value="1" <!--#if $rating_enable == 1 then 'checked="checked"' else ''#-->> <label for="rating_enable">$T('opt-rating_enable')</label><br />
<br class="clear" />
<div>
<label class="label">$T('opt-rating_api_key')</label><input type="text" size="35" value="$rating_api_key" name="rating_api_key" id="rating_api_key">
<div class="tips">$T('tip-rating_api_key')</div>
</div>
<br class="clear" />
</div>
</div>
<hr /><br/>
<div class="full-width">
<table class="full-width">
<tr class="align-center">
<td><input type="hidden" name="session" id="apikey" value="$session"><input class="bigbutton disabled" type="button" onclick="document.location ='$access_url'" value="$T('wizard-goto')" disabled="disabled"/></td>
<tr>
<td><input class="bigbutton" type="button" onclick="document.location ='./two'" value="&lsaquo; $T('wizard-previous')" /></td>
<td>
<div class="align-center">
<!--#for $step in xrange($steps)#-->
<!--#set $step = $step + 1#-->
<span class="<!--#if $step == $number then 'selected' else 'unselected'#-->">$step</span>
<!--#end for#-->
</div>
</td>
<td class="align-right"><input class="bigbutton" type="submit" value="$T('wizard-next') &raquo;" /></td>
</tr>
</table>
</div>
<!--#include $webdir + "/inc_bottom.tmpl"#-->
</form>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

30
licenses/License-OrderedDict.txt

@ -0,0 +1,30 @@
The Backport of OrderedDict() is coming from ActiveState's Python recipe website.
It has been written by Raymond Hettinger.
Home of the module:
http://code.activestate.com/recipes/576693-ordered-dictionary-for-py24/
It is covered by the MIT License.
===================
(c) 2009 Raymond Hettinger
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
licenses/License-PythonParts.txt

@ -0,0 +1,3 @@
pystone.py and msgfmt.py have been copied from the Python sourcecode.
They are covered by the same license as Python itself,
that license is contained in the file "License_Python.txt".

2
licenses/License-pynewsleecher.txt

@ -1,4 +1,4 @@
The original author of SABnzbd based his work on Pynewleecher by Freddy@madcowdesease.org.
The original author of SABnzbd based his work on Pynewsleecher by Freddy@madcowdesease.org.
Few parts of Pynewsleecher have survived the generations of SABnzbd in a
recognizable form.

31
make_dmg.py

@ -20,6 +20,15 @@
import os
import sys
import re
import platform
OSX_MAV = [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 9, 0]
# Check if signing is possible
authority = os.environ.get('SIGNING_AUTH')
if authority and not OSX_MAV:
print 'Signing should be done on OSX Mavericks (10.9.x) or higher'
exit(1)
if len(sys.argv) < 2:
print 'Usage: %s <release>' % os.path.split(sys.argv[0])[1]
@ -33,15 +42,15 @@ 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)'
'Snow Leopard',
'Lion',
'.'
)
# Check presense of all builds
sharepath = os.environ.get('SHARE')
if not (sharepath and os.path.exists(sharepath)):
print 'Build share not defined or not found'
print 'Build share not defined or not found. Path expected in env variable SHARE'
exit(1)
build_paths = []
@ -69,15 +78,13 @@ 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
# Unpack build into image and sign if possible and not already done and not SnowLeopard
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))
if authority and builds[build] != 'sl':
if not os.path.exists(os.path.join(vol_path, 'SABnzbd.app/Contents/_CodeSignature')):
os.system('codesign --deep -f -i "org.sabnzbd.SABnzbd" -s "%s" "%s/SABnzbd.app"' % (authority, vol_path))
# Put README.rtf in root
@ -106,3 +113,7 @@ os.system("hdiutil internet-enable %s" % fileDmg)
print 'Copy GZ file'
os.system('cp "%s" .' % os.path.join(sharepath, fileOSr))
if not authority:
print "Images are not signed!"
print

BIN
osx/image/template.sparseimage.zip

Binary file not shown.

BIN
osx/unrar/unrar

Binary file not shown.

0
osx/unrar/unrar-leopard → osx/unrar/unrar-ppc

26
package.py

@ -416,7 +416,15 @@ if target == 'app':
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")
# Create a symlink for Mavericks compatibility
if not OSX_SL:
cdir = os.getcwd()
os.chdir('dist/SABnzbd.app/Contents/Frameworks/Python.framework/Versions')
if os.path.exists('2.7') and not os.path.exists('Current'):
os.system('ln -s 2.7 Current')
os.chdir(cdir)
# 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")
@ -424,9 +432,8 @@ if target == 'app':
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_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 -pR osx/unrar/unrar-ppc dist/SABnzbd.app/Contents/Resources/osx/unrar/ >/dev/null")
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("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")
@ -435,8 +442,8 @@ if target == 'app':
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')
os.system('rm dist/SABnzbd.app/Contents/Resources/site.py 2>/dev/null')
# Add the SabNotifier app
if OSX_ML and os.path.exists(os.path.join(os.environ['HOME'], 'sabnotifier/SABnzbd.app')):
os.system("cp -pR $HOME/sabnotifier/SABnzbd.app dist/SABnzbd.app/Contents/Resources/")
@ -448,6 +455,11 @@ if target == 'app':
os.system("sleep 5")
# Sign if possible
authority = os.environ.get('SIGNING_AUTH')
if authority:
os.system('codesign --deep -f -i "org.sabnzbd.SABnzbd" -s "%s" "dist/SABnzbd.app"' % authority)
# Archive result to share, if present
dest_path = os.environ.get('SHARE')
if dest_path and os.path.exists(dest_path):
@ -612,7 +624,7 @@ else:
os.mkdir(root)
# Set data files
data_files.extend(['po/', 'cherrypy/', 'gntp/'])
data_files.extend(['po/', 'cherrypy/', 'gntp/', 'solaris/'])
options['data_files'] = PairList(data_files)
options['data_files'].append(('tools', ['tools/make_mo.py', 'tools/msgfmt.py']))

2
po/email/SABemail.pot

@ -1,6 +1,6 @@
#
# SABnzbd Translation Template file EMAIL
# Copyright (C) 2012 by the SABnzbd Team
# Copyright (C) 2011-2012 by the SABnzbd Team
# team@sabnzbd.org
#
msgid ""

6
po/email/da.po

@ -7,15 +7,15 @@ msgid ""
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"
"POT-Creation-Date: 2013-12-10 20:31+0000\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-08-04 05:38+0000\n"
"X-Generator: Launchpad (build 15742)\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""

6
po/email/de.po

@ -7,15 +7,15 @@ msgid ""
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"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2012-12-28 10:58+0000\n"
"Last-Translator: Thomas Lucke (Lucky) <Unknown>\n"
"Language-Team: German <de@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-12-29 05:11+0000\n"
"X-Generator: Launchpad (build 16378)\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""

6
po/email/es.po

@ -7,15 +7,15 @@ msgid ""
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"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2012-04-03 09:00+0000\n"
"Last-Translator: shypike <Unknown>\n"
"Language-Team: Spanish <es@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-04-29 05:17+0000\n"
"X-Generator: Launchpad (build 15149)\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""

212
po/email/fi.po

@ -0,0 +1,212 @@
# Finnish translation for sabnzbd
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
# This file is distributed under the same license as the sabnzbd package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
#
msgid ""
msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2013-02-19 15:28+0000\n"
"Last-Translator: Matti Ylönen <Unknown>\n"
"Language-Team: Finnish <fi@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""
"##\n"
"## Default Email template for SABnzbd\n"
"## This a Cheetah template\n"
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"##\n"
"## These are the email headers\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd has <!--#if $status then \"completed\" else \"failed\" #--> "
"job $name\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## After this comes the body, the empty line is required!\n"
"\n"
"Hi,\n"
"<!--#if $status #-->\n"
"SABnzbd has downloaded \"$name\" <!--#if $msgid==\"\" then \"\" else "
"\"(newzbin #\" + $msgid + \")\"#-->\n"
"<!--#else#-->\n"
"SABnzbd has failed to download \"$name\" <!--#if $msgid==\"\" then \"\" else "
"\"(newzbin #\" + $msgid + \")\"#-->\n"
"<!--#end if#-->\n"
"Finished at $end_time\n"
"Downloaded $size\n"
"\n"
"Results of the job:\n"
"<!--#for $stage in $stages #-->\n"
"Stage $stage <!--#slurp#-->\n"
"<!--#for $result in $stages[$stage]#-->\n"
" $result <!--#slurp#-->\n"
"<!--#end for#-->\n"
"<!--#end for#-->\n"
"<!--#if $script!=\"\" #-->\n"
"Output from user script \"$script\" (Exit code = $script_ret):\n"
"$script_output\n"
"<!--#end if#-->\n"
"<!--#if $status #-->\n"
"Enjoy!\n"
"<!--#else#-->\n"
"Sorry!\n"
"<!--#end if#-->\n"
msgstr ""
"##\n"
"## Oletus sähköpostipohja SABnzbd:lle\n"
"## Tämä on Cheetah pohja\n"
"## Dokumentaatio: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Rivinvaihdot ja välilyönnit ovat merkitseviä!\n"
"##\n"
"## Nämä ovat otsaketiedot. Rivien ensimmäisiä sanoja ei saa vaihtaa!\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd on <!--#if $status then \"valmistunut\" else "
"\"epäonnistunut\" #--> työssä $name\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## Tämän jälkeen tulee viestin runko, ensimmäinen rivinvaihto on "
"pakollinen!\n"
"\n"
"Hei,\n"
"<!--#if $status #-->\n"
"SABnzbd on ladannut \"$name\" <!--#if $msgid==\"\" then \"\" else \"(newzbin "
"#\" + $msgid + \")\"#-->\n"
"<!--#else#-->\n"
"SABnzbd on epäonnistunut \"$name\" <!--#if $msgid==\"\" then \"\" else "
"\"(newzbin #\" + $msgid + \")\"#--> latauksessa\n"
"<!--#end if#-->\n"
"Valmistui $end_time\n"
"Ladattu $size\n"
"\n"
"Työn lopputulos:\n"
"<!--#for $stage in $stages #-->\n"
"Tila $stage <!--#slurp#-->\n"
"<!--#for $result in $stages[$stage]#-->\n"
" $result <!--#slurp#-->\n"
"<!--#end for#-->\n"
"<!--#end for#-->\n"
"<!--#if $script!=\"\" #-->\n"
"Käyttäjän skriptin tuloste \"$script\" (Exit code = $script_ret):\n"
"$script_output\n"
"<!--#end if#-->\n"
"<!--#if $status #-->\n"
"Nauti!\n"
"<!--#else#-->\n"
"Pahoittelut!\n"
"<!--#end if#-->\n"
#: email/rss.tmpl:1
msgid ""
"##\n"
"## RSS Email template for SABnzbd\n"
"## This a Cheetah template\n"
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"##\n"
"## These are the email headers\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd has added $amount jobs to the queue\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## After this comes the body, the empty line is required!\n"
"\n"
"Hi,\n"
"\n"
"SABnzbd has added $amount job(s) to the queue.\n"
"They are from RSS feed \"$feed\".\n"
"<!--#for $job in $jobs#-->\n"
" $job <!--#slurp#-->\n"
"<!--#end for#-->\n"
"\n"
"Bye\n"
msgstr ""
"##\n"
"## RSS sähköpostipohja SABnzbd:lle\n"
"## Tämä on Cheetah pohja\n"
"## Dokumentaatio: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Rivinvaihdot ja välilyönnit ovat merkitseviä!\n"
"##\n"
"## Nämä ovat otsaketiedot. Rivien ensimmäisiä sanoja ei saa vaihtaa!\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd on lisännyt $amount työtä jonoon\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## Tämän jälkeen tulee viestin runko, ensimmäinen rivinvaihto on "
"pakollinen!\n"
"\n"
"Hei,\n"
"\n"
"SABnzbd on lisännyt $amount työtä jonoon.\n"
"Ne ovat RSS syötteestä \"$feed\".\n"
"<!--#for $job in $jobs#-->\n"
" $job <!--#slurp#-->\n"
"<!--#end for#-->\n"
"\n"
"Heippa\n"
#: email/badfetch.tmpl:1
msgid ""
"##\n"
"## Bad URL Fetch Email template for SABnzbd\n"
"## This a Cheetah template\n"
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"##\n"
"## These are the email headers\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd failed to fetch an NZB\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## After this comes the body, the empty line is required!\n"
"\n"
"Hi,\n"
"\n"
"SABnzbd has failed to retrieve the NZB from $url.\n"
"The error message was: $msg\n"
"\n"
"Bye\n"
msgstr ""
"##\n"
"## Virheellisen URL-noudon sähköpostin pohja SABnzbd ohjelmalle\n"
"## Tämä on Cheetah pohja\n"
"## Dokumentaatio: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Rivinvaihdot ja välilyönnit ovat merkitseviä!\n"
"##\n"
"## Tässä on sähköpostin otsikkotiedot\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd ei voinut hakea NZB tiedostoa\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## Tämän jälkeen tulee viestin sisältö, tyhjä rivi on pakollinen!\n"
"\n"
"Hei,\n"
"\n"
"SABnzbd ei voinut hakea NZB tiedostoa osoitteesta $url.\n"
"Virheviesti: $msg\n"

6
po/email/fr.po

@ -7,15 +7,15 @@ msgid ""
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"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2012-03-18 07:02+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-04-29 05:17+0000\n"
"X-Generator: Launchpad (build 15149)\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""

32
po/email/nb.po

@ -7,15 +7,15 @@ msgid ""
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: 2011-06-26 10:50+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2013-12-10 20:12+0000\n"
"Last-Translator: shypike <Unknown>\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-04-29 05:17+0000\n"
"X-Generator: Launchpad (build 15149)\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""
@ -190,3 +190,25 @@ msgid ""
"\n"
"Bye\n"
msgstr ""
"##\n"
"## Bad URL Fetch Email template for SABnzbd\n"
"## This a Cheetah template\n"
"## Documentation: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"##\n"
"## These are the email headers\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd ikke klarte å hente en NZB fil\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## Etter dette kommer meldingen, den tomme linjen er nødvendig!\n"
"\n"
"Hei,\n"
"\n"
"SABnzbd klarte ikke å hente NZB fra $url.\n"
"Feilmeldingen var: $msg\n"
"\n"
"Hade\n"

32
po/email/nl.po

@ -7,15 +7,15 @@ msgid ""
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-09 19:11+0000\n"
"Last-Translator: shypike <Unknown>\n"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2013-11-03 22:34+0000\n"
"Last-Translator: markheloking <markheloking@live.nl>\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-04-29 05:17+0000\n"
"X-Generator: Launchpad (build 15149)\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""
@ -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 sjabloon voor SABnzbd\n"
"## Dit is een Cheetah sjabloon\n"
"## Documentatie: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"## Lege regels en spaties zijn belangrijk!\n"
"##\n"
"## These are the email headers\n"
"## Dit zijn de email koppen\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"
"Subject: SABnzbd heeft $amount opdrachten aan de wachtrij toegevoegd\n"
"X-priority: 5\n"
"X-MS-priority: 5\n"
"## After this comes the body, the empty line is required!\n"
"## Hierna komt de inhoud, de lege regel is benodigd!\n"
"\n"
"Hallo,\n"
"\n"
@ -187,13 +187,13 @@ 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"
"## Ongeldige URL Ophaal Email sjabloon voor SABnzbd\n"
"## Dit is een Cheetah sjabloon\n"
"## Documentatie: http://sabnzbd.wikidot.com/email-templates\n"
"##\n"
"## Newlines and whitespace are significant!\n"
"## Lege regels en spaties zijn belangrijk!\n"
"##\n"
"## These are the email headers\n"
"## Dit zijn de email koppen\n"
"To: $to\n"
"From: $from\n"
"Date: $date\n"

6
po/email/pl.px

@ -7,15 +7,15 @@ msgid ""
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"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2012-05-02 09:57+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: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""

6
po/email/pt_BR.po

@ -7,15 +7,15 @@ msgid ""
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"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2012-03-10 04:16+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-04-29 05:17+0000\n"
"X-Generator: Launchpad (build 15149)\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""

6
po/email/ro.px

@ -7,15 +7,15 @@ msgid ""
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"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2012-04-16 03:32+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-04-29 05:17+0000\n"
"X-Generator: Launchpad (build 15149)\n"
"X-Launchpad-Export-Date: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""

6
po/email/sv.po

@ -7,15 +7,15 @@ msgid ""
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"
"POT-Creation-Date: 2013-12-10 20:31+0000\n"
"PO-Revision-Date: 2012-05-15 19:28+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: 2013-12-11 06:16+0000\n"
"X-Generator: Launchpad (build 16869)\n"
#: email/email.tmpl:1
msgid ""

1888
po/main/SABnzbd.pot

File diff suppressed because it is too large

1909
po/main/da.po

File diff suppressed because it is too large

1938
po/main/de.po

File diff suppressed because it is too large

1899
po/main/es.po

File diff suppressed because it is too large

4536
po/main/fi.po

File diff suppressed because it is too large

1915
po/main/fr.po

File diff suppressed because it is too large

2214
po/main/nb.po

File diff suppressed because it is too large

1919
po/main/nl.po

File diff suppressed because it is too large

1915
po/main/pl.px

File diff suppressed because it is too large

1923
po/main/pt_BR.po

File diff suppressed because it is too large

1900
po/main/ro.px

File diff suppressed because it is too large

1915
po/main/sv.po

File diff suppressed because it is too large

116
po/nsis/fi.po

@ -0,0 +1,116 @@
# Finnish translation for sabnzbd
# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
# This file is distributed under the same license as the sabnzbd package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
#
msgid ""
msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-08-14 18:42+0000\n"
"PO-Revision-Date: 2013-02-19 15:24+0000\n"
"Last-Translator: Matti Ylönen <Unknown>\n"
"Language-Team: Finnish <fi@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-20 05:14+0000\n"
"X-Generator: Launchpad (build 16491)\n"
#: NSIS_Installer.nsi:425
msgid "Go to the SABnzbd Wiki"
msgstr "Siirry SABnzbd wikiin"
#: NSIS_Installer.nsi:427
msgid "Show Release Notes"
msgstr "Näytä julkaisutiedot"
#: NSIS_Installer.nsi:429
msgid "Support the project, Donate!"
msgstr "Tue projektia, lahjoita!"
#: NSIS_Installer.nsi:431
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Ole hyvä ja sulje \"SABnzbd.exe\" ensin"
#: 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 ""
" >>>> VAROITUS <<<<\\r\\n\\r\\nOle hyvä ja tarkista "
"julkaisutiedot tai käy osoitteessa http://wiki.sabnzbd.org/introducing-0-7-0 "
"!"
#: NSIS_Installer.nsi:435
msgid "This will uninstall SABnzbd from your system"
msgstr "Tämä poistaa SABnzbd:n tietokoneestasi"
#: NSIS_Installer.nsi:437
msgid "Run at startup"
msgstr "Suorita käynnistyksen yhteydessä"
#: NSIS_Installer.nsi:439
msgid "Desktop Icon"
msgstr "Työpöydän kuvake"
#: NSIS_Installer.nsi:441
msgid "NZB File association"
msgstr "NZB tiedostosidos"
#: NSIS_Installer.nsi:443
msgid "Delete Program"
msgstr "Poista sovellus"
#: NSIS_Installer.nsi:445
msgid "Delete Settings"
msgstr "Poista asetukset"
#: 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 ""
"Tämä järjestelmä vaatii, että Microsoft runtime kirjasto VC90 täytyy asentaa "
"ensin. Haluatko asentaa sen nyt?"
#: NSIS_Installer.nsi:449
msgid "Downloading Microsoft runtime installer..."
msgstr "Ladataan Microsoft runtime asennusta..."
#: NSIS_Installer.nsi:451
msgid "Download error, retry?"
msgstr "Latausvirhe, yritä uudelleen?"
#: NSIS_Installer.nsi:453
msgid "Cannot install without runtime library, retry?"
msgstr "Ei voida asentaa ilman runtime kirjastoa, yritä uudelleen?"
#: 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 ""
"Et voi asentaa tätä vanhan asennuksen päälle. \\n\\nPaina `OK` poistaaksesi "
"edellisen version tai paina `Peruuta` peruuttaaksesi tämän päivityksen."
#: NSIS_Installer.nsi:457
msgid "Your settings and data will be preserved."
msgstr "Asetuksiasi ja tietojasi ei poisteta."
#~ msgid ""
#~ " >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
#~ "release notes or go to http://wiki.sabnzbd.org/introducing-0-6-0 !"
#~ msgstr ""
#~ " >>>> VAROITUS <<<<\\r\\n\\r\\nOle hyvä ja tarkista "
#~ "julkaisutiedot tai mene osoitteeseen http://wiki.sabnzbd.org/introducing-0-6-"
#~ "0 !"
#~ msgid "Start SABnzbd (hidden)"
#~ msgstr "Aloita SABnzbd (piilotettuna)"
#~ msgid "Delete Cache"
#~ msgstr "Poista välimuisti"
#~ msgid "Delete Logs"
#~ msgstr "Poista lokit"

78
po/nsis/nb.po

@ -8,85 +8,93 @@ msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\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"
"PO-Revision-Date: 2013-12-04 12:09+0000\n"
"Last-Translator: Daniel Sebastian <trinitytest@hotmail.com>\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-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
"X-Launchpad-Export-Date: 2013-12-05 05:57+0000\n"
"X-Generator: Launchpad (build 16863)\n"
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:416
msgid "Go to the SABnzbd Wiki"
msgstr ""
msgstr "Gå til SABnzbd Wiki"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:418
msgid "Show Release Notes"
msgstr ""
msgstr "Vis versjonsmerknader"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:420
msgid "Support the project, Donate!"
msgstr ""
msgstr "Støtt prosjektet, donèr!"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:422
msgid "Please close \"SABnzbd.exe\" first"
msgstr ""
msgstr "Vennligst lukk \"SABnzbd.exe\" først"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:424
msgid ""
" >>>> WARNING <<<<\\r\\n\\r\\nPlease, first check the "
"release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
msgstr ""
" >>>> ADVARSEL <<<<\\r\\n\\r\\nVennligst sjekk "
"versjonsmerknadene først, eller gå til http://wiki.sabnzbd.org/introducing-0-"
"7-0 !"
#: NSIS_Installer.nsi:435
#: NSIS_Installer.nsi:426
msgid "This will uninstall SABnzbd from your system"
msgstr ""
msgstr "Dette vil avinstallere SABnzbd fra ditt system"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:428
msgid "Run at startup"
msgstr ""
msgstr "Kjør ved oppstart"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:430
msgid "Desktop Icon"
msgstr ""
msgstr "Skrivebordsikon"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:432
msgid "NZB File association"
msgstr ""
msgstr "NZB-filassosiering"
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:434
msgid "Delete Program"
msgstr ""
msgstr "Fjern program"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:436
msgid "Delete Settings"
msgstr ""
msgstr "Slett innstillinger"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:438
msgid ""
"This system requires the Microsoft runtime library VC90 to be installed "
"first. Do you want to do that now?"
msgstr ""
"Dette sytemet krever at Microsoft runtime library VC90 er installert først. "
"Ønsker du å gjøre dette nå?"
#: NSIS_Installer.nsi:449
#: NSIS_Installer.nsi:440
msgid "Downloading Microsoft runtime installer..."
msgstr ""
msgstr "Laster ned Microsoft runtime installer..."
#: NSIS_Installer.nsi:451
#: NSIS_Installer.nsi:442
msgid "Download error, retry?"
msgstr ""
msgstr "Nedlasting feilet, prøve på nytt?"
#: NSIS_Installer.nsi:453
#: NSIS_Installer.nsi:444
msgid "Cannot install without runtime library, retry?"
msgstr ""
msgstr "Kan ikke installere uten runtime library, prøve på nytt?"
#: NSIS_Installer.nsi:455
#: NSIS_Installer.nsi:446
msgid ""
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
"the previous version or `Cancel` to cancel this upgrade."
msgstr ""
"Du kan ikke overskrive en eksisterende installasjon. \\n\\nTrykk 'OK' for å "
"fjerne tidligere installasjon, eller 'Avbryt' for å avbryte denne "
"oppgraderingen."
#: NSIS_Installer.nsi:457
#: NSIS_Installer.nsi:448
msgid "Your settings and data will be preserved."
msgstr ""
msgstr "Dine innstillinger og data vil bli tatt vare på."

50
po/nsis/nl.po

@ -8,32 +8,32 @@ msgstr ""
"Project-Id-Version: sabnzbd\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\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"
"PO-Revision-Date: 2013-11-03 22:38+0000\n"
"Last-Translator: markheloking <markheloking@live.nl>\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-08-15 05:11+0000\n"
"X-Generator: Launchpad (build 15801)\n"
"X-Launchpad-Export-Date: 2013-11-04 05:55+0000\n"
"X-Generator: Launchpad (build 16820)\n"
#: NSIS_Installer.nsi:425
#: NSIS_Installer.nsi:416
msgid "Go to the SABnzbd Wiki"
msgstr "Ga naar de SABnzbd Wiki"
#: NSIS_Installer.nsi:427
#: NSIS_Installer.nsi:418
msgid "Show Release Notes"
msgstr "Toon vrijgave bericht"
#: NSIS_Installer.nsi:429
#: NSIS_Installer.nsi:420
msgid "Support the project, Donate!"
msgstr "Steun het project, Doneer!"
#: NSIS_Installer.nsi:431
#: NSIS_Installer.nsi:422
msgid "Please close \"SABnzbd.exe\" first"
msgstr "Sluit \"SABnzbd.exe\" eerst af"
#: NSIS_Installer.nsi:433
#: NSIS_Installer.nsi:424
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:435
#: NSIS_Installer.nsi:426
msgid "This will uninstall SABnzbd from your system"
msgstr "Dit verwijdert SABnzbd van je systeem"
#: NSIS_Installer.nsi:437
#: NSIS_Installer.nsi:428
msgid "Run at startup"
msgstr "Opstarten bij systeem start"
#: NSIS_Installer.nsi:439
#: NSIS_Installer.nsi:430
msgid "Desktop Icon"
msgstr "Pictogram op bureaublad"
msgstr "Bureaubladpictogram"
#: NSIS_Installer.nsi:441
#: NSIS_Installer.nsi:432
msgid "NZB File association"
msgstr "NZB bestanden koppelen aan SABnzbd"
#: NSIS_Installer.nsi:443
#: NSIS_Installer.nsi:434
msgid "Delete Program"
msgstr "Verwijder programma"
msgstr "Programma verwijderen"
#: NSIS_Installer.nsi:445
#: NSIS_Installer.nsi:436
msgid "Delete Settings"
msgstr "Verwijder instellingen"
#: NSIS_Installer.nsi:447
#: NSIS_Installer.nsi:438
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:449
#: NSIS_Installer.nsi:440
msgid "Downloading Microsoft runtime installer..."
msgstr "Downloaden van de Microsoft bibliotheek"
#: NSIS_Installer.nsi:451
#: NSIS_Installer.nsi:442
msgid "Download error, retry?"
msgstr "Download mislukt, opnieuw?"
msgstr "Download mislukt, opnieuw proberen?"
#: NSIS_Installer.nsi:453
#: NSIS_Installer.nsi:444
msgid "Cannot install without runtime library, retry?"
msgstr "Installeren heeft geen zin zonder de bibliotheek, opnieuw?"
msgstr "Installeren heeft geen zin zonder de bibliotheek, opnieuw proberen?"
#: NSIS_Installer.nsi:455
#: NSIS_Installer.nsi:446
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:457
#: NSIS_Installer.nsi:448
msgid "Your settings and data will be preserved."
msgstr "Je instellingen en bestanden blijven behouden."

34
sabnzbd/__init__.py

@ -30,6 +30,7 @@ import gzip
import subprocess
import time
import cherrypy
import sys
from threading import RLock, Lock, Condition, Thread
try:
import sleepless
@ -61,12 +62,13 @@ elif os.name == 'posix':
FOUNDATION = True
except:
pass
if platform.machine() == 'i386':
if '86' in platform.machine():
DARWIN_INTEL = True
if DARWIN:
DARWIN_YS = [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 9]
DARWIN_ML = [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 8]
else:
DARWIN_ML = False
DARWIN_YS = DARWIN_ML = False
#------------------------------------------------------------------------
@ -75,6 +77,7 @@ from sabnzbd.postproc import PostProcessor
from sabnzbd.downloader import Downloader
from sabnzbd.assembler import Assembler
from sabnzbd.newzbin import Bookmarks, MSGIDGrabber
from sabnzbd.rating import Rating
import sabnzbd.misc as misc
import sabnzbd.powersup as powersup
from sabnzbd.dirscanner import DirScanner, ProcessArchiveFile, ProcessSingleFile
@ -319,6 +322,8 @@ def initialize(pause_downloader = False, clean_up = False, evalSched=False, repa
DirScanner()
MSGIDGrabber()
Rating()
URLGrabber()
@ -354,6 +359,8 @@ def start():
MSGIDGrabber.do.start()
Rating.do.start()
logging.debug('Starting urlgrabber')
URLGrabber.do.start()
@ -384,6 +391,13 @@ def halt():
except:
pass
logging.debug('Stopping rating')
Rating.do.stop()
try:
Rating.do.join()
except:
pass
logging.debug('Stopping dirscanner')
DirScanner.do.stop()
try:
@ -509,6 +523,7 @@ def save_state(flag=False):
BPSMeter.do.save()
rss.save()
Bookmarks.do.save()
Rating.do.save()
DirScanner.do.save()
PostProcessor.do.save()
#if flag:
@ -946,7 +961,8 @@ def load_admin(_id, remove=False, do_pickle=True):
if remove:
os.remove(path)
except:
logging.error(Ta('Loading %s failed'), path)
excepterror = str(sys.exc_info()[0])
logging.error(Ta('Loading %s failed with error %s'), path, excepterror)
logging.info("Traceback: ", exc_info = True)
return None
@ -1037,6 +1053,9 @@ def check_all_tasks():
if not MSGIDGrabber.do.isAlive():
logging.info('Restarting crashed newzbin')
MSGIDGrabber.do.__init__()
if not Rating.do.isAlive():
logging.info('Restarting crashed rating')
Rating.do.__init__()
if not sabnzbd.scheduler.sched_check():
logging.info('Restarting crashed scheduler')
sabnzbd.scheduler.init()
@ -1051,12 +1070,15 @@ def check_all_tasks():
return True
def pid_file(pid_path=None, port=0):
def pid_file(pid_path=None, pid_file= None, port=0):
""" Create or remove pid file
"""
global DIR_PID
if not sabnzbd.WIN32 and pid_path and pid_path.startswith('/'):
DIR_PID = os.path.join(pid_path, 'sabnzbd-%s.pid' % port)
if not sabnzbd.WIN32:
if pid_path and pid_path.startswith('/'):
DIR_PID = os.path.join(pid_path, 'sabnzbd-%s.pid' % port)
elif pid_file and pid_file.startswith('/'):
DIR_PID = pid_file
if DIR_PID:
try:

105
sabnzbd/api.py

@ -58,6 +58,7 @@ from sabnzbd.postproc import PostProcessor
from sabnzbd.articlecache import ArticleCache
from sabnzbd.utils.servertests import test_nntp_server_dict
from sabnzbd.newzbin import Bookmarks
from sabnzbd.rating import Rating
from sabnzbd.bpsmeter import BPSMeter
from sabnzbd.database import build_history_info, unpack_history_info, get_history_handle
import sabnzbd.growler
@ -177,10 +178,11 @@ def _api_queue_delete_nzf(output, value, kwargs):
def _api_queue_rename(output, value, kwargs):
""" API: accepts output, value(=old name), value2(=new name) """
""" API: accepts output, value(=old name), value2(=new name), value3(=password) """
value2 = kwargs.get('value2')
value3 = kwargs.get('value3')
if value and value2:
NzbQueue.do.change_name(value, special_fixer(value2))
NzbQueue.do.change_name(value, special_fixer(value2), special_fixer(value3))
return report(output)
else:
return report(output, _MSG_NO_VALUE2)
@ -269,6 +271,25 @@ def _api_queue_default(output, value, kwargs):
else:
return report(output, _MSG_NOT_IMPLEMENTED)
def _api_queue_rating(output, value, kwargs):
""" API: accepts output, value(=nzo_id), type, setting, detail """
vote_map = {'up': Rating.VOTE_UP, 'down': Rating.VOTE_DOWN}
flag_map = {'spam': Rating.FLAG_SPAM, 'encrypted': Rating.FLAG_ENCRYPTED, 'expired': Rating.FLAG_EXPIRED, 'other': Rating.FLAG_OTHER, 'comment': Rating.FLAG_COMMENT}
type = kwargs.get('type')
setting = kwargs.get('setting')
if value:
try:
video = setting if type == 'video' and setting != "-" else None
audio = setting if type == 'audio' and setting != "-" else None
vote = vote_map[setting] if type == 'vote' else None
flag = flag_map[setting] if type == 'flag' else None
if cfg.rating_enable():
Rating.do.update_user_rating(value, video, audio, vote, flag, kwargs.get('detail'))
return report(output)
except:
return report(output, _MSG_BAD_SERVER_PARMS)
else:
return report(output, _MSG_NO_VALUE)
#------------------------------------------------------------------------------
def _api_options(name, output, kwargs):
@ -320,8 +341,11 @@ def _api_retry(name, output, kwargs):
if name is None or isinstance(name, str) or isinstance(name, unicode):
name = kwargs.get('nzbfile')
if retry_job(value, name):
return report(output)
nzo_id = retry_job(value, name)
if nzo_id:
if isinstance(nzo_id, list):
nzo_id = nzo_id[0]
return report(output, keyword='', data={'status' : True, 'nzo_id' : nzo_id})
else:
return report(output, _MSG_NO_ITEM)
@ -632,6 +656,18 @@ def _api_watched_now(name, output, kwargs):
return report(output)
def _api_resume_pp(name, output, kwargs):
""" API: accepts output """
PostProcessor.do.paused = False
return report(output)
def _api_pause_pp(name, output, kwargs):
""" API: accepts output """
PostProcessor.do.paused = True
return report(output)
def _api_rss_now(name, output, kwargs):
""" API: accepts output """
# Run RSS scan async, because it can take a long time
@ -650,7 +686,8 @@ def _api_test_email(name, output, kwargs):
pack['unpack'] = ['action 1', 'action 2']
res = sabnzbd.emailer.endjob('I had a d\xe8ja vu', 123, 'unknown', True,
os.path.normpath(os.path.join(cfg.complete_dir.get_path(), '/unknown/I had a d\xe8ja vu')),
123*MEBI, None, pack, 'my_script', 'Line 1\nLine 2\nLine 3\nd\xe8ja vu\n', 0)
123*MEBI, None, pack, 'my_script', 'Line 1\nLine 2\nLine 3\nd\xe8ja vu\n', 0,
test=kwargs)
if res == 'Email succeeded':
res = None
return report(output, error=res)
@ -658,7 +695,7 @@ def _api_test_email(name, output, kwargs):
def _api_test_notif(name, output, kwargs):
""" API: send a test notification, return result """
logging.info("Sending test notification")
res = sabnzbd.growler.send_notification('SABnzbd', T('Test Notification'), 'other', wait=True)
res = sabnzbd.growler.send_notification('SABnzbd', T('Test Notification'), 'other', wait=True, test=kwargs)
return report(output, error=res)
def _api_undefined(name, output, kwargs):
@ -757,8 +794,22 @@ def _api_config_undefined(output, kwargs):
return report(output, _MSG_NOT_IMPLEMENTED)
def _api_server_stats(name, output, kwargs):
""" API: accepts output """
sum_t, sum_m, sum_w, sum_d = BPSMeter.do.get_sums()
stats = {'total': sum_t, 'month': sum_m, 'week': sum_w, 'day': sum_d}
stats['servers'] = {}
for svr in config.get_servers():
t, m, w, d = BPSMeter.do.amounts(svr)
stats['servers'][svr] = {'total': t or 0, 'month': m or 0, 'week': w or 0, 'day': d or 0}
return report(output, keyword='', data=stats)
#------------------------------------------------------------------------------
_api_table = {
'server_stats' : _api_server_stats,
'get_config' : _api_get_config,
'set_config' : _api_set_config,
'del_config' : _api_del_config,
@ -795,6 +846,8 @@ _api_table = {
'rescan' : _api_rescan,
'eval_sort' : _api_eval_sort,
'watched_now' : _api_watched_now,
'resume_pp' : _api_resume_pp,
'pause_pp' : _api_pause_pp,
'rss_now' : _api_rss_now,
'browse' : _api_browse,
'reset_quota' : _api_reset_quota,
@ -811,7 +864,8 @@ _api_queue_table = {
'pause' : _api_queue_pause,
'resume' : _api_queue_resume,
'priority' : _api_queue_priority,
'sort' : _api_queue_sort
'sort' : _api_queue_sort,
'rating' : _api_queue_rating
}
_api_config_table = {
@ -1036,6 +1090,7 @@ def build_queue(web_dir=None, root=None, verbose=False, prim=True, webdir='', ve
info['script_list'] = list_scripts()
info['cat_list'] = list_cats(output is None)
info['rating_enable'] = bool(cfg.rating_enable())
n = 0
found_active = False
@ -1131,7 +1186,8 @@ def build_queue(web_dir=None, root=None, verbose=False, prim=True, webdir='', ve
slot['percentage'] = "%s" % (int(((mb-mbleft) / mb) * 100))
slot['missing'] = missing
if Downloader.do.paused or Downloader.do.postproc or status not in (Status.DOWNLOADING, Status.QUEUED):
if (Downloader.do.paused or Downloader.do.postproc or status not in (Status.DOWNLOADING, Status.QUEUED)) \
and priority != TOP_PRIORITY:
slot['timeleft'] = '0:00:00'
slot['eta'] = 'unknown'
else:
@ -1204,8 +1260,13 @@ def build_queue(web_dir=None, root=None, verbose=False, prim=True, webdir='', ve
slot['finished'] = finished
slot['active'] = active
slot['queued'] = queued
rating = Rating.do.get_rating_by_nzo(nzo_id)
slot['has_rating'] = rating is not None
if rating:
slot['rating_avg_video'] = rating.avg_video
slot['rating_avg_audio'] = rating.avg_audio
if (start <= n and n < start + limit) or not limit:
slotinfo.append(slot)
n += 1
@ -1406,6 +1467,7 @@ def rss_qstatus():
bytes = pnfo[PNFO_BYTES_FIELD] / MEBI
mbleft = (bytesleft / MEBI)
mb = (bytes / MEBI)
nzo_id = pnfo[PNFO_NZO_ID_FIELD]
if mb == mbleft:
@ -1423,6 +1485,8 @@ def rss_qstatus():
else:
item.link = "http://%s:%s/sabnzbd/history" % ( \
cfg.cherryhost(), cfg.cherryport() )
item.guid = nzo_id
status_line = []
status_line.append('<tr>')
#Total MB/MB left
@ -1464,10 +1528,10 @@ def retry_job(job, new_nzb):
history_db = cherrypy.thread_data.history_db
path = history_db.get_path(job)
if path:
repair_job(platform_encode(path), new_nzb)
nzo_id = repair_job(platform_encode(path), new_nzb)
history_db.remove_history(job)
return True
return False
return nzo_id
return None
#------------------------------------------------------------------------------
@ -1594,6 +1658,7 @@ def build_header(prim, webdir=''):
header['quota'] = to_units(BPSMeter.do.quota)
header['have_quota'] = bool(BPSMeter.do.quota > 0.0)
header['left_quota'] = to_units(BPSMeter.do.left)
header['pp_pause_event'] = sabnzbd.scheduler.pp_pause_event()
status = ''
if Downloader.do.paused or Downloader.do.postproc:
@ -1749,6 +1814,20 @@ def build_history(start=None, limit=None, verbose=False, verbose_list=None, sear
if item['retry']:
retry_folders.append(path)
if Rating.do:
rating = Rating.do.get_rating_by_nzo(item['nzo_id'])
else:
rating = None
item['has_rating'] = rating is not None
if rating:
item['rating_avg_video'] = rating.avg_video
item['rating_avg_audio'] = rating.avg_audio
item['rating_avg_vote_up'] = rating.avg_vote_up
item['rating_avg_vote_down'] = rating.avg_vote_down
item['rating_user_video'] = rating.user_video
item['rating_user_audio'] = rating.user_audio
item['rating_user_vote'] = rating.user_vote
total_items += full_queue_size
fetched_items = len(items)

10
sabnzbd/articlecache.py

@ -24,6 +24,7 @@ import threading
import sabnzbd
from sabnzbd.decorators import synchronized
from sabnzbd.constants import GIGI
ARTICLE_LOCK = threading.Lock()
@ -31,6 +32,7 @@ class ArticleCache(object):
do = None
def __init__(self):
self.__cache_limit_org = 0
self.__cache_limit = 0
self.__cache_size = 0
@ -40,12 +42,16 @@ class ArticleCache(object):
@synchronized(ARTICLE_LOCK)
def cache_info(self):
return (len(self.__article_list), self.__cache_size, self.__cache_limit)
return (len(self.__article_list), self.__cache_size, self.__cache_limit_org)
@synchronized(ARTICLE_LOCK)
def new_limit(self, limit):
""" Called when cache limit changes """
self.__cache_limit = limit
self.__cache_limit_org = limit
if limit < 0:
self.__cache_limit = GIGI
else:
self.__cache_limit = min(limit, GIGI)
@synchronized(ARTICLE_LOCK)

105
sabnzbd/assembler.py

@ -24,6 +24,7 @@ import Queue
import binascii
import logging
import struct
import re
from threading import Thread
from time import sleep
try:
@ -43,13 +44,14 @@ from sabnzbd.postproc import PostProcessor
import sabnzbd.downloader
from sabnzbd.utils.rarfile import RarFile, is_rarfile
from sabnzbd.encoding import latin1, unicoder, is_utf8
from sabnzbd.rating import Rating
#------------------------------------------------------------------------------
class Assembler(Thread):
do = None # Link to the instance of this method
def __init__ (self, queue = None):
def __init__(self, queue=None):
Thread.__init__(self)
if queue:
@ -100,7 +102,7 @@ class Assembler(Thread):
# Pause without saving
sabnzbd.downloader.Downloader.do.pause(save=False)
except:
logging.error('Fatal error in Assembler', exc_info = True)
logging.error('Fatal error in Assembler', exc_info=True)
break
nzf.remove_admin()
@ -120,6 +122,31 @@ class Assembler(Thread):
nzo.fail_msg = T('Aborted, encryption detected')
import sabnzbd.nzbqueue
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
unwanted = rar_contains_unwanted_file(filepath)
if unwanted:
logging.warning(Ta('WARNING: In "%s" unwanted extension in RAR file. Unwanted file is %s '), latin1(nzo.final_name), unwanted)
logging.debug(Ta('Unwanted extension is in rar file %s'), filepath)
if cfg.action_on_unwanted_extensions() == 1 and nzo.unwanted_ext == 0:
logging.debug('Unwanted extension ... pausing')
nzo.unwanted_ext = 1
nzo.pause()
if cfg.action_on_unwanted_extensions() == 2:
logging.debug('Unwanted extension ... aborting')
nzo.fail_msg = T('Aborted, unwanted extension detected')
import sabnzbd.nzbqueue
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
filter, reason = nzo_filtered_by_rating(nzo)
if filter == 1:
logging.warning(Ta('WARNING: Paused job "%s" because of rating (%s)'), latin1(nzo.final_name), reason)
nzo.pause()
elif filter == 2:
logging.warning(Ta('WARNING: Aborted job "%s" because of rating (%s)'), latin1(nzo.final_name), reason)
nzo.fail_msg = T('Aborted, rating filter matched (%s)') % reason
import sabnzbd.nzbqueue
sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo)
nzf.completed = True
else:
sabnzbd.nzbqueue.NzbQueue.do.remove(nzo.nzo_id, add_to_history=False, cleanup=False)
@ -128,7 +155,7 @@ class Assembler(Thread):
def _assemble(nzf, path, dupe):
if os.path.exists(path):
unique_path = get_unique_path(path, create_dir = False)
unique_path = get_unique_filename(path)
if dupe:
path = unique_path
else:
@ -239,7 +266,7 @@ def GetMD5Hashes(fname, force=False):
table = {}
except:
logging.debug('QuickCheck parser crashed in file %s', fname)
logging.info('Traceback: ', exc_info = True)
logging.info('Traceback: ', exc_info=True)
table = {}
f.close()
@ -287,12 +314,19 @@ def ParseFilePacket(f, header):
return nothing
RE_SUBS = re.compile(r'\W+sub|subs|subpack|subtitle|subtitles(?![a-z])', re.I)
def is_cloaked(path, names):
""" Return True if this is likely to be a cloaked encrypted post """
fname = unicoder(os.path.split(path)[1]).lower()
fname = os.path.splitext(fname)[0]
for name in names:
name = unicoder(name.lower())
if fname == name or 'password' in name:
name = os.path.split(name.lower())[1]
name, ext = os.path.splitext(unicoder(name))
if ext == u'.rar' and fname.startswith(name) and (len(fname) - len(name)) < 8 and len(names) < 3 and not RE_SUBS.search(fname):
logging.debug('File %s is probably encrypted due to RAR with same name inside this RAR', fname)
return True
elif 'password' in name:
logging.debug('RAR %s is probably encrypted: "password" in filename %s', fname, name)
return True
return False
@ -314,3 +348,62 @@ def check_encrypted_rar(nzo, filepath):
logging.debug('RAR file %s cannot be inspected', filepath)
return encrypted
def rar_contains_unwanted_file(filepath):
# checks for unwanted extensions in the rar file 'filepath'
# ... unwanted extensions are defined in global variable cfg.unwanted_extensions()
# returns False if no unwanted extensions are found in the rar file
# returns name of file if unwanted extension is found in the rar file
unwanted = None
if is_rarfile(filepath):
#logging.debug('rar file to check: %s',filepath)
#logging.debug('unwanted extensions are: %s', cfg.unwanted_extensions())
try:
zf = RarFile(filepath, all_names=True)
#logging.debug('files in rar file: %s', zf.namelist())
for somefile in zf.namelist() :
logging.debug('file in rar file: %s', somefile)
if os.path.splitext(somefile)[1].replace('.', '').lower() in cfg.unwanted_extensions():
logging.debug('Unwanted file %s', somefile)
unwanted = somefile
zf.close()
except:
logging.debug('RAR file %s cannot be inspected.', filepath)
return unwanted
def nzo_filtered_by_rating(nzo):
if Rating.do and cfg.rating_enable() and cfg.rating_filter_enable() and (nzo.rating_filtered < 2):
rating = Rating.do.get_rating_by_nzo(nzo.nzo_id)
if rating is not None:
nzo.rating_filtered = 1
reason = rating_filtered(rating, nzo.filename.lower(), True)
if reason is not None: return (2, reason)
reason = rating_filtered(rating, nzo.filename.lower(), False)
if reason is not None: return (1, reason)
return (0, "")
def rating_filtered(rating, filename, abort):
def check_keyword(keyword):
clean_keyword = keyword.strip().lower()
return (len(clean_keyword) > 0) and (clean_keyword in filename)
audio = cfg.rating_filter_abort_audio() if abort else cfg.rating_filter_pause_audio()
video = cfg.rating_filter_abort_video() if abort else cfg.rating_filter_pause_video()
spam = cfg.rating_filter_abort_spam() if abort else cfg.rating_filter_pause_spam()
spam_confirm = cfg.rating_filter_abort_spam_confirm() if abort else cfg.rating_filter_pause_spam_confirm()
encrypted = cfg.rating_filter_abort_encrypted() if abort else cfg.rating_filter_pause_encrypted()
encrypted_confirm = cfg.rating_filter_abort_encrypted_confirm() if abort else cfg.rating_filter_pause_encrypted_confirm()
downvoted = cfg.rating_filter_abort_downvoted() if abort else cfg.rating_filter_pause_downvoted()
keywords = cfg.rating_filter_abort_keywords() if abort else cfg.rating_filter_pause_keywords()
if (video > 0) and (rating.avg_video > 0) and (rating.avg_video <= video):
return T('video')
if (audio > 0) and (rating.avg_audio > 0) and (rating.avg_audio <= audio):
return T('audio')
if (spam and ((rating.avg_spam_cnt > 0) or rating.avg_encrypted_confirm)) or (spam_confirm and rating.avg_spam_confirm):
return T('spam')
if (encrypted and ((rating.avg_encrypted_cnt > 0) or rating.avg_encrypted_confirm)) or (encrypted_confirm and rating.avg_encrypted_confirm):
return T('passworded')
if downvoted and (rating.avg_vote_up < rating.avg_vote_down):
return T('downvoted')
if any(check_keyword(k) for k in keywords.split(',')):
return T('keywords')
return None

35
sabnzbd/cfg.py

@ -80,7 +80,6 @@ email_dir = OptionDir('misc', 'email_dir', create=True)
email_rss = OptionBool('misc', 'email_rss', False)
version_check = OptionNumber('misc', 'check_new_rel', 1)
news_items = OptionBool('misc', 'news_items', True)
autobrowser = OptionBool('misc', 'auto_browser', True)
replace_illegal = OptionBool('misc', 'replace_illegal', True)
pre_script = OptionStr('misc', 'pre_script', 'None')
@ -88,12 +87,14 @@ start_paused = OptionBool('misc', 'start_paused', False)
enable_unrar = OptionBool('misc', 'enable_unrar', True)
enable_unzip = OptionBool('misc', 'enable_unzip', True)
enable_recursive = OptionBool('misc', 'enable_recursive', True)
enable_filejoin = OptionBool('misc', 'enable_filejoin', True)
enable_tsjoin = OptionBool('misc', 'enable_tsjoin', True)
enable_par_cleanup = OptionBool('misc', 'enable_par_cleanup', True)
never_repair = OptionBool('misc', 'never_repair', False)
ignore_unrar_dates = OptionBool('misc', 'ignore_unrar_dates', False)
overwrite_files = OptionBool('misc', 'overwrite_files', False)
flat_unpack = OptionBool('misc', 'flat_unpack', False)
par_option = OptionStr('misc', 'par_option', '', validation=no_nonsense)
nice = OptionStr('misc', 'nice', '', validation=no_nonsense)
@ -113,6 +114,28 @@ newzbin_unbookmark = OptionBool('newzbin', 'unbookmark', True)
bookmark_rate = OptionNumber('newzbin', 'bookmark_rate', 60, minval=15, maxval=24*60)
newzbin_url = OptionStr('newzbin', 'url', 'www.newzbin2.es')
rating_enable = OptionBool('misc', 'rating_enable', False)
rating_host = OptionStr('misc', 'rating_host', 'api.oznzb.com')
rating_api_key = OptionStr('misc', 'rating_api_key')
rating_feedback = OptionBool('misc', 'rating_feedback', True)
rating_filter_enable = OptionBool('misc', 'rating_filter_enable', False)
rating_filter_abort_audio = OptionNumber('misc', 'rating_filter_abort_audio', 0)
rating_filter_abort_video = OptionNumber('misc', 'rating_filter_abort_video', 0)
rating_filter_abort_encrypted = OptionBool('misc', 'rating_filter_abort_encrypted', False)
rating_filter_abort_encrypted_confirm = OptionBool('misc', 'rating_filter_abort_encrypted_confirm', False)
rating_filter_abort_spam = OptionBool('misc', 'rating_filter_abort_spam', False)
rating_filter_abort_spam_confirm = OptionBool('misc', 'rating_filter_abort_spam_confirm', False)
rating_filter_abort_downvoted = OptionBool('misc', 'rating_filter_abort_downvoted', False)
rating_filter_abort_keywords = OptionStr('misc', 'rating_filter_abort_keywords')
rating_filter_pause_audio = OptionNumber('misc', 'rating_filter_pause_audio', 0)
rating_filter_pause_video = OptionNumber('misc', 'rating_filter_pause_video', 0)
rating_filter_pause_encrypted = OptionBool('misc', 'rating_filter_pause_encrypted', False)
rating_filter_pause_encrypted_confirm = OptionBool('misc', 'rating_filter_pause_encrypted_confirm', False)
rating_filter_pause_spam = OptionBool('misc', 'rating_filter_pause_spam', False)
rating_filter_pause_spam_confirm = OptionBool('misc', 'rating_filter_pause_spam_confirm', False)
rating_filter_pause_downvoted = OptionBool('misc', 'rating_filter_pause_downvoted', False)
rating_filter_pause_keywords = OptionStr('misc', 'rating_filter_pause_keywords')
top_only = OptionBool('misc', 'top_only', False)
autodisconnect = OptionBool('misc', 'auto_disconnect', True)
queue_complete = OptionStr('misc', 'queue_complete')
@ -128,6 +151,7 @@ folder_rename = OptionBool('misc', 'folder_rename', True)
folder_max_length = OptionNumber('misc', 'folder_max_length', DEF_FOLDER_MAX, 20, 65000)
pause_on_pwrar = OptionBool('misc', 'pause_on_pwrar', True)
prio_sort_list = OptionList('misc', 'prio_sort_list')
enable_meta = OptionBool('misc', 'enable_meta', True)
safe_postproc = OptionBool('misc', 'safe_postproc', True)
empty_postproc = OptionBool('misc', 'empty_postproc', False)
@ -179,6 +203,7 @@ password_file = OptionDir('misc', 'password_file', '', create=False)
fsys_type = OptionNumber('misc', 'fsys_type', 0, 0, 2)
wait_for_dfolder = OptionBool('misc', 'wait_for_dfolder', False)
warn_empty_nzb = OptionBool('misc', 'warn_empty_nzb', True)
sanitize_safe = OptionBool('misc', 'sanitize_safe', False)
cherryhost = OptionStr('misc', 'host', DEF_HOST)
if sabnzbd.WIN32:
@ -204,6 +229,9 @@ web_color2 = OptionStr('misc', 'web_color2')
cleanup_list = OptionList('misc', 'cleanup_list')
warned_old_queue = OptionBool('misc', 'warned_old_queue', False)
unwanted_extensions = OptionList('misc', 'unwanted_extensions')
action_on_unwanted_extensions = OptionNumber('misc', 'action_on_unwanted_extensions', 0)
log_web = OptionBool('logging', 'enable_cherrypy_logging', False)
log_dir = OptionDir('misc', 'log_dir', 'logs', validation=validate_notempty)
log_level = OptionNumber('logging', 'log_level', 1, -1, 2)
@ -259,6 +287,9 @@ 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)
web_watchdog = OptionBool('misc', 'web_watchdog', False)
warn_dupl_jobs = OptionBool('misc', 'warn_dupl_jobs', True)
new_nzb_on_failure = OptionBool('misc', 'new_nzb_on_failure', False)
#------------------------------------------------------------------------------
# Set root folders for Folder config-items
@ -278,4 +309,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())
https_chain.set_root(admin_dir.get_path())

4
sabnzbd/config.py

@ -1012,6 +1012,10 @@ def compatibility_fix(cf):
del cf[old]
except KeyError:
pass
try:
cf['rating_host'] = cf['rating_host'].replace('www.oznzb.com', 'api.oznzb.com')
except KeyError:
pass
def newzbin_fix(cf):
""" Replace old newzbin links """

2
sabnzbd/database.py

@ -365,7 +365,7 @@ def build_history_info(nzo, storage='', downpath='', postproc_time=0, script_out
downloaded = nzo.bytes_downloaded
completeness = 0
fail_message = decode_factory(nzo.fail_msg)
url_info = nzo_info.get('more_info', '')
url_info = nzo_info.get('details', '') or nzo_info.get('more_info', '')
# Get the dictionary containing the stages and their unpack process
stages = decode_factory(nzo.unpack_info)

7
sabnzbd/dirscanner.py

@ -110,7 +110,6 @@ def ProcessArchiveFile(filename, path, pp=None, script=None, cat=None, catdir=No
return -1, []
name = re.sub(r'\[.*nzbmatrix.com\]', '', name)
name = os.path.basename(name)
name = misc.sanitize_foldername(name)
if data:
try:
nzo = nzbstuff.NzbObject(name, 0, pp, script, data, cat=cat, url=url,
@ -119,6 +118,7 @@ def ProcessArchiveFile(filename, path, pp=None, script=None, cat=None, catdir=No
nzo = None
if nzo:
nzo_ids.append(add_nzo(nzo))
nzo.update_rating()
zf.close()
try:
if not keep: os.remove(path)
@ -169,7 +169,9 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
if name:
name, cat = name_to_cat(name, catdir)
# The name is used as the name of the folder, so sanitize it using folder specific santization
name = misc.sanitize_foldername(name)
if not nzbname:
# Prevent embedded password from being damaged by sanitize and trimming
nzbname = os.path.split(name)[1]
try:
nzo = nzbstuff.NzbObject(name, 0, pp, script, data, cat=cat, priority=priority, nzbname=nzbname,
@ -189,6 +191,7 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
if nzo:
nzo_ids.append(add_nzo(nzo))
nzo.update_rating()
try:
if not keep: os.remove(path)
except:

3
sabnzbd/downloader.py

@ -277,6 +277,9 @@ class Downloader(Thread):
return True
return False
def nzo_servers(self, nzo):
return filter(nzo.server_in_try_list, self.servers)
def maybe_block_server(self, server):
from sabnzbd.nzbqueue import NzbQueue
if server.optional and server.active and (server.bad_cons/server.threads) > 3:

60
sabnzbd/emailer.py

@ -37,22 +37,43 @@ def errormsg(msg):
logging.error(latin1(msg))
return msg
################################################################################
# EMAIL_SEND
#
#
################################################################################
def send(message, recipient):
def send(message, email_to, test=None):
""" Send message if message non-empty and email-parms are set """
# we should not use CFG if we are testing. we should use values
# from UI instead.
if test:
email_server = test.get('email_server')
email_from = test.get('email_from')
email_account = test.get('email_account')
email_pwd = test.get('email_pwd')
if email_pwd and not email_pwd.replace('*', ''):
# If all stars, get stored password instead
email_pwd = cfg.email_pwd()
else:
email_server = cfg.email_server()
email_from = cfg.email_from()
email_account = cfg.email_account()
email_pwd = cfg.email_pwd()
# email_to is replaced at send_with_template, since it can be an array
if not message.strip('\n\r\t '):
return "Skipped empty message"
if cfg.email_server() and recipient and cfg.email_from():
if email_server and email_to and email_from:
message = _prepare_message(message)
server, port = split_host(cfg.email_server())
server, port = split_host(email_server)
if not port:
port = 25
@ -92,14 +113,20 @@ def send(message, recipient):
return errormsg(T('Failed to initiate TLS connection'))
# Authentication
if (cfg.email_account() != "") and (cfg.email_pwd() != ""):
if (email_account != "") and (email_pwd != ""):
try:
mailconn.login(cfg.email_account(), cfg.email_pwd())
mailconn.login(email_account, email_pwd)
except smtplib.SMTPHeloError:
return errormsg(T("The server didn't reply properly to the helo greeting"))
except smtplib.SMTPAuthenticationError:
return errormsg(T("Failed to authenticate to mail server"))
except smtplib.SMTPException:
return errormsg(T("No suitable authentication method was found"))
except:
return errormsg(T('Failed to authenticate to mail server'))
return errormsg(T("Unknown authentication failure in mail server"))
try:
mailconn.sendmail(cfg.email_from(), recipient, message)
mailconn.sendmail(email_from, email_to, message)
msg = None
except smtplib.SMTPHeloError:
msg = errormsg('The server didn\'t reply properly to the helo greeting.')
@ -139,7 +166,7 @@ def get_email_date():
################################################################################
from Cheetah.Template import Template
def send_with_template(prefix, parm):
def send_with_template(prefix, parm, test=None):
""" Send an email using template """
parm['from'] = cfg.email_from()
@ -166,15 +193,20 @@ def send_with_template(prefix, parm):
source = _decode_file(temp)
if source:
sent = True
if len(cfg.email_to()):
for recipient in cfg.email_to():
if test:
recipients = [ test.get('email_to') ]
else:
recipients = cfg.email_to()
if len(recipients):
for recipient in recipients:
parm['to'] = recipient
message = Template(source=source,
searchList=[parm],
filter=EmailFilter,
compilerSettings={'directiveStartToken': '<!--#',
'directiveEndToken': '#-->'})
ret = send(message.respond(), recipient)
ret = send(message.respond(), recipient, test)
del message
else:
ret = T('No recipients given, no email sent')
@ -187,7 +219,7 @@ def send_with_template(prefix, parm):
return ret
def endjob(filename, msgid, cat, status, path, bytes, fail_msg, stages, script, script_output, script_ret):
def endjob(filename, msgid, cat, status, path, bytes, fail_msg, stages, script, script_output, script_ret, test=None):
""" Send end-of-job email """
# Translate the stage names
@ -196,7 +228,7 @@ def endjob(filename, msgid, cat, status, path, bytes, fail_msg, stages, script,
xstages = {tr('stage-fail'): (fail_msg,)}
else:
xstages = {}
for stage in stages:
lines = []
for line in stages[stage]:
@ -219,7 +251,7 @@ def endjob(filename, msgid, cat, status, path, bytes, fail_msg, stages, script,
parm['size'] = "%sB" % to_units(bytes)
parm['end_time'] = time.strftime(time_format('%Y-%m-%d %H:%M:%S'), time.localtime(time.time()))
return send_with_template('email', parm)
return send_with_template('email', parm, test)
def rss_mail(feed, jobs):

5
sabnzbd/encoding.py

@ -119,7 +119,10 @@ def special_fixer(p):
return p
except:
# Now assume it's latin-1
return p.decode('Latin-1').encode('utf-8')
try:
return p.decode('Latin-1').encode('utf-8')
except:
return p
else:
return p

68
sabnzbd/growler.py

@ -102,25 +102,39 @@ def have_ntfosd():
#------------------------------------------------------------------------------
def send_notification(title , msg, gtype, wait=False):
def send_notification(title , msg, gtype, wait=False, test=None):
""" Send Notification message
Return '' when OK, otherwise an error string
"""
res = []
if gtype in sabnzbd.cfg.notify_classes() or wait:
if sabnzbd.DARWIN_ML and sabnzbd.cfg.ncenter_enable():
# support testing values from UI
if (test):
ncenter_enable = test.get('ncenter_enable') == 'true'
ntfosd_enable = test.get('ntfosd_enable') == 'true'
growl_enable = test.get('growl_enable') == 'true'
growl_server = test.get('growl_server') or None
else:
ncenter_enable = sabnzbd.cfg.ncenter_enable()
ntfosd_enable = sabnzbd.cfg.ntfosd_enable()
growl_enable = sabnzbd.cfg.growl_enable()
growl_server = sabnzbd.cfg.growl_server()
if sabnzbd.DARWIN_ML and 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():
if growl_enable:
if _HAVE_CLASSIC_GROWL and not growl_server:
return send_local_growl(title, msg, gtype)
else:
if wait:
res.append(send_growl(title, msg, gtype))
# we only test with wait=True
res.append(send_growl(title, msg, gtype, test))
else:
res.append('ok')
Thread(target=send_growl, args=(title, msg, gtype)).start()
time.sleep(0.5)
if have_ntfosd():
if ntfosd_enable and have_ntfosd():
res.append(send_notify_osd(title, msg))
return ' / '.join([r for r in res if r])
@ -135,11 +149,19 @@ def reset_growl():
#------------------------------------------------------------------------------
def register_growl():
def register_growl(test=None):
""" Register this app with Growl
"""
error = None
host, port = sabnzbd.misc.split_host(sabnzbd.cfg.growl_server())
if (test):
growl_server = test.get('growl_server')
growl_password = test.get('growl_password')
else:
growl_server = sabnzbd.cfg.growl_server()
growl_password = sabnzbd.cfg.growl_password()
host, port = sabnzbd.misc.split_host(growl_server)
sys_name = hostname(host)
@ -153,7 +175,7 @@ def register_growl():
notifications = [Tx(NOTIFICATION[key]) for key in NOTIFY_KEYS],
hostname = host or 'localhost',
port = port or 23053,
password = sabnzbd.cfg.growl_password() or None
password = growl_password or None
)
try:
@ -182,15 +204,19 @@ def register_growl():
#------------------------------------------------------------------------------
def send_growl(title , msg, gtype):
def send_growl(title , msg, gtype, test=None):
""" Send Growl message
"""
global _GROWL, _GROWL_REG
for n in (0, 1):
if not _GROWL_REG: _GROWL = None
if test:
reset_growl()
if not _GROWL:
_GROWL, error = register_growl()
_GROWL, error = register_growl(test)
if _GROWL:
assert isinstance(_GROWL, GrowlNotifier)
_GROWL_REG = True
@ -204,22 +230,27 @@ def send_growl(title , msg, gtype):
description = unicoder(msg),
)
if ret is None or isinstance(ret, bool):
return None
result = None
elif ret[0] == '401':
_GROWL = False
else:
logging.debug('Growl error %s', ret)
return 'Growl error %s', ret
result = 'Growl error %s', ret
except socket.error, err:
error = 'Growl error %s' % err
logging.debug(error)
return error
result = error
except:
error = 'Growl error (unknown)'
logging.debug(error)
return error
result = error
else:
return error
result = error
if test:
reset_growl()
return result
return None
#------------------------------------------------------------------------------
@ -292,11 +323,12 @@ def send_notification_center(title, msg, gtype):
tool = ncenter_path()
if tool:
try:
command = [tool, '-title', title, '-message', msg, '-group', Tx(NOTIFICATION.get(gtype, 'other'))]
command = [tool, '-title', title, '-message', msg, '-group', Tx(NOTIFICATION.get(gtype, 'other')),
'-sender', 'org.sabnzbd.team']
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
output = proc.stdout.read()
proc.wait()
if 'Notification delivered' in output:
if 'Notification delivered' in output or 'Removing previously' in output:
output = ''
except:
logging.info('Cannot run notifier "%s"', tool)

87
sabnzbd/interface.py

@ -39,6 +39,7 @@ from sabnzbd.misc import real_path, to_units, \
from sabnzbd.panic import panic_old_queue
from sabnzbd.newswrapper import GetServerParms
from sabnzbd.newzbin import Bookmarks
from sabnzbd.rating import Rating
from sabnzbd.bpsmeter import BPSMeter
from sabnzbd.encoding import TRANS, xml_name, LatinFilter, unicoder, special_fixer, \
platform_encode, latin1, encode_for_xml
@ -461,6 +462,12 @@ class MainPage(object):
retry_job(kwargs.get('job'), kwargs.get('nzbfile'))
raise dcRaiser(self.__root, kwargs)
@cherrypy.expose
def robots_txt(self):
""" Keep web crawlers out """
cherrypy.response.headers['Content-Type'] = 'text/plain'
return 'User-agent: *\nDisallow: /\n'
#------------------------------------------------------------------------------
class NzoPage(object):
@ -534,12 +541,19 @@ class NzoPage(object):
cat = pnfo[PNFO_EXTRA_FIELD1]
if not cat:
cat = 'None'
filename = xml_name(nzo.final_name_pw_clean)
filename_pw = xml_name(nzo.final_name_pw_clean)
filename = xml_name(nzo.final_name)
if nzo.password:
password = xml_name(nzo.password).replace('"', '&quot;')
else:
password = ''
priority = pnfo[PNFO_PRIORITY_FIELD]
slot['nzo_id'] = str(nzo_id)
slot['cat'] = cat
slot['filename'] = filename
slot['filename'] = filename_pw
slot['filename_clean'] = filename
slot['password'] = password or ''
slot['script'] = script
slot['priority'] = str(priority)
slot['unpackopts'] = str(unpackopts)
@ -587,6 +601,9 @@ class NzoPage(object):
def save_details(self, nzo_id, args, kwargs):
index = kwargs.get('index', None)
name = kwargs.get('name', None)
password = kwargs.get('password', None)
if password == "":
password = None
pp = kwargs.get('pp', None)
script = kwargs.get('script', None)
cat = kwargs.get('cat', None)
@ -596,9 +613,9 @@ class NzoPage(object):
if index != None:
NzbQueue.do.switch(nzo_id, index)
if name != None:
NzbQueue.do.change_name(nzo_id, special_fixer(name))
NzbQueue.do.change_name(nzo_id, special_fixer(name), password)
if cat != None:
NzbQueue.do.change_cat(nzo_id,cat)
NzbQueue.do.change_cat(nzo_id, cat, priority)
if script != None:
NzbQueue.do.change_script(nzo_id,script)
if pp != None:
@ -864,7 +881,8 @@ class HistoryPage(object):
self.__verbose_list = []
self.__failed_only = False
self.__prim = prim
self.__edit_rating = None
@cherrypy.expose
def index(self, **kwargs):
if not check_access(): return Protected()
@ -880,6 +898,8 @@ class HistoryPage(object):
history['isverbose'] = self.__verbose
history['failed_only'] = failed_only
history['rating_enable'] = bool(cfg.rating_enable())
if cfg.newzbin_username() and cfg.newzbin_password():
history['newzbinDetails'] = True
@ -894,6 +914,12 @@ class HistoryPage(object):
history['lines'], history['fetched'], history['noofslots'] = build_history(limit=limit, start=start, verbose=self.__verbose, verbose_list=self.__verbose_list, search=search, failed_only=failed_only)
for line in history['lines']:
if self.__edit_rating is not None and line.get('nzo_id') == self.__edit_rating:
line['edit_rating'] = True
else:
line['edit_rating'] = ''
if search:
history['search'] = escape(search)
else:
@ -1012,6 +1038,30 @@ class HistoryPage(object):
del_hist_job(job, del_files=True)
raise dcRaiser(self.__root, kwargs)
@cherrypy.expose
def show_edit_rating(self, **kwargs):
msg = check_session(kwargs)
if msg: return msg
self.__edit_rating = kwargs.get('job');
raise queueRaiser(self.__root, kwargs)
@cherrypy.expose
def action_edit_rating(self, **kwargs):
flag_map = {'spam': Rating.FLAG_SPAM, 'encrypted': Rating.FLAG_ENCRYPTED, 'expired': Rating.FLAG_EXPIRED}
msg = check_session(kwargs)
if msg: return msg
try:
if kwargs.get('send'):
video = kwargs.get('video') if kwargs.get('video') != "-" else None
audio = kwargs.get('audio') if kwargs.get('audio') != "-" else None
flag = flag_map.get(kwargs.get('rating_flag'))
detail = kwargs.get('expired_host') if kwargs.get('expired_host') != '<Host>' else None
if cfg.rating_enable():
Rating.do.update_user_rating(kwargs.get('job'), video, audio, flag, detail)
except:
pass
self.__edit_rating = None;
raise queueRaiser(self.__root, kwargs)
#------------------------------------------------------------------------------
class ConfigPage(object):
@ -1044,7 +1094,6 @@ class ConfigPage(object):
for svr in config.get_servers():
new[svr] = {}
conf['servers'] = new
conf['news_items'] = cfg.news_items()
conf['folders'] = sabnzbd.nzbqueue.scan_jobs(all=False, action=False)
@ -1163,7 +1212,15 @@ SWITCH_LIST = \
'ignore_samples', 'pause_on_post_processing', 'quick_check', 'nice', 'ionice',
'ssl_type', 'pre_script', 'pause_on_pwrar', 'ampm', 'sfv_check', 'folder_rename',
'unpack_check', 'quota_size', 'quota_day', 'quota_resume', 'quota_period',
'pre_check', 'max_art_tries', 'max_art_opt', 'fail_hopeless'
'pre_check', 'max_art_tries', 'max_art_opt', 'fail_hopeless',
'rating_enable', 'rating_api_key', 'rating_feedback', 'rating_filter_enable',
'rating_filter_abort_audio', 'rating_filter_abort_video', 'rating_filter_abort_encrypted',
'rating_filter_abort_encrypted_confirm', 'rating_filter_abort_spam', 'rating_filter_abort_spam_confirm',
'rating_filter_abort_downvoted', 'rating_filter_abort_keywords',
'rating_filter_pause_audio', 'rating_filter_pause_video', 'rating_filter_pause_encrypted',
'rating_filter_pause_encrypted_confirm', 'rating_filter_pause_spam', 'rating_filter_pause_spam_confirm',
'rating_filter_pause_downvoted', 'rating_filter_pause_keywords',
'unwanted_extensions', 'action_on_unwanted_extensions'
)
#------------------------------------------------------------------------------
@ -1186,6 +1243,7 @@ class ConfigSwitches(object):
for kw in SWITCH_LIST:
conf[kw] = config.get_config('misc', kw)()
conf['unwanted_extensions'] = cfg.unwanted_extensions.get_string()
conf['script_list'] = list_scripts() or ['None']
conf['have_ampm'] = HAVE_AMPM
@ -1202,6 +1260,8 @@ class ConfigSwitches(object):
for kw in SWITCH_LIST:
item = config.get_config('misc', kw)
value = platform_encode(kwargs.get(kw))
if kw == 'unwanted_extensions' and value:
value = value.lower().replace('.', '')
msg = item.set(value)
if msg:
return badParameterResponse(msg)
@ -1215,17 +1275,18 @@ class ConfigSwitches(object):
SPECIAL_BOOL_LIST = \
( 'start_paused', 'no_penalties', 'ignore_wrong_unrar', 'create_group_folders',
'queue_complete_pers', 'api_warnings', 'allow_64bit_tools', 'par2_multicore',
'never_repair', 'allow_streaming', 'ignore_unrar_dates', 'rss_filenames', 'news_items',
'never_repair', 'allow_streaming', 'ignore_unrar_dates', 'rss_filenames',
'osx_menu', 'osx_speed', 'win_menu', 'uniconfig', 'use_pickle', 'allow_incomplete_nzb',
'randomize_server_ip', 'no_ipv6', 'keep_awake', 'overwrite_files', 'empty_postproc',
'web_watchdog', 'wait_for_dfolder', 'warn_empty_nzb'
'web_watchdog', 'wait_for_dfolder', 'warn_empty_nzb', 'enable_recursive', 'sanitize_safe',
'enable_meta', 'flat_unpack', 'warn_dupl_jobs', 'new_nzb_on_failure'
)
SPECIAL_VALUE_LIST = \
( 'size_limit', 'folder_max_length', 'fsys_type', 'movie_rename_limit', 'nomedia_marker',
'req_completion_rate', 'wait_ext_drive', 'history_limit', 'show_sysload', 'ipv6_servers'
)
SPECIAL_LIST_LIST = \
( 'rss_odd_titles', 'prio_sort_list'
( 'rss_odd_titles', 'prio_sort_list', 'rating_host'
)
class ConfigSpecial(object):
@ -2180,6 +2241,7 @@ class ConfigCats(object):
for cat in sorted(categories.keys()):
slot = categories[cat].get_dict()
slot['name'] = cat
slot['newzbin'] = slot['newzbin'].replace('"', '&quot;')
slotinfo.append(slot)
slotinfo.insert(1, empty)
conf['slotinfo'] = slotinfo
@ -2527,7 +2589,7 @@ def GetRssLog(feed):
if sabnzbd.rss.special_rss_site(url):
nzbname = ""
else:
nzbname = xml_name(sanitize_foldername(job.get('title', '')))
nzbname = xml_name(job.get('title', ''))
return url, \
title, \
'*' * int(job.get('status', '').endswith('*')), \
@ -2544,7 +2606,7 @@ def GetRssLog(feed):
# 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))
dnames.sort(lambda x, y: int(jobs[y].get('time', 0) - jobs[x].get('time', 0)))
done = [xml_name(jobs[job]['title']) for job in dnames]
@ -2729,6 +2791,7 @@ def rss_history(url, limit=50, search=None):
item.link = history['url_info']
else:
item.link = url
item.guid = history['nzo_id']
stageLine = []
for stage in history['stage_log']:

269
sabnzbd/misc.py

@ -31,6 +31,7 @@ import socket
import time
import glob
import stat
import Queue
try:
socket.ssl
_HAVE_SSL = True
@ -225,7 +226,7 @@ FL_LEGAL = CH_LEGAL + "-''"
uFL_ILLEGAL = FL_ILLEGAL.decode('latin-1')
uFL_LEGAL = FL_LEGAL.decode('latin-1')
def sanitize_foldername(name):
def sanitize_foldername(name, limit=True):
""" Return foldername with dodgy chars converted to safe ones
Remove any leading and trailing dot and space characters
"""
@ -238,6 +239,11 @@ def sanitize_foldername(name):
illegal = FL_ILLEGAL
legal = FL_LEGAL
if cfg.sanitize_safe():
# Remove all bad Windows chars too
illegal += r'\/<>?*|":'
legal += r'++{}!@#`;'
repl = cfg.replace_illegal()
lst = []
for ch in name.strip():
@ -249,16 +255,38 @@ def sanitize_foldername(name):
lst.append(ch)
name = ''.join(lst)
name = name.strip('. ')
name = name.strip()
if name != '.' and name != '..':
name = name.rstrip('.')
if not name:
name = 'unknown'
maxlen = cfg.folder_max_length()
if len(name) > maxlen:
if limit and len(name) > maxlen:
name = name[:maxlen]
return name
#------------------------------------------------------------------------------
def sanitize_and_trim_path(path):
""" Remove illegal characters and trim element size
"""
path = path.strip()
path = path.replace('\\', '/')
parts = path.split('/')
if sabnzbd.WIN32 and len(parts[0]) == 2 and ':' in parts[0]:
new_path = parts[0] + '/'
parts.pop(0)
elif path.startswith('//'):
new_path = '//'
elif path.startswith('/'):
new_path = '/'
else:
new_path = ''
for part in parts:
new_path = os.path.join(new_path, sanitize_foldername(part))
return os.path.abspath(os.path.normpath(new_path))
#------------------------------------------------------------------------------
def flag_file(path, flag, create=False):
@ -869,10 +897,13 @@ def get_filepath(path, nzo, filename):
nzo.created = True
fPath = os.path.join(os.path.join(path, dName), filename)
fPath, ext = os.path.splitext(fPath)
n = 0
while True:
fullPath = fPath
if n: fullPath += '.' + str(n)
if n:
fullPath = "%s.%d%s" % (fPath, n, ext)
else:
fullPath = fPath + ext
if os.path.exists(fullPath):
n = n + 1
else:
@ -1229,6 +1260,13 @@ def ip_extract():
def renamer(old, new):
""" Rename file/folder with retries for Win32 """
# Sanitize last part of new name
path, name = os.path.split(new)
# Use the more stringent folder rename to end up with a nicer name,
# but do not trim size
new = os.path.join(path, sanitize_foldername(name, False))
logging.debug('Renaming "%s" to "%s"', old, new)
if sabnzbd.WIN32:
retries = 15
while retries > 0:
@ -1236,6 +1274,7 @@ def renamer(old, new):
os.rename(old, new)
return
except WindowsError, err:
logging.debug('Error renaming "%s" to "%s" <%s>', old, new, err)
if err[0] == 32:
logging.debug('Retry rename %s to %s', old, new)
retries -= 1
@ -1375,3 +1414,223 @@ def set_permissions(path, recursive=True):
set_chmod(path, umask, report)
else:
set_chmod(path, umask_file, report)
#------------------------------------------------------------------------------
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
# Passes Python2.7's test suite and incorporates all the latest updates.
class OrderedDict(dict):
# An inherited dict maps keys to values.
# The inherited dict provides __getitem__, __len__, __contains__, and get.
# The remaining methods are order-aware.
# Big-O running times for all methods are the same as for regular dictionaries.
# The internal self.__map dictionary maps keys to links in a doubly linked list.
# The circular doubly linked list starts and ends with a sentinel element.
# The sentinel element never gets deleted (this simplifies the algorithm).
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
def __init__(self, *args, **kwds):
'''Initialize an ordered dictionary. Signature is the same as for
regular dictionaries, but keyword arguments are not recommended
because their insertion order is arbitrary.
'''
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__root
except AttributeError:
self.__root = root = [] # sentinel node
root[:] = [root, root, None]
self.__map = {}
self.__update(*args, **kwds)
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
# Setting a new item creates a new link which goes at the end of the linked
# list, and the inherited dictionary is updated with the new key/value pair.
if key not in self:
root = self.__root
last = root[0]
last[1] = root[0] = self.__map[key] = [last, root, key]
dict_setitem(self, key, value)
def __delitem__(self, key, dict_delitem=dict.__delitem__):
# Deleting an existing item uses self.__map to find the link which is
# then removed by updating the links in the predecessor and successor nodes.
dict_delitem(self, key)
link_prev, link_next, key = self.__map.pop(key)
link_prev[1] = link_next
link_next[0] = link_prev
def __iter__(self):
root = self.__root
curr = root[1]
while curr is not root:
yield curr[2]
curr = curr[1]
def __reversed__(self):
root = self.__root
curr = root[0]
while curr is not root:
yield curr[2]
curr = curr[0]
def clear(self):
try:
for node in self.__map.itervalues():
del node[:]
root = self.__root
root[:] = [root, root, None]
self.__map.clear()
except AttributeError:
pass
dict.clear(self)
def popitem(self, last=True):
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
Pairs are returned in LIFO order if last is true or FIFO order if false.
'''
if not self:
raise KeyError('dictionary is empty')
root = self.__root
if last:
link = root[0]
link_prev = link[0]
link_prev[1] = root
root[0] = link_prev
else:
link = root[1]
link_next = link[1]
root[1] = link_next
link_next[0] = root
key = link[2]
del self.__map[key]
value = dict.pop(self, key)
return key, value
# -- the following methods do not depend on the internal structure --
def keys(self):
return list(self)
def values(self):
return [self[key] for key in self]
def items(self):
return [(key, self[key]) for key in self]
def iterkeys(self):
return iter(self)
def itervalues(self):
for k in self:
yield self[k]
def iteritems(self):
for k in self:
yield (k, self[k])
def update(*args, **kwds):
if len(args) > 2:
raise TypeError('update() takes at most 2 positional '
'arguments (%d given)' % (len(args),))
elif not args:
raise TypeError('update() takes at least 1 argument (0 given)')
self = args[0]
# Make progressively weaker assumptions about "other"
other = ()
if len(args) == 2:
other = args[1]
if isinstance(other, dict):
for key in other:
self[key] = other[key]
elif hasattr(other, 'keys'):
for key in other.keys():
self[key] = other[key]
else:
for key, value in other:
self[key] = value
for key, value in kwds.items():
self[key] = value
__update = update # let subclasses override update without breaking __init__
__marker = object()
def pop(self, key, default=__marker):
if key in self:
result = self[key]
del self[key]
return result
if default is self.__marker:
raise KeyError(key)
return default
def setdefault(self, key, default=None):
if key in self:
return self[key]
self[key] = default
return default
def __repr__(self, _repr_running={}):
call_key = id(self), _get_ident()
if call_key in _repr_running:
return '...'
_repr_running[call_key] = 1
try:
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
finally:
del _repr_running[call_key]
def __reduce__(self):
items = [[k, self[k]] for k in self]
inst_dict = vars(self).copy()
for k in vars(OrderedDict()):
inst_dict.pop(k, None)
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def copy(self):
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
if isinstance(other, OrderedDict):
return len(self)==len(other) and self.items() == other.items()
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other
# -- the following methods are only used in Python 2.7 --
def viewkeys(self):
return KeysView(self)
def viewvalues(self):
return ValuesView(self)
def viewitems(self):
return ItemsView(self)
#------------------------------------------------------------------------------
# A queue which ignores duplicates but maintains ordering
class OrderedSetQueue(Queue.Queue):
def _init(self, maxsize):
self.maxsize = maxsize
self.queue = OrderedDict()
def _put(self, item):
self.queue[item] = None
def _get(self):
return self.queue.popitem()[0]

138
sabnzbd/newsunpack.py

@ -26,6 +26,7 @@ import subprocess
import logging
from time import time
import binascii
import shutil
import sabnzbd
from sabnzbd.encoding import TRANS, UNTRANS, unicode2local, name_fixer, \
@ -93,7 +94,7 @@ def find_programs(curdir):
if sabnzbd.DARWIN:
try:
os_version = subprocess.Popen("sw_vers -productVersion", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).stdout.read()
os_version = run_simple('sw_vers -productVersion')
#par2-sl from Macpar Deluxe 4.1 is only 10.6 and later
if int(os_version.split('.')[1]) >= 6:
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'osx/par2/par2-sl')
@ -102,7 +103,10 @@ def find_programs(curdir):
except:
sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'osx/par2/par2-classic')
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'osx/unrar/unrar')
if sabnzbd.DARWIN_INTEL:
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'osx/unrar/unrar')
else:
sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'osx/unrar/unrar-ppc')
if sabnzbd.WIN32:
if sabnzbd.WIN64 and cfg.allow_64bit_tools.get():
@ -128,19 +132,22 @@ def find_programs(curdir):
sabnzbd.newsunpack.RAR_PROBLEM = not unrar_check(sabnzbd.newsunpack.RAR_COMMAND)
#------------------------------------------------------------------------------
def external_processing(extern_proc, complete_dir, filename, msgid, nicename, cat, group, status):
def external_processing(extern_proc, complete_dir, filename, msgid, nicename, cat, group, status, failure_url):
""" Run a user postproc script, return console output and exit value
"""
command = [str(extern_proc), str(complete_dir), str(filename),
str(nicename), str(msgid), str(cat), str(group), str(status)]
if failure_url:
command.extend(str(failure_url))
if extern_proc.endswith('.py') and (sabnzbd.WIN32 or not os.access(extern_proc, os.X_OK)):
command.insert(0, 'python')
stup, need_shell, command, creationflags = build_command(command)
env = fix_env()
logging.info('Running external script %s(%s, %s, %s, %s, %s, %s, %s)',
extern_proc, complete_dir, filename, nicename, msgid, cat, group, status)
logging.info('Running external script %s(%s, %s, %s, %s, %s, %s, %s, %s)',
extern_proc, complete_dir, filename, nicename, msgid, cat, group, status, failure_url)
try:
p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE,
@ -187,7 +194,8 @@ def unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, joinables, zi
rerun = False
newfiles = []
error = False
error = 0
new_joins = new_rars = new_zips = new_ts = None
if cfg.enable_filejoin():
new_joins = [jn for jn in xjoinables if jn not in joinables]
@ -217,7 +225,7 @@ def unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, joinables, zi
rerun = True
logging.info('Unzip starting on %s', workdir)
if unzip(nzo, workdir, workdir_complete, dele, one_folder, new_zips):
error = True
error = 1
logging.info('Unzip finished on %s', workdir)
nzo.set_action_line()
@ -233,7 +241,7 @@ def unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, joinables, zi
nzo.set_action_line()
if rerun:
if rerun and (cfg.enable_recursive() or new_ts or new_joins):
z, y = unpack_magic(nzo, workdir, workdir_complete, dele, one_folder,
xjoinables, xzips, xrars, xts, depth)
if z:
@ -289,7 +297,6 @@ def get_seq_number(name):
match, set, num = match_ts(name)
else:
num = tail[1:]
assert isinstance(num, str)
if num.isdigit():
return int(num)
else:
@ -300,20 +307,18 @@ def file_join(nzo, workdir, workdir_complete, delete, joinables):
when succesful, delete originals
"""
newfiles = []
bufsize = 24*1024*1024
# Create matching sets from the list of files
joinable_sets = {}
joinable_set = None
set = num = None
for joinable in joinables:
head, tail = os.path.splitext(joinable)
if tail == '.ts':
match, set, num = match_ts(joinable)
if not set:
set = head
if set not in joinable_sets:
joinable_sets[set] = []
joinable_sets[set].append(joinable)
head = match_ts(joinable)[1]
if head not in joinable_sets:
joinable_sets[head] = []
joinable_sets[head].append(joinable)
logging.debug("joinable_sets: %s", joinable_sets)
try:
@ -330,6 +335,11 @@ def file_join(nzo, workdir, workdir_complete, delete, joinables):
# done, go to next set
continue
# Only join when there is more than one file
size = len(current)
if size < 2:
continue
# Prepare joined file
filename = joinable_set
if workdir_complete:
@ -338,7 +348,6 @@ def file_join(nzo, workdir, workdir_complete, delete, joinables):
joined_file = open(filename, 'ab')
# Join the segments
size = len(current)
n = get_seq_number(current[0])
seq_error = n > 1
for joinable in current:
@ -348,7 +357,7 @@ def file_join(nzo, workdir, workdir_complete, delete, joinables):
logging.debug("Processing %s", joinable)
nzo.set_action_line(T('Joining'), '%.0f%%' % perc)
f = open(joinable, 'rb')
joined_file.write(f.read())
shutil.copyfileobj(f, joined_file, bufsize)
f.close()
if delete:
logging.debug("Deleting %s", joinable)
@ -455,7 +464,7 @@ def rar_unpack(nzo, workdir, workdir_complete, delete, one_folder, rars):
except OSError:
logging.warning(Ta('Deleting %s failed!'), latin1(brokenrar))
return not success, extracted_files
return fail, extracted_files
def rar_extract(rarfile, numrars, one_folder, nzo, setname, extraction_path):
@ -468,20 +477,24 @@ def rar_extract(rarfile, numrars, one_folder, nzo, setname, extraction_path):
new_files = None
rars = []
if nzo.password:
passwords = [nzo.password]
logging.info('Got a password set by user')
passwords = [nzo.password.strip()]
else:
passwords = []
# Append meta passwords, to prevent changing the original list
passwords.extend(nzo.meta.get('password', []))
if passwords:
logging.info('Read %s passwords from meta data in NZB', len(passwords))
pw_file = cfg.password_file.get_path()
if pw_file:
try:
pwf = open(pw_file, 'r')
lines = pwf.read().split('\n')
# Remove empty lines and space-only passwords and remove surrounding spaces
passwords.extend([pw.strip('\r\n ') for pw in lines if pw.strip('\r\n ')])
pws = [pw.strip('\r\n ') for pw in lines if pw.strip('\r\n ')]
passwords.extend(pws)
pwf.close()
logging.info('Read the passwords file %s', pw_file)
logging.info('Read %s passwords from file %s', len(pws), pw_file)
except IOError:
logging.info('Failed to read the passwords file %s', pw_file)
@ -510,7 +523,7 @@ def rar_extract(rarfile, numrars, one_folder, nzo, setname, extraction_path):
def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path, password):
""" Unpack single rar set 'rarfile' to 'extraction_path'
Return fail==0(ok)/fail==1(error)/fail==2(wrong password), new_files, rars
Return fail==0(ok)/fail==1(error)/fail==2(wrong password)/fail==3(crc-error), new_files, rars
"""
start = time()
@ -532,7 +545,7 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
############################################################################
if one_folder:
if one_folder or cfg.flat_unpack():
action = 'e'
else:
action = 'x'
@ -605,7 +618,7 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
msg = ('[%s] '+Ta('ERROR: CRC failed in "%s"')) % (setname, latin1(filename))
nzo.set_unpack_info('Unpack', unicoder(msg), set=setname)
logging.warning(Ta('ERROR: CRC failed in "%s"'), latin1(setname))
fail = 1
fail = 2 # Older unrar versions report a wrong password as a CRC error
elif line.startswith('Write error'):
nzo.fail_msg = T('Unpacking failed, write error or disk is full?')
@ -634,8 +647,12 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
nzo.set_unpack_info('Unpack', unicoder(msg), set=setname)
fail = 1
elif 'ncrypted file' in line and 'CRC failed' in line:
# unrar 4.x syntax
elif 'The specified password is incorrect' in line or \
('ncrypted file' in line and (('CRC failed' in line) or ('Checksum error' in line))):
# unrar 3.x: "Encrypted file: CRC failed in oLKQfrcNVivzdzSG22a2xo7t001.part1.rar (password incorrect ?)"
# unrar 4.x: "CRC failed in the encrypted file oLKQfrcNVivzdzSG22a2xo7t001.part1.rar. Corrupt file or wrong password."
# unrar 5.x: "Checksum error in the encrypted file oLKQfrcNVivzdzSG22a2xo7t001.part1.rar. Corrupt file or wrong password."
# unrar 5.01 : "The specified password is incorrect."
m = re.search('encrypted file (.+)\. Corrupt file', line)
if not m:
# unrar 3.x syntax
@ -643,12 +660,24 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
if m:
filename = TRANS(m.group(1)).strip()
else:
filename = '???'
filename = os.path.split(rarfile)[1]
nzo.fail_msg = T('Unpacking failed, archive requires a password')
msg = ('[%s][%s] '+Ta('Unpacking failed, archive requires a password')) % (setname, latin1(filename))
nzo.set_unpack_info('Unpack', unicoder(msg), set=setname)
fail = 2
elif 'is not RAR archive' in line:
# Unrecognizable RAR file
m = re.search('(.+) is not RAR archive', line)
if m:
filename = TRANS(m.group(1)).strip()
else:
filename = '???'
nzo.fail_msg = T('Unusable RAR file')
msg = ('[%s][%s] '+ Ta('Unusable RAR file')) % (setname, latin1(filename))
nzo.set_unpack_info('Unpack', unicoder(msg), set=setname)
fail = 3
else:
m = re.search(r'^(Extracting|Creating|...)\s+(.*?)\s+OK\s*$', line)
if m:
@ -671,7 +700,7 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
missing = []
# Loop through and check for the presence of all the files the archive contained
for path in expected_files:
if one_folder:
if one_folder or cfg.flat_unpack():
path = os.path.split(path)[1]
path = unicode2local(path)
if '?' in path:
@ -766,7 +795,7 @@ def unzip(nzo, workdir, workdir_complete, delete, one_folder, zips):
def ZIP_Extract(zipfile, extraction_path, one_folder):
""" Unzip single zip set 'zipfile' to 'extraction_path'
"""
if one_folder:
if one_folder or cfg.flat_unpack():
option = '-j' # Unpack without folders
else:
option = '-qq' # Dummy option
@ -789,7 +818,7 @@ def ZIP_Extract(zipfile, extraction_path, one_folder):
# PAR2 Functions
#------------------------------------------------------------------------------
def par2_repair(parfile_nzf, nzo, workdir, setname):
def par2_repair(parfile_nzf, nzo, workdir, setname, single):
""" Try to repair a set, return readd or correctness """
#set the current nzo status to "Repairing". Used in History
@ -823,7 +852,7 @@ def par2_repair(parfile_nzf, nzo, workdir, setname):
joinables, zips, rars, ts = build_filelists(workdir, None, check_rar=False)
finished, readd, pars, datafiles, used_joinables, used_par2 = PAR_Verify(parfile, parfile_nzf, nzo,
setname, joinables)
setname, joinables, single=single)
if finished:
result = True
@ -915,7 +944,7 @@ _RE_IS_MATCH_FOR = re.compile('File: "([^"]+)" - is a match for "([^"]+)"')
_RE_LOADING_PAR2 = re.compile('Loading "([^"]+)"\.')
_RE_LOADED_PAR2 = re.compile('Loaded (\d+) new packets')
def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False, single=False):
""" Run par2 on par-set """
if cfg.never_repair():
cmd = 'v'
@ -943,16 +972,16 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
command = [str(PAR2C_COMMAND), cmd, parfile]
classic = True
for joinable in joinables:
if setname in joinable:
command.append(joinable)
# Append the wildcard for this set
wildcard = '%s*' % os.path.join(os.path.split(parfile)[0], setname)
if len(globber(wildcard, None)) < 2:
if single or len(globber(wildcard, None)) < 2:
# Support bizarre naming conventions
wildcard = os.path.join(os.path.split(parfile)[0], '*')
command.append(wildcard)
if sabnzbd.WIN32 or sabnzbd.DARWIN:
command.append(wildcard)
else:
flist = [item for item in globber(wildcard, None) if os.path.isfile(item)]
command.extend(flist)
stup, need_shell, command, creationflags = build_command(command)
logging.debug('Starting par2: %s', command)
@ -1112,7 +1141,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
nzo.fail_msg = msg
msg = u'[%s] %s' % (unicoder(setname), msg)
nzo.set_unpack_info('Repair', msg, set=setname)
nzo.status = Status.FAILED
nzo.status = Status.FETCHING
needed_blocks = avail_blocks
force = True
@ -1258,7 +1287,7 @@ def PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=False):
if retry_classic:
logging.debug('Retry PAR2-joining with par2-classic')
return PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=True)
return PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables, classic=True, single=single)
else:
return finished, readd, pars, datafiles, used_joinables, used_par2
@ -1353,12 +1382,14 @@ def build_filelists(workdir, workdir_complete, check_rar=True):
if workdir_complete:
for root, dirs, files in os.walk(workdir_complete):
for _file in files:
filelist.append(os.path.join(root, _file))
if '.AppleDouble' not in root and '.DS_Store' not in root:
filelist.append(os.path.join(root, _file))
if workdir and not filelist:
for root, dirs, files in os.walk(workdir):
for _file in files:
filelist.append(os.path.join(root, _file))
if '.AppleDouble' not in root and '.DS_Store' not in root:
filelist.append(os.path.join(root, _file))
if check_rar:
joinables = [f for f in filelist if SPLITFILE_RE.search(f) and not is_rarfile(f)]
@ -1367,10 +1398,7 @@ def build_filelists(workdir, workdir_complete, check_rar=True):
zips = [f for f in filelist if ZIP_RE.search(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)]
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]
@ -1436,7 +1464,7 @@ def unrar_check(rar):
""" Return True if correct version of unrar is found """
if rar:
try:
version = subprocess.Popen(rar, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).stdout.read()
version = run_simple(rar)
except:
return False
m = re.search("RAR\s(\d+)\.(\d+)\s+.*Alexander Roshal", version)
@ -1501,7 +1529,7 @@ def crc_check(path, target_crc):
def analyse_show(name):
""" Do a quick SeasonSort check and return basic facts """
job = SeriesSorter(name, None, None)
job = SeriesSorter(None, name, None, None)
job.match(force=True)
if job.is_match():
job.get_values()
@ -1564,7 +1592,7 @@ def list2cmdline(lst):
for arg in lst:
if not arg:
nlst.append('""')
elif (' ' in arg) or ('\t' in arg) or ('&' in arg) or ('|' in arg) or (';' in arg):
elif (' ' in arg) or ('\t' in arg) or ('&' in arg) or ('|' in arg) or (';' in arg) or (',' in arg):
nlst.append('"%s"' % arg)
else:
nlst.append(arg)
@ -1600,3 +1628,13 @@ def get_from_url(url, timeout=None):
except:
output = None
return output
#------------------------------------------------------------------------------
def run_simple(cmd):
""" Run simple external command and return output
"""
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
txt = p.stdout.read()
p.wait()
return txt

34
sabnzbd/nzbqueue.py

@ -154,6 +154,7 @@ class NzbQueue(TryList):
verified = sabnzbd.load_data(VERIFIED_FILE, path, remove=False) or {'x':False}
return not bool([True for x in verified if not verified[x]])
nzo_id = None
name = os.path.basename(folder)
path = os.path.join(folder, JOB_ADMIN)
if hasattr(new_nzb, 'filename'):
@ -165,15 +166,18 @@ class NzbQueue(TryList):
filename = globber(path, '*.gz')
if len(filename) > 0:
logging.debug('Repair job %s by reparsing stored NZB', latin1(name))
sabnzbd.add_nzbfile(filename[0], pp=None, script=None, cat=None, priority=None, nzbname=name, reuse=True)
nzo_id = sabnzbd.add_nzbfile(filename[0], pp=None, script=None, cat=None, priority=None, nzbname=name, reuse=True)[1]
else:
logging.debug('Repair job %s without stored NZB', latin1(name))
nzo = NzbObject(name, 0, pp=None, script=None, nzb='', cat=None, priority=None, nzbname=name, reuse=True)
self.add(nzo)
nzo_id = nzo.nzo_id
else:
remove_all(path, '*.gz')
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)
nzo_id = sabnzbd.add_nzbfile(new_nzb, pp=None, script=None, cat=None, priority=None, nzbname=name, reuse=True)[1]
return nzo_id
def send_back(self, nzo):
@ -186,7 +190,9 @@ class NzbQueue(TryList):
from sabnzbd.dirscanner import ProcessSingleFile
res, nzo_ids = ProcessSingleFile(nzo.work_name + '.nzb', nzb_path, reuse=True)
if res == 0 and nzo_ids:
self.replace_in_q(nzo, nzo_ids[0])
nzo = self.replace_in_q(nzo, nzo_ids[0])
# Reset reuse flag to make pause/abort on encryption possible
nzo.reuse = False
@synchronized(NZBQUEUE_LOCK)
@ -200,10 +206,11 @@ class NzbQueue(TryList):
self.__nzo_list.pop(pos)
del self.__nzo_table[nzo.nzo_id]
del nzo
return new_nzo
except:
logging.error('Failed to restart NZB after pre-check (%s)', nzo.nzo_id)
logging.info("Traceback: ", exc_info = True)
return
return nzo
@synchronized(NZBQUEUE_LOCK)
def save(self, save_nzo=None):
@ -220,7 +227,7 @@ class NzbQueue(TryList):
if save_nzo is None or nzo is save_nzo:
sabnzbd.save_data(nzo, nzo.nzo_id, nzo.workpath)
if not nzo.futuretype:
nzo.save_attribs()
nzo.save_to_disk()
sabnzbd.save_admin((QUEUE_VERSION, nzo_ids, []), QUEUE_FILE_NAME)
@ -299,20 +306,21 @@ class NzbQueue(TryList):
self.__nzo_table[nzo_id].script = script
@synchronized(NZBQUEUE_LOCK)
def change_cat(self, nzo_ids, cat):
def change_cat(self, nzo_ids, cat, explicit_priority=None):
for nzo_id in [item.strip() for item in nzo_ids.split(',')]:
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]
nzo.cat, pp, nzo.script, prio = cat_to_opts(cat)
nzo.set_pp(pp)
self.set_priority(nzo_id, prio)
if explicit_priority is None:
self.set_priority(nzo_id, prio)
@synchronized(NZBQUEUE_LOCK)
def change_name(self, nzo_id, name):
def change_name(self, nzo_id, name, password=None):
if nzo_id in self.__nzo_table:
nzo = self.__nzo_table[nzo_id]
if not nzo.futuretype:
nzo.set_final_name_pw(name)
nzo.set_final_name_pw(name, password)
else:
# Reset url fetch wait time
nzo.wait = None
@ -595,7 +603,7 @@ class NzbQueue(TryList):
return nzo_id_pos1
nzo.priority = priority
nzo.save_attribs()
nzo.save_to_disk()
if nzo_id_pos1 != -1:
del self.__nzo_list[nzo_id_pos1]
@ -755,7 +763,7 @@ class NzbQueue(TryList):
if not nzo.deleted:
nzo.deleted = True
if nzo.precheck:
nzo.save_attribs()
nzo.save_to_disk()
# Check result
enough, ratio = nzo.check_quality()
if enough:
@ -886,7 +894,7 @@ def _nzo_date_cmp(nzo1, nzo2):
return cmp(avg_date1, avg_date2)
def _nzo_name_cmp(nzo1, nzo2):
return cmp(nzo1.filename, nzo2.filename)
return cmp(nzo1.final_name.lower(), nzo2.final_name.lower())
def _nzo_size_cmp(nzo1, nzo2):
return cmp(nzo1.bytes, nzo2.bytes)
@ -947,7 +955,7 @@ def sort_queue(field, reverse=False):
@synchronized_CV
@synchronized(NZBQUEUE_LOCK)
def repair_job(folder, new_nzb):
NzbQueue.do.repair_job(folder, new_nzb)
return NzbQueue.do.repair_job(folder, new_nzb)
@synchronized_CV
@synchronized(NZBQUEUE_LOCK)

143
sabnzbd/nzbstuff.py

@ -36,7 +36,7 @@ except ImportError:
import sabnzbd
from sabnzbd.constants import sample_match, GIGI, ATTRIB_FILE, JOB_ADMIN, \
DEFAULT_PRIORITY, LOW_PRIORITY, NORMAL_PRIORITY, \
HIGH_PRIORITY, PAUSED_PRIORITY, TOP_PRIORITY, DUP_PRIORITY, \
HIGH_PRIORITY, PAUSED_PRIORITY, TOP_PRIORITY, DUP_PRIORITY, REPAIR_PRIORITY, \
RENAMES_FILE, Status
from sabnzbd.misc import to_units, cat_to_opts, cat_convert, sanitize_foldername, \
get_unique_path, get_admin_path, remove_all, format_source_url, \
@ -45,6 +45,7 @@ from sabnzbd.misc import to_units, cat_to_opts, cat_convert, sanitize_foldername
import sabnzbd.cfg as cfg
from sabnzbd.trylist import TryList
from sabnzbd.encoding import unicoder, platform_encode, latin1, name_fixer
from sabnzbd.rating import Rating
__all__ = ['Article', 'NzbFile', 'NzbObject']
@ -532,7 +533,9 @@ NzbObjectMapper = (
('precheck', 'precheck'),
('incomplete', 'incomplete'), # Was detected as incomplete
('reuse', 'reuse'),
('meta', 'meta') # Meta-date from 1.1 type NZB
('meta', 'meta'), # Meta-date from 1.1 type NZB
('unwanted_ext', 'unwanted_ext'),
('rating_filtered', 'rating_filtered') # Has passed through ratings filter
)
class NzbObject(TryList):
@ -544,7 +547,6 @@ class NzbObject(TryList):
filename = platform_encode(filename)
nzbname = platform_encode(nzbname)
nzbname = sanitize_foldername(nzbname)
if pp is None:
r = u = d = None
@ -562,8 +564,11 @@ class NzbObject(TryList):
dname, ext = os.path.splitext(work_name) # Used for folder name for final unpack
if ext.lower() == '.nzb':
work_name = dname
work_name = sanitize_foldername(work_name)
work_name, password = scan_password(work_name)
if not work_name:
work_name = filename
if nzb and work_name and not reuse:
work_name = sanitize_foldername(work_name)
self.work_name = work_name
self.final_name = work_name
@ -620,6 +625,8 @@ class NzbObject(TryList):
self.oversized = False
self.precheck = False
self.incomplete = False
self.unwanted_ext = 0
self.rating_filtered = 0
self.reuse = reuse
if self.status == Status.QUEUED and not reuse:
self.precheck = cfg.pre_check()
@ -673,7 +680,8 @@ class NzbObject(TryList):
adir = os.path.join(wdir, JOB_ADMIN)
# Duplicate checking, needs to be done before the backup
duplicate = (not reuse) and nzb and dup_check and sabnzbd.backup_exists(filename)
duplicate = (not reuse) and nzb and dup_check and sabnzbd.backup_exists(filename) \
and priority != REPAIR_PRIORITY
if reuse:
remove_all(adir, 'SABnzbd_nz?_*')
@ -750,7 +758,7 @@ class NzbObject(TryList):
cat = cat_convert(grp)
if cat:
break
if cfg.create_group_folders():
self.dirprefix.append(self.group)
@ -801,12 +809,12 @@ class NzbObject(TryList):
self.priority = LOW_PRIORITY
if duplicate and cfg.no_dupes() == 1:
logging.warning(Ta('Ignoring duplicate NZB "%s"'), filename)
if cfg.warn_dupl_jobs(): logging.warning(Ta('Ignoring duplicate NZB "%s"'), filename)
self.purge_data(keep_basic=False)
raise TypeError
if duplicate or self.priority == DUP_PRIORITY:
logging.warning(Ta('Pausing duplicate NZB "%s"'), filename)
if cfg.warn_dupl_jobs(): logging.warning(Ta('Pausing duplicate NZB "%s"'), filename)
self.duplicate = True
self.pause()
self.priority = NORMAL_PRIORITY
@ -823,6 +831,33 @@ class NzbObject(TryList):
else:
self.files.sort(cmp=nzf_cmp_name)
# In the hunt for Unwanted Extensions:
# The file with the unwanted extension often is in the first or the last rar file
# So put the last rar immediatly after the first rar file so that it gets detected early
if cfg.unwanted_extensions() and not cfg.auto_sort():
# ... only useful if there are unwanted extensions defined and there is no sorting on date
logging.debug('Unwanted Extension: putting last rar after first rar')
nzfposcounter = firstrarpos = lastrarpos = 0
for nzf in self.files:
nzfposcounter += 1
if '.rar' in str(nzf):
# a NZF found with '.rar' in the name
if firstrarpos == 0:
# this is the first .rar found, so remember this position
firstrarpos = nzfposcounter
lastrarpos = nzfposcounter
lastrarnzf = nzf # The NZF itself
if firstrarpos != lastrarpos:
# at least two different .rar's found
logging.debug('Unwanted Extension: First rar at %s, Last rar at %s',firstrarpos, lastrarpos)
logging.debug('Unwanted Extension: Last rar is %s', str(lastrarnzf))
try:
self.files.remove(lastrarnzf) # first remove. NB: remove() does searches for lastrarnzf
self.files.insert(firstrarpos,lastrarnzf) # ... and only then add after position firstrarpos
except:
logging.debug('The lastrar swap did not go well')
# Set nzo save-delay to 6 sec per GB with a max of 5 min
self.save_timeout = min(6.0 * float(self.bytes) / GIGI, 300.0)
@ -889,8 +924,8 @@ class NzbObject(TryList):
head, vol, block = analyse_par2(fn)
## Is a par2file and repair mode activated
if head and (self.repair or cfg.allow_streaming()):
## Skip if mini-par2 is not complete
if not block and nzf.bytes_left:
## Skip if mini-par2 is not complete and there are more par2 files
if not block and nzf.bytes_left and self.extrapars.get(head):
return
nzf.set_par2(head, vol, block)
## Already got a parfile for this set?
@ -934,7 +969,7 @@ class NzbObject(TryList):
if file_done:
self.remove_nzf(nzf)
if not self.reuse and not self.precheck and cfg.fail_hopeless() and not self.check_quality(99)[0]:
if not self.reuse and cfg.fail_hopeless() and not self.check_quality(99)[0]:
#set the nzo status to return "Queued"
self.status = Status.QUEUED
self.set_download_report()
@ -1022,6 +1057,7 @@ class NzbObject(TryList):
def set_pp(self, value):
self.repair, self.unpack, self.delete = sabnzbd.pp_to_opts(value)
self.save_to_disk()
@property
def final_name_pw(self):
@ -1034,27 +1070,35 @@ class NzbObject(TryList):
prefix += Ta('TOO LARGE') + ' / ' #: Queue indicator for oversized job
if self.incomplete and self.status == 'Paused':
prefix += Ta('INCOMPLETE') + ' / ' #: Queue indicator for incomplete NZB
if self.unwanted_ext and self.status == 'Paused':
prefix += Ta('UNWANTED') + ' / ' #: Queue indicator for unwanted extensions
if self.rating_filtered and self.status == 'Paused':
prefix += Ta('FILTERED') + ' / ' #: Queue indicator for filtered
if isinstance(self.wait, float):
dif = int(self.wait - time.time() + 0.5)
if dif > 0:
prefix += Ta('WAIT %s sec') % dif + ' / ' #: Queue indicator for waiting URL fetch
if self.password:
return '%s%s / %s' % (name_fixer(prefix), self.final_name, self.password)
return '%s%s/%s' % (name_fixer(prefix), self.final_name, self.password)
else:
return '%s%s' % (name_fixer(prefix), self.final_name)
@property
def final_name_pw_clean(self):
if self.password:
return '%s / %s' % (self.final_name, self.password)
return '%s/%s' % (self.final_name, self.password)
else:
return self.final_name
def set_final_name_pw(self, name):
def set_final_name_pw(self, name, password=None):
if isinstance(name, str):
name, self.password = scan_password(platform_encode(name))
if password is not None:
name = platform_encode(name)
self.password = platform_encode(password)
else:
name, self.password = scan_password(platform_encode(name))
self.final_name = sanitize_foldername(name)
self.save_attribs()
self.save_to_disk()
def pause(self):
self.status = 'Paused'
@ -1067,10 +1111,16 @@ class NzbObject(TryList):
if self.encrypted:
# If user resumes after encryption warning, no more auto-pauses
self.encrypted = 2
# If user resumes after warning, reset duplicate/oversized/incomplete indicators
if self.rating_filtered:
# If user resumes after filtered warning, no more auto-pauses
self.rating_filtered = 2
# If user resumes after warning, reset duplicate/oversized/incomplete/unwanted indicators
self.duplicate = False
self.oversized = False
self.incomplete = False
if self.unwanted_ext:
# If user resumes after "unwanted" warning, no more auto-pauses
self.unwanted_ext = 2
def add_parfile(self, parfile):
if parfile not in self.files:
@ -1257,6 +1307,23 @@ class NzbObject(TryList):
self.files[pos+1] = nzf
self.files[pos] = tmp_nzf
# Determine if rating information (including site identifier so rating can be updated)
# is present in metadata and if so store it
def update_rating(self):
if cfg.rating_enable():
try:
def _get_first_meta(type):
values = self.meta.get('x-oznzb-rating-' + type, None) or self.meta.get('x-rating-' + type, None)
return values[0] if values else None
rating_types = ['video', 'videocnt', 'audio', 'audiocnt', 'voteup' ,'votedown', \
'spam', 'confirmed-spam', 'passworded', 'confirmed-passworded']
fields = {}
for k in rating_types:
fields[k] = _get_first_meta(k)
Rating.do.add_rating(_get_first_meta('id'), self.nzo_id, self.meta.get('x-rating-host'), fields)
except:
pass
## end nzo.Mutators #######################################################
###########################################################################
@property
@ -1412,6 +1479,12 @@ class NzbObject(TryList):
def repair_opts(self):
return self.repair, self.unpack, self.delete
def save_to_disk(self):
""" Save job's admin to disk """
self.save_attribs()
if self.nzo_id:
sabnzbd.save_data(self, self.nzo_id, self.workpath)
def save_attribs(self):
set_attrib_file(self.workpath, (self.cat, self.pp, self.script, self.priority, self.final_name_pw_clean, self.url))
@ -1604,24 +1677,36 @@ def format_time_string(seconds, days=0):
return completestr.strip()
RE_PASSWORD1 = re.compile(r'([^/\\]+)[/\\](.+)')
RE_PASSWORD2 = re.compile(r'(.+){{([^{}]+)}}$')
RE_PASSWORD3 = re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)
def scan_password(name):
""" Get password (if any) from the title
"""
if 'http://' in name or 'https://' in name:
return name, None
m = RE_PASSWORD1.search(name)
if not m:
m = RE_PASSWORD2.search(name)
if not m:
m = RE_PASSWORD3.search(name)
if m:
return m.group(1).strip('. '), m.group(2).strip()
else:
return name.strip('. '), None
braces = name.find('{{')
if braces < 0:
braces = len(name)
slash = name.find('/')
# Look for name/password, but make sure that '/' comes before any {{
if slash >= 0 and slash < braces and not 'password=' in name:
return name[:slash].strip('. '), name[slash+1:]
# Look for "name password=password"
pw = name.find('password=')
if pw >= 0:
return name[:pw].strip('. '), name[pw+9:]
# Look for name{{password}}
if braces < len(name) and name.endswith('}}'):
return name[:braces].strip('. '), name[braces+2:len(name)-2]
# Look again for name/password
if slash >= 0:
return name[:slash].strip('. '), name[slash+1:]
# No password found
return name, None
def get_attrib_file(path, size):

3
sabnzbd/osxmenu.py

@ -84,6 +84,9 @@ class SABnzbdDelegate(NSObject):
self.status_item = status_bar.statusItemWithLength_(NSVariableStatusItemLength)
for i in status_icons.keys():
self.icons[i] = NSImage.alloc().initByReferencingFile_(status_icons[i])
if sabnzbd.DARWIN_YS:
# Support for Yosemite Dark Mode
self.icons[i].setTemplate_(YES)
self.status_item.setImage_(self.icons['idle'])
self.status_item.setAlternateImage_(self.icons['clicked'])
self.status_item.setHighlightMode_(1)

66
sabnzbd/postproc.py

@ -31,7 +31,7 @@ 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, \
make_script_path, \
make_script_path, sanitize_and_trim_path, \
on_cleanup_list, renamer, remove_dir, remove_all, globber, \
set_permissions, cleanup_empty_directories
from sabnzbd.tvsort import Sorter
@ -39,6 +39,7 @@ from sabnzbd.constants import REPAIR_PRIORITY, TOP_PRIORITY, POSTPROC_QUEUE_FILE
POSTPROC_QUEUE_VERSION, sample_match, JOB_ADMIN, Status, VERIFIED_FILE
from sabnzbd.encoding import TRANS, unicoder
from sabnzbd.newzbin import Bookmarks
from sabnzbd.rating import Rating
import sabnzbd.emailer as emailer
import sabnzbd.dirscanner as dirscanner
import sabnzbd.downloader
@ -125,13 +126,14 @@ class PostProcessor(Thread):
except:
nzo_id = getattr(nzo, 'nzo_id', 'unknown id')
logging.error(Ta('Failed to remove nzo from postproc queue (id)') + ' ' + nzo_id)
logging.info('Traceback: ', exc_info = True)
self.save()
def stop(self):
""" Stop thread after finishing running job """
self.__stop = True
self.queue.put(None)
self.save()
self.__stop = True
def empty(self):
""" Return True if pp queue is empty """
@ -160,12 +162,14 @@ class PostProcessor(Thread):
continue
try:
nzo = self.queue.get(timeout=3)
nzo = self.queue.get(timeout=1)
except Queue.Empty:
if check_eoq:
check_eoq = False
handle_empty_queue()
continue
continue
else:
nzo = self.queue.get()
## Stop job
if not nzo:
@ -238,7 +242,8 @@ def process_job(nzo):
nzo.status = Status.FAILED
nzo.save_attribs()
all_ok = False
par_error = unpack_error = True
par_error = True
unpack_error = 1
try:
@ -268,7 +273,8 @@ def process_job(nzo):
flag_repair = flag_unpack = False
all_ok = cfg.empty_postproc() and empty
if not all_ok:
par_error = unpack_error = True
par_error = True
unpack_error = 1
script = nzo.script
cat = nzo.cat
@ -307,11 +313,16 @@ def process_job(nzo):
complete_dir = real_path(cfg.complete_dir.get_path(), catdir)
## TV/Movie/Date Renaming code part 1 - detect and construct paths
file_sorter = Sorter(cat)
if cfg.enable_meta():
file_sorter = Sorter(nzo, cat)
else:
file_sorter = Sorter(None, cat)
complete_dir = file_sorter.detect(dirname, complete_dir)
if file_sorter.sort_file:
one_folder = False
complete_dir = sanitize_and_trim_path(complete_dir)
if one_folder:
workdir_complete = create_dirs(complete_dir)
else:
@ -405,7 +416,7 @@ def process_job(nzo):
if empty:
job_result = -1
else:
job_result = int(par_error) + int(unpack_error)*2
job_result = int(par_error) + int(bool(unpack_error))*2
if cfg.ignore_samples() > 0:
remove_samples(workdir_complete)
@ -429,7 +440,8 @@ def process_job(nzo):
nzo.set_action_line(T('Running script'), unicoder(script))
nzo.set_unpack_info('Script', T('Running user script %s') % unicoder(script), unique=True)
script_log, script_ret = external_processing(script_path, workdir_complete, nzo.filename,
msgid, dirname, cat, nzo.group, job_result)
msgid, dirname, cat, nzo.group, job_result,
nzo.nzo_info.get('failure', ''))
script_line = get_last_line(script_log)
if script_log:
script_output = nzo.nzo_id
@ -476,6 +488,16 @@ def process_job(nzo):
## Force error for empty result
all_ok = all_ok and not empty
## Update indexer with results
if cfg.rating_enable():
if nzo.encrypted > 0:
Rating.do.update_auto_flag(nzo.nzo_id, Rating.FLAG_ENCRYPTED)
if empty:
hosts = map(lambda s: s.host, sabnzbd.downloader.Downloader.do.nzo_servers(nzo))
if not hosts: hosts = [None]
for host in hosts:
Rating.do.update_auto_flag(nzo.nzo_id, Rating.FLAG_EXPIRED, host)
## Show final status in history
if all_ok:
growler.send_notification(T('Download Completed'), filename, 'complete')
@ -534,6 +556,10 @@ def process_job(nzo):
logging.error(Ta('Error removing workdir (%s)'), workdir)
logging.info("Traceback: ", exc_info = True)
# Use automatic retry link on par2 errors and encrypted/bad RARs
if par_error or unpack_error in (2, 3):
try_alt_nzb(nzo)
return True
@ -559,6 +585,7 @@ def parring(nzo, workdir):
re_add = False
par_error = False
single = len(repair_sets) == 1
if repair_sets:
for setname in repair_sets:
@ -567,13 +594,14 @@ def parring(nzo, workdir):
if not verified.get(setname, False):
logging.info("Running repair on set %s", setname)
parfile_nzf = par_table[setname]
if not os.path.exists(os.path.join(nzo.downpath, parfile_nzf.filename)):
if os.path.exists(os.path.join(nzo.downpath, parfile_nzf.filename)) or parfile_nzf.extrapars:
need_re_add, res = par2_repair(parfile_nzf, nzo, workdir, setname, single=single)
re_add = re_add or need_re_add
if not res and not need_re_add and cfg.sfv_check():
res = try_sfv_check(nzo, workdir, setname)
verified[setname] = res
else:
continue
need_re_add, res = par2_repair(parfile_nzf, nzo, workdir, setname)
re_add = re_add or need_re_add
if not res and not need_re_add and cfg.sfv_check():
res = try_sfv_check(nzo, workdir, setname)
verified[setname] = res
par_error = par_error or not res
else:
logging.info("No par2 sets for %s", filename)
@ -607,7 +635,7 @@ def try_sfv_check(nzo, workdir, setname):
par_error = False
found = False
for sfv in sfvs:
if setname in os.path.basename(sfv):
if setname.lower() in os.path.basename(sfv).lower():
found = True
nzo.set_unpack_info('Repair', T('Trying SFV verification'))
failed = sfv_check(sfv)
@ -814,3 +842,9 @@ def remove_from_list(name, lst):
logging.debug('Popping %s', lst[n])
lst.pop(n)
return
def try_alt_nzb(nzo):
""" Try to get a new NZB if available """
url = nzo.nzo_info.get('failure')
if url and cfg.new_nzb_on_failure():
sabnzbd.add_url(url, nzo.pp, nzo.script, nzo.cat, nzo.priority)

285
sabnzbd/rating.py

@ -0,0 +1,285 @@
#!/usr/bin/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.
"""
sabnzbd.rating - Rating support functions
"""
import httplib
import urllib
import time
import logging
import copy
import socket
try:
socket.ssl
_HAVE_SSL = True
except:
_HAVE_SSL = False
from threading import *
import sabnzbd
from sabnzbd.decorators import synchronized
from sabnzbd.misc import OrderedSetQueue
import sabnzbd.cfg as cfg
RATING_URL = "/releaseRatings/releaseRatings.php"
RATING_LOCK = RLock()
_g_warnings = 0
def _warn(msg):
global _g_warnings
_g_warnings += 1
if _g_warnings < 3:
logging.warning(msg)
def _reset_warn():
global _g_warnings
_g_warnings = 0
class NzbRating(object):
def __init__(self):
self.avg_video = 0
self.avg_video_cnt = 0
self.avg_audio = 0
self.avg_audio_cnt = 0
self.avg_vote_up = 0
self.avg_vote_down = 0
self.user_video = None
self.user_audio = None
self.user_vote = None
self.user_flag = {}
self.auto_flag = {}
self.changed = 0
class NzbRatingV2(NzbRating):
def __init__(self):
super(NzbRatingV2, self).__init__()
self.avg_spam_cnt = 0
self.avg_spam_confirm = False
self.avg_encrypted_cnt = 0
self.avg_encrypted_confirm = False
def to_v2(self, rating):
self.__dict__.update(rating.__dict__)
return self
class Rating(Thread):
VERSION = 2
VOTE_UP = 1
VOTE_DOWN = 2
FLAG_OK = 0
FLAG_SPAM = 1
FLAG_ENCRYPTED = 2
FLAG_EXPIRED = 3
FLAG_OTHER = 4
FLAG_COMMENT = 5
CHANGED_USER_VIDEO = 0x01
CHANGED_USER_AUDIO = 0x02
CHANGED_USER_VOTE = 0x04
CHANGED_USER_FLAG = 0x08
CHANGED_AUTO_FLAG = 0x10
do = None
def __init__(self):
Rating.do = self
self.shutdown = False
self.queue = OrderedSetQueue()
try:
(self.version, self.ratings, self.nzo_indexer_map) = sabnzbd.load_admin("Rating.sab")
if self.version == 1:
ratings = {}
for k, v in self.ratings.iteritems():
ratings[k] = NzbRatingV2().to_v2(v)
self.ratings = ratings
self.version = 2
if (self.version != Rating.VERSION):
raise Exception()
except:
self.version = Rating.VERSION
self.ratings = {}
self.nzo_indexer_map = {}
Thread.__init__(self)
if not _HAVE_SSL:
logging.warning('Ratings server requires secure connection')
self.stop()
def stop(self):
self.shutdown = True
self.queue.put(None) # Unblock queue
def run(self):
self.shutdown = False
while not self.shutdown:
time.sleep(1)
if not cfg.rating_enable(): continue
indexer_id = self.queue.get()
try:
if indexer_id and not self._send_rating(indexer_id):
for i in range(0, 60):
if self.shutdown: break
time.sleep(1)
self.queue.put(indexer_id)
except:
pass
logging.debug('Stopping ratings')
@synchronized(RATING_LOCK)
def save(self):
if self.ratings and self.nzo_indexer_map:
sabnzbd.save_admin((self.version, self.ratings, self.nzo_indexer_map), "Rating.sab")
# The same file may be uploaded multiple times creating a new nzo_id each time
@synchronized(RATING_LOCK)
def add_rating(self, indexer_id, nzo_id, host, fields):
if indexer_id and nzo_id and (len(fields) == 10):
logging.debug('Add rating (%s, %s: %s, %s, %s, %s)', indexer_id, nzo_id, fields['video'], fields['audio'], fields['voteup'], fields['votedown'])
try:
rating = self.ratings.get(indexer_id, NzbRatingV2())
if fields['video'] and fields['videocnt']:
rating.avg_video = int(float(fields['video']))
rating.avg_video_cnt = int(float(fields['videocnt']))
if fields['audio'] and fields['audiocnt']:
rating.avg_audio = int(float(fields['audio']))
rating.avg_audio_cnt = int(float(fields['audiocnt']))
if fields['voteup']: rating.avg_vote_up = int(float(fields['voteup']))
if fields['votedown']: rating.avg_vote_down = int(float(fields['votedown']))
if fields['spam']: rating.avg_spam_cnt = int(float(fields['spam']))
if fields['confirmed-spam']: rating.avg_spam_confirm = (fields['confirmed-spam'].lower() == 'yes')
if fields['passworded']: rating.avg_encrypted_cnt = int(float(fields['passworded']))
if fields['confirmed-passworded']: rating.avg_encrypted_confirm = (fields['confirmed-passworded'].lower() == 'yes')
rating.host = host[0] if host and isinstance(host, list) else host
self.ratings[indexer_id] = rating
self.nzo_indexer_map[nzo_id] = indexer_id
except:
pass
@synchronized(RATING_LOCK)
def update_user_rating(self, nzo_id, video, audio, vote, flag, flag_detail = None):
logging.debug('Updating user rating (%s: %s, %s, %s, %s)', nzo_id, video, audio, vote, flag)
if nzo_id not in self.nzo_indexer_map:
logging.warning('indexer id (%s) not found for ratings file', nzo_id)
return
indexer_id = self.nzo_indexer_map[nzo_id]
rating = self.ratings[indexer_id]
if video:
rating.user_video = int(video)
rating.avg_video = int((rating.avg_video_cnt * rating.avg_video + rating.user_video) / (rating.avg_video_cnt + 1))
rating.changed = rating.changed | Rating.CHANGED_USER_VIDEO
if audio:
rating.user_audio = int(audio)
rating.avg_audio = int((rating.avg_audio_cnt * rating.avg_audio + rating.user_audio) / (rating.avg_audio_cnt + 1))
rating.changed = rating.changed | Rating.CHANGED_USER_AUDIO
if flag:
rating.user_flag = { 'val': int(flag), 'detail': flag_detail }
rating.changed = rating.changed | Rating.CHANGED_USER_FLAG
if vote and not rating.user_vote:
rating.user_vote = int(vote)
rating.changed = rating.changed | Rating.CHANGED_USER_VOTE
if rating.user_vote == Rating.VOTE_UP:
rating.avg_vote_up += 1
else:
rating.avg_vote_down += 1
self.queue.put(indexer_id)
@synchronized(RATING_LOCK)
def update_auto_flag(self, nzo_id, flag, flag_detail = None):
if not flag or not cfg.rating_enable() or not cfg.rating_feedback() or (nzo_id not in self.nzo_indexer_map):
return
logging.debug('Updating auto flag (%s: %s)', nzo_id, flag)
indexer_id = self.nzo_indexer_map[nzo_id]
rating = self.ratings[indexer_id]
rating.auto_flag = { 'val': int(flag), 'detail': flag_detail }
rating.changed = rating.changed | Rating.CHANGED_AUTO_FLAG
self.queue.put(indexer_id)
@synchronized(RATING_LOCK)
def get_rating_by_nzo(self, nzo_id):
if nzo_id not in self.nzo_indexer_map:
return None
return copy.copy(self.ratings[self.nzo_indexer_map[nzo_id]])
@synchronized(RATING_LOCK)
def _get_rating_by_indexer(self, indexer_id):
return copy.copy(self.ratings[indexer_id])
def _flag_request(self, val, flag_detail, auto):
if val == Rating.FLAG_SPAM:
return {'m': 'rs', 'auto': auto}
if val == Rating.FLAG_ENCRYPTED:
return {'m': 'rp', 'auto': auto}
if val == Rating.FLAG_EXPIRED:
expired_host = flag_detail if flag_detail and len(flag_detail) > 0 else 'Other'
return {'m': 'rpr', 'pr': expired_host, 'auto': auto}
if (val == Rating.FLAG_OTHER) and flag_detail and len(flag_detail) > 0:
return {'m': 'o', 'r': flag_detail}
if (val == Rating.FLAG_COMMENT) and flag_detail and len(flag_detail) > 0:
return {'m': 'rc', 'r': flag_detail}
def _send_rating(self, indexer_id):
logging.debug('Updating indexer rating (%s)', indexer_id)
api_key = cfg.rating_api_key()
rating_host = cfg.rating_host()
if not api_key:
return True
requests = []
_headers = {'User-agent' : 'SABnzbd+/%s' % sabnzbd.version.__version__, 'Content-type': 'application/x-www-form-urlencoded'}
rating = self._get_rating_by_indexer(indexer_id) # Requesting info here ensures always have latest information even on retry
if hasattr(rating, 'host') and rating.host:
rating_host = rating.host
if not rating_host:
return True
if rating.changed & Rating.CHANGED_USER_VIDEO:
requests.append({'m': 'r', 'r': 'videoQuality', 'rn': rating.user_video})
if rating.changed & Rating.CHANGED_USER_AUDIO:
requests.append({'m': 'r', 'r': 'audioQuality', 'rn': rating.user_audio})
if rating.changed & Rating.CHANGED_USER_VOTE:
up_down = 'up' if rating.user_vote == Rating.VOTE_UP else 'down'
requests.append({'m': 'v', 'v': up_down, 'r': 'overall'})
if rating.changed & Rating.CHANGED_USER_FLAG:
requests.append(self._flag_request(rating.user_flag.get('val'), rating.user_flag.get('detail'), 0))
if rating.changed & Rating.CHANGED_AUTO_FLAG:
requests.append(self._flag_request(rating.auto_flag.get('val'), rating.auto_flag.get('detail'), 1))
try:
conn = httplib.HTTPSConnection(rating_host)
for request in filter(lambda r: r is not None, requests):
request['apikey'] = api_key
request['i'] = indexer_id
conn.request('POST', RATING_URL, urllib.urlencode(request), headers = _headers)
response = conn.getresponse()
response.read()
if response.status == httplib.UNAUTHORIZED:
_warn('Ratings server unauthorized user')
return False
elif response.status != httplib.OK:
_warn('Ratings server failed to process request (%s, %s)' % (response.status, response.reason))
return False
self.ratings[indexer_id].changed = self.ratings[indexer_id].changed & ~rating.changed
_reset_warn()
return True
except:
_warn('Problem accessing ratings server: %s' % rating_host)
return False

12
sabnzbd/rss.py

@ -342,6 +342,10 @@ class RSSQueue(object):
msg = Ta('Do not have valid authentication for feed %s') % feed
logging.info(msg)
return unicoder(msg)
if status >= 500 and status <=599:
msg = Ta('Server side error (server code %s); could not get %s on %s') % (status, feed, uri)
logging.info(msg)
return unicoder(msg)
entries = d.get('entries')
if 'bozo_exception' in d and not entries:
@ -392,11 +396,7 @@ class RSSQueue(object):
if link:
# Make sure spaces are quoted in the URL
if 'nzbclub.com' in link:
link, path = os.path.split(link.strip())
link = '%s/%s' % (link, urllib.quote(path))
else:
link = link.strip().replace(' ','%20')
link = link.strip().replace(' ','%20')
newlinks.append(link)
@ -588,7 +588,7 @@ def _HandleLink(jobs, link, title, flag, orgcat, cat, pp, script, download, star
if special_rss_site(link):
nzbname = None
else:
nzbname = sanitize_foldername(title)
nzbname = title
m = RE_NEWZBIN.search(link)
if m and m.group(1).lower() == 'newz' and m.group(2) and m.group(3):
if download:

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

Loading…
Cancel
Save