diff --git a/README.md b/README.md index 7fbdb48..589b743 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -NZBGet 11.0 for xbian +NZBGet 14.1 for xbian ===================== diff --git a/content/DEBIAN/control b/content/DEBIAN/control index f17ee18..fac6f7d 100644 --- a/content/DEBIAN/control +++ b/content/DEBIAN/control @@ -1,9 +1,9 @@ Origin: XBian Package: xbian-package-nzbget -Version: 11.0 +Version: 14.1 Section: net Priority: optional Architecture: armhf -Depends: xbian-update, libsigc++-dev, libncurses5, libpar2-0, libssl1.0.0, gnutls-bin, libxml2, unrar -Maintainer: sjengfred -Description: NZBGet 11.0 compiled, patched and configured for XBian +Depends: xbian-update, libncurses5, libssl1.0.0, libxml2, unrar +Maintainer: freem@n +Description: NZBGet 14.1 compiled, patched and configured for XBian diff --git a/content/etc/nzbget/nzbget.conf b/content/etc/nzbget/nzbget.conf index 9099c7c..5fd23d8 100644 --- a/content/etc/nzbget/nzbget.conf +++ b/content/etc/nzbget/nzbget.conf @@ -10,6 +10,8 @@ LockFile=/run/nzbget.lock LogFile=/var/log/nzbget.log ConfigTemplate=/usr/local/share/nzbget/nzbget.conf +Server1.Active=yes +Server1.Name= Server1.Level=0 Server1.Group=0 Server1.Host=my.newsserver.com @@ -18,7 +20,7 @@ Server1.Username=user Server1.Password=pass Server1.JoinGroup=no Server1.Encryption=no -Server1.Cipher= +Server1.Cipher=RC4-MD5 Server1.Connections=4 ControlIP=0.0.0.0 @@ -29,46 +31,50 @@ SecureControl=no SecurePort=6791 SecureCert= SecureKey= +AuthorizedIP= DaemonUsername=xbian UMask=1000 AppendCategoryDir=yes -NzbDirInterval=5 +NzbDirInterval=900 NzbDirFileAge=60 -MergeNzb=no -NzbProcess= -NzbAddedProcess= DupeCheck=yes SaveQueue=yes ReloadQueue=yes -ReloadUrlQueue=yes -ReloadPostQueue=yes ContinuePartial=yes +PropagationDelay=0 Decode=yes +ArticleCache=0 DirectWrite=yes +WriteBuffer=0 CrcCheck=yes Retries=3 RetryInterval=10 -ConnectionTimeout=60 +ArticleTimeout=60 +UrlTimeout=60 TerminateTimeout=600 DownloadRate=0 AccurateRate=no -WriteBufferSize=0 DiskSpace=250 DeleteCleanupDisk=no +NzbCleanupDisk=yes KeepHistory=7 -UrlConnections=4 +FeedHistory=7 +UrlConnections=2 +UrlForce=yes Category1.Name=Movies Category1.DestDir= -Category1.DefScript= +Category1.Unpack=yes +Category1.PostScript= +Category1.Aliases= Category2.Name=Series Category3.Name=Music Category4.Name=Software -CreateLog=yes -ResetLog=no +WriteLog=rotate +RotateLog=3 ErrorTarget=both WarningTarget=both InfoTarget=both @@ -77,6 +83,7 @@ DebugTarget=both LogBufferSize=1000 CreateBrokenLog=yes DumpCore=no +TimeCorrection=0 OutputMode=curses CursesNzbName=yes @@ -85,9 +92,14 @@ CursesTime=no UpdateInterval=200 ParCheck=auto +ParRename=yes ParRepair=yes ParScan=limited -StrictParName=yes +ParQuick=yes +ParBuffer=16 +ParThreads=0 +ParIgnoreExt=.sfv, .nzb, .nfo +HealthCheck=delete ParTimeLimit=0 ParPauseQueue=yes ParCleanupQueue=yes @@ -100,6 +112,9 @@ UnpackCleanupDisk=yes UnrarCmd=unrar SevenZipCmd=7z -DefScript= +PostScript= +ScanScript= +QueueScript= ScriptOrder= ScriptPauseQueue=yes +EventInterval=0 diff --git a/content/etc/nzbget/ppscripts/EMail.py b/content/etc/nzbget/ppscripts/EMail.py index 01a7836..aa43f59 100644 --- a/content/etc/nzbget/ppscripts/EMail.py +++ b/content/etc/nzbget/ppscripts/EMail.py @@ -2,7 +2,7 @@ # # E-Mail post-processing script for NZBGet # -# Copyright (C) 2013 Andrey Prygunkov +# Copyright (C) 2013-2014 Andrey Prygunkov # # 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 @@ -18,8 +18,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -# $Revision: 707 $ -# $Date: 2013-06-12 22:25:06 +0200 (Wed, 12 Jun 2013) $ +# $Revision: 1107 $ +# $Date: 2014-08-27 18:27:40 +0200 (Wed, 27 Aug 2014) $ # @@ -56,6 +56,9 @@ # SMTP server password, if required. #Password=mypass +# Append statistics to the message (yes, no). +#Statistics=yes + # Append list of files to the message (yes, no). # # Add the list of downloaded files (the content of destination directory). @@ -92,74 +95,47 @@ POSTPROCESS_ERROR=94 # Check if the script is called from nzbget 11.0 or later -if not 'NZBOP_SCRIPTDIR' in os.environ: +if not 'NZBPP_TOTALSTATUS' in os.environ: print('*** NZBGet post-processing script ***') - print('This script is supposed to be called from nzbget (11.0 or later).') + print('This script is supposed to be called from nzbget (13.0 or later).') sys.exit(POSTPROCESS_ERROR) print('[DETAIL] Script successfully started') sys.stdout.flush() -required_options = ('NZBPO_FROM', 'NZBPO_TO', 'NZBPO_SERVER', 'NZBPO_PORT', 'NZBPO_ENCRYPTION', - 'NZBPO_USERNAME', 'NZBPO_PASSWORD', 'NZBPO_FILELIST', 'NZBPO_BROKENLOG', 'NZBPO_POSTPROCESSLOG') +required_options = ('NZBPO_FROM', 'NZBPO_TO', 'NZBPO_SERVER', 'NZBPO_PORT', 'NZBPO_ENCRYPTION', 'NZBPO_USERNAME', 'NZBPO_PASSWORD') for optname in required_options: if (not optname in os.environ): print('[ERROR] Option %s is missing in configuration file. Please check script settings' % optname[6:]) sys.exit(POSTPROCESS_ERROR) + +status = os.environ['NZBPP_STATUS'] +total_status = os.environ['NZBPP_TOTALSTATUS'] + +# If any script fails the status of the item in the history is "WARNING/SCRIPT". +# This status however is not passed to pp-scripts in the env var "NZBPP_STATUS" +# because most scripts are independent of each other and should work even +# if a previous script has failed. But not in the case of E-Mail script, +# which should take the status of the previous scripts into account as well. +if total_status == 'SUCCESS' and os.environ['NZBPP_SCRIPTSTATUS'] == 'FAILURE': + total_status = 'WARNING' + status = 'WARNING/SCRIPT' -# Check par and unpack status for errors. -success=False -if os.environ['NZBPP_PARSTATUS'] == '1' or os.environ['NZBPP_UNPACKSTATUS'] == '1': - subject = 'Failure for "%s"' % (os.environ['NZBPP_NZBNAME']) - text = 'Download of "%s" has failed.' % (os.environ['NZBPP_NZBNAME']) -elif os.environ['NZBPP_PARSTATUS'] == '4': - subject = 'Damaged for "%s"' % (os.environ['NZBPP_NZBNAME']) - text = 'Download of "%s" requires par-repair.' % (os.environ['NZBPP_NZBNAME']) -else: +success = total_status == 'SUCCESS' +if success: subject = 'Success for "%s"' % (os.environ['NZBPP_NZBNAME']) text = 'Download of "%s" has successfully completed.' % (os.environ['NZBPP_NZBNAME']) - success=True - -# NZBPP_PARSTATUS - result of par-check: -# 0 = not checked: par-check is disabled or nzb-file does -# not contain any par-files; -# 1 = checked and failed to repair; -# 2 = checked and successfully repaired; -# 3 = checked and can be repaired but repair is disabled. -# 4 = par-check needed but skipped (option ParCheck=manual); -parStatus = { '0': 'skipped', '1': 'failed', '2': 'repaired', '3': 'repairable', '4': 'manual' } -text += '\nPar-Status: %s' % parStatus[os.environ['NZBPP_PARSTATUS']] - -# NZBPP_UNPACKSTATUS - result of unpack: -# 0 = unpack is disabled or was skipped due to nzb-file -# properties or due to errors during par-check; -# 1 = unpack failed; -# 2 = unpack successful. -unpackStatus = { '0': 'skipped', '1': 'failed', '2': 'success' } -text += '\nUnpack-Status: %s' % unpackStatus[os.environ['NZBPP_UNPACKSTATUS']] - -# add list of downloaded files -if os.environ['NZBPO_FILELIST'] == 'yes': - text += '\n\nFiles:' - for dirname, dirnames, filenames in os.walk(os.environ['NZBPP_DIRECTORY']): - for filename in filenames: - text += '\n' + os.path.join(dirname, filename)[len(os.environ['NZBPP_DIRECTORY']) + 1:] +else: + subject = 'Failure for "%s"' % (os.environ['NZBPP_NZBNAME']) + text = 'Download of "%s" has failed.' % (os.environ['NZBPP_NZBNAME']) -# add _brokenlog.txt (if exists) -if os.environ['NZBPO_BROKENLOG'] == 'yes': - brokenlog = '%s/_brokenlog.txt' % os.environ['NZBPP_DIRECTORY'] - if os.path.exists(brokenlog): - text += '\n\nBrokenlog:\n' + open(brokenlog, 'r').read().strip() +text += '\nStatus: %s' % status -# add post-processing log -if os.environ['NZBPO_POSTPROCESSLOG'] == 'Always' or \ - (os.environ['NZBPO_POSTPROCESSLOG'] == 'OnFailure' and not success): - # To get the post-processing log we connect to NZBGet via XML-RPC - # and call method "postqueue", which returns the list of post-processing job. - # The first item in the list is current job. This item has a field 'Log', - # containing an array of log-entries. - # For more info visit http://nzbget.sourceforge.net/RPC_API_reference - +if os.environ.get('NZBPO_STATISTICS') == 'yes' or \ + os.environ.get('NZBPO_POSTPROCESSLOG') == 'Always' or \ + (os.environ.get('NZBPO_POSTPROCESSLOG') == 'OnFailure' and not success): + # To get statistics or the post-processing log we connect to NZBGet via XML-RPC. + # For more info visit http://nzbget.net/RPC_API_reference # First we need to know connection info: host, port and password of NZBGet server. # NZBGet passes all configuration options to post-processing script as # environment variables. @@ -175,6 +151,73 @@ # Create remote server object server = ServerProxy(rpcUrl) + +if os.environ.get('NZBPO_STATISTICS') == 'yes': + # Find correct nzb in method listgroups + groups = server.listgroups(0) + nzbID = int(os.environ['NZBPP_NZBID']) + for nzbGroup in groups: + if nzbGroup['NZBID'] == nzbID: + break + + text += '\n\nStatistics:'; + + # add download size + DownloadedSize = float(nzbGroup['DownloadedSizeMB']) + unit = ' MB' + if DownloadedSize > 1024: + DownloadedSize = DownloadedSize / 1024 # GB + unit = ' GB' + text += '\nDownloaded size: %.2f' % (DownloadedSize) + unit + + # add average download speed + DownloadedSizeMB = float(nzbGroup['DownloadedSizeMB']) + DownloadTimeSec = float(nzbGroup['DownloadTimeSec']) + if DownloadTimeSec > 0: # check x/0 errors + avespeed = (DownloadedSizeMB/DownloadTimeSec) # MB/s + unit = ' MB/s' + if avespeed < 1: + avespeed = avespeed * 1024 # KB/s + unit = ' KB/s' + text += '\nAverage download speed: %.2f' % (avespeed) + unit + + def format_time_sec(sec): + Hour = sec/3600 + Min = (sec - (sec/3600)*3600)/60 + Sec = (sec - (sec/3600)*3600)%60 + return '%d:%02d:%02d' % (Hour,Min,Sec) + + # add times + text += '\nTotal time: ' + format_time_sec(int(nzbGroup['DownloadTimeSec']) + int(nzbGroup['PostTotalTimeSec'])) + text += '\nDownload time: ' + format_time_sec(int(nzbGroup['DownloadTimeSec'])) + text += '\nVerification time: ' + format_time_sec(int(nzbGroup['ParTimeSec']) - int(nzbGroup['RepairTimeSec'])) + text += '\nRepair time: ' + format_time_sec(int(nzbGroup['RepairTimeSec'])) + text += '\nUnpack time: ' + format_time_sec(int(nzbGroup['UnpackTimeSec'])) + +# add list of downloaded files +files = False +if os.environ.get('NZBPO_FILELIST') == 'yes': + text += '\n\nFiles:' + for dirname, dirnames, filenames in os.walk(os.environ['NZBPP_DIRECTORY']): + for filename in filenames: + text += '\n' + os.path.join(dirname, filename)[len(os.environ['NZBPP_DIRECTORY']) + 1:] + files = True + if not files: + text += '\n' + +# add _brokenlog.txt (if exists) +if os.environ.get('NZBPO_BROKENLOG') == 'yes': + brokenlog = '%s/_brokenlog.txt' % os.environ['NZBPP_DIRECTORY'] + if os.path.exists(brokenlog): + text += '\n\nBrokenlog:\n' + open(brokenlog, 'r').read().strip() + +# add post-processing log +if os.environ.get('NZBPO_POSTPROCESSLOG') == 'Always' or \ + (os.environ.get('NZBPO_POSTPROCESSLOG') == 'OnFailure' and not success): + # To get the post-processing log we call method "postqueue", which returns + # the list of post-processing job. + # The first item in the list is current job. This item has a field 'Log', + # containing an array of log-entries. # Call remote method 'postqueue'. The only parameter tells how many log-entries to return as maximum. postqueue = server.postqueue(10000) @@ -193,6 +236,8 @@ msg['Subject'] = subject msg['From'] = os.environ['NZBPO_FROM'] msg['To'] = os.environ['NZBPO_TO'] +msg['Date'] = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000") +msg['X-Application'] = 'NZBGet' # Send message print('[DETAIL] Sending E-Mail') diff --git a/content/etc/nzbget/ppscripts/Logger.py b/content/etc/nzbget/ppscripts/Logger.py index 5444826..663ba34 100644 --- a/content/etc/nzbget/ppscripts/Logger.py +++ b/content/etc/nzbget/ppscripts/Logger.py @@ -18,8 +18,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -# $Revision: 707 $ -# $Date: 2013-06-12 22:25:06 +0200 (Wed, 12 Jun 2013) $ +# $Revision: 978 $ +# $Date: 2014-04-04 23:45:48 +0200 (Fri, 04 Apr 2014) $ # @@ -64,7 +64,7 @@ # and call method "postqueue", which returns the list of post-processing job. # The first item in the list is current job. This item has a field 'Log', # containing an array of log-entries. -# For more info visit http://nzbget.sourceforge.net/RPC_API_reference +# For more info visit http://nzbget.net/RPC_API_reference # First we need to know connection info: host, port and password of NZBGet server. # NZBGet passes all configuration options to post-processing script as diff --git a/content/usr/local/bin/nzbget b/content/usr/local/bin/nzbget index 0f96391..5e87e5a 100755 Binary files a/content/usr/local/bin/nzbget and b/content/usr/local/bin/nzbget differ diff --git a/content/usr/local/share/doc/nzbget/AUTHORS b/content/usr/local/share/doc/nzbget/AUTHORS new file mode 100644 index 0000000..fc4921a --- /dev/null +++ b/content/usr/local/share/doc/nzbget/AUTHORS @@ -0,0 +1,27 @@ +NZBGet: + Andrey Prygunkov (versions 0.3.0 and later) + Bo Cordes Petersen (versions ? - 0.2.3) + Sven Henkel (versions 0.1.0 - ?) + +PAR2: + Peter Brian Clements + +PAR2 library API: + Francois Lesueur + +jQuery: + John Resig + The Dojo Foundation + +Bootstrap: + Twitter, Inc + +Raphaël: + Dmitry Baranovskiy + Sencha Labs + +Elycharts: + Void Labs s.n.c. + +iconSweets: + Yummygum \ No newline at end of file diff --git a/content/usr/local/share/doc/nzbget/ChangeLog b/content/usr/local/share/doc/nzbget/ChangeLog index 824817f..7935654 100644 --- a/content/usr/local/share/doc/nzbget/ChangeLog +++ b/content/usr/local/share/doc/nzbget/ChangeLog @@ -1,118 +1,1007 @@ +nzbget-14.1: + - fixed: program could crash during unpack (Posix) or unpack failure + was reported (Windows); + - fixed: quick par-check could hang on certain nzb-files containing multiple + par-sets (occured only in 64 bit mode); + - fixed: menubar icon was not visible on OSX in dark mode; + - system sleep on idle state is now prevented during download and + post-processing (Mac OSX only); + - fixed: unrar may sometimes fail with message "no files to extract" + (certain Linux systems); + - fixed false memory leak warning when compiled in debug mode (Windows only); + +nzbget-14.0: + - added article cache: + - new option "ArticleCache" defines memory limit to use for cache; + - when cache is active the articles are written into cache first and + then all flushed to disk into the destination file; + - article cache reduces disk IO and may reduce file fragmentation + improving post-processing speed (unpack); + - it works with both writing modes (direct write on and off); + - when option "DirectWrite" is disabled the cache should be big enough + (for best performance) to accommodate all articles of one file + (sometimes up to 500 MB) in order to avoid writing articles into + temporary files, otherwise temporary files are used for articles which + do not fit into cache; + - when used in combination with DirectWrite there is no such limitation + and even a small cache (100 MB or even less) can be used effectively; + when the cache becomes full it is flushed automatically (directly into + destination file) providing room for new articles; + - new row in the "statistics and status dialog" in web-interface + indicates the amount of memory used for cache; + - new fields "ArticleCacheLo", "ArticleCacheHi" and "ArticleCacheMB" + returned by RPC-method "status"; + - renamed option "WriteBufferSize" into "WriteBuffer": + - changed the dimension - now option is set in kilobytes instead of bytes; + - old name and value are automatically converted; + - if the size of article is below the value defined by the option, the + buffer is allocated with the articles size (to not waste memory); + - therefore the special value "-1" is not required anymore; during + conversion "-1" is replaced with "1024" (1 megabyte) but it can be of + course manually changed to any other value later; + - integrated par2-module (libpar2) into NZBGet’s source code tree: + - the par2-module is now built automatically during building of NZBGet; + - this eliminates dependency from external libpar2 and libsigc++; + - making it much easier for users to compile NZBGet without patching + libpar2; + - added quick file verification during par-check/repair: + - if par-repair is required for download the files downloaded without + errors are verified quickly by comparing their checksums against the + checksums stored in the par2-file; + - this makes the verification of undamaged files almost instant; + - damaged (partially downloaded) files are also verified quickly by + comparing block's checksums against the checksums stored in the + par2-file; when necessary the small amounts of data is read from files + to calculate block's checksums; + - this makes the verification of damaged files very fast; + - new option "ParQuick" (active by default); + - when quick par verification is active the repaired files are not + verified to save time; the only reason for incorrect files after + repair can be hardware errors (memory, disk) but this is not something + NZBGet should care about; + - if unpack fails (excluding invalid password errors) and quick + par-check does not find any errors or quick par-check was already + performed the full par-check is performed; this helps in certain rare + situations caused by abnormal program termination; + - added multithreading par-repair: + - doesn't depend on other libraries and works everywhere, on all + platforms and all CPUs (with multiple cores), no special compiling + steps are required; + - new option "ParThreads" to set the number of threads for repairing; + - the number of repair threads is automatically reduced to the amount of + bad blocks if there are too few of them; if there is only one bad + block the multithreading par-repair is switched off to avoid overhead + of thread synchronisation (which does not make sense for one working + thread); + - new option "ParBuffer" to define the memory limit to use during + par-repair; + - added support for detection of bad downloads (fakes, etc.): + - queue-scripts are now called after every downloaded file included in + nzb; + - new events "FILE_DOWNLOADED" and "NZB_DOWNLOADED" of parameter + "NZBNA_EVENT"; new env. var "NZBNA_DIRECTORY" passed to queue scripts; + - queue-scripts have a chance to detect bad downloads when the download + is in progress and cancel bad downloads by printing a special command; + downloads marked as bad become status "FAILURE/BAD" and are processed + by the program as failures (triggering duplicate handling); scripts + executed thereafter see the new status and can react accordingly + (inform an indexer or a third-party automation tool); + - when a script marks nzb as bad the nzb is deleted from queue, no + further internal post-processing (par, unrar, etc.) is made for the + nzb but all post-processing scripts are executed; if option + "DeleteCleanupDisk" is active the already downloaded files are deleted; + - new status "BAD" for field "DeleteStatus" of nzb-item in RPC-method + "history"; + - queue-scripts can set post-processing parameters by printing special + command, just like post-processing-scripts can do that; this + simplifies transferring (of small amount) of information between + queue-scripts and post-processing-scripts; + - scripts supporting two modes (post-processing-mode and queue-mode) are + now executed if selected in post-processing parameters: either in + options "PostScript" and "CategoryX.PostScript" or manually on page + "Postprocess" of download details dialog in web-interface; it is not + necessary to select dual-mode scripts in option "QueueScript"; that + provides more flexibility: the scripts can be selected per-category or + activated/deactivated for each nzb individually; + - added option "EventInterval" allowing to reduce the number of calls of + queue-scripts, which can be useful on slow systems; + - queue scripts can define what events they are interested in; this + avoids unnecessary calling of the scripts which do not process certain + events; + - the list of scripts (pp-scripts, queue-scripts, etc.) is now read once on + program start instead of reading everytime a script is executed: + - that eliminates the unnecessary disk access; + - the settings page of web-interface loads available scripts every time + the page is shown; + - this allows to configure newly added scripts without restarting the + program first (just like it was before); a restart is still required + to apply the settings (just like it was before); + - RPC-method "configtemplates" has new parameter "loadFromDisk"; + - options "ParIgnoreExt" and "ExtCleanupDisk" are now respected by par-check + (in addition to being respected by par-rename): if all damaged or missing + files are covered by these options then no par-repair is performed and the + download assumed successful; + - added new search field "dupestatus" for use in rss filters: + - the search is performed through download queue and history testing + items with the same dupekey or title as current rss item; + - the field contains comma-separated list of following possible statuses + (if duplicates were found): QUEUED, DOWNLOADING, SUCCESS, WARNING, + FAILURE or an empty string if there were no matching items found; + - added log file rotation: + - options "CreateLog" and "ResetLog" replaced with new option "WriteLog + (none, append, reset, rotate)"; + - new option "RotateLog" defines rotation period; + - improved joining of splitted files: + - instead of performing par-repair the files are now joined by unpacker, + which is much faster; + - the files splitted before creating of par-sets are now joined as well + (they were not joined in v13 because par-repair has nothing to repair + in this case); + - the unpacker can detect missing fragments and requests par-check if + necessary; + - added per-nzb time and size statistics: + - total time, download, verify, repair and unpack times, downloaded size + and average speed, shown in history details dialog via click on the + row with total size in statistics block; + - RPC-methods "listgroups" and "history" return new fields: + "DownloadedSizeLo", "DownloadedSizeHi", "DownloadedSizeMB", + "DownloadTimeSec", "PostTotalTimeSec", "ParTimeSec", "RepairTimeSec", + "UnpackTimeSec"; + - pp-script "EMail.py" now supports mail server relays (thanks to l2g for the + patch); + - when compiled in debug mode new field "process id" is printed to the file + log for each row (it is easier to identify processes than threads); + - if an nzb has only few failed articles it may have completion shown as 100%; + now it is shown as 99.9% to indicate that not everything was successfully + downloaded; + - updated configure-script to not require gcrypt for newer GnuTLS versions + (when gcrypt is not needed); + - for downloads delayed due to propagation delay (option "PropagationDelay") + a new badge "propagation" is now shown near download name; + - added new option "UrlTimeout" to set timeout for URL fetching and RSS feed + fetching; renamed option "ConnectionTimeout" to "ArticleTimeout"; + - improved pp-script EMail.py: now it can send time statistics (thanks to JVM + for the patch); + - improvement in duplicate check: + - if a new download with empty dupekey and empty dupescore is marked as + "dupe" and the another download with the same name have non empty + dupekey or dupescore these properties are copied from that download; + - this is useful because the new download is most likely another upload + of the same file and it should have the same duplicate properties for + best duplicate handling results; + - when connecting in remote mode using command line parameter "--connect/-C" + the option "ControlIP" is now interpreted as "127.0.0.1" if it is set to + "0.0.0.0" (instead of failing with an error message); + - when option "ContinuePartial" is active the current state is saved not more + often than once per second instead of after every downloaded article; this + significantly reduce the amount of disk writings on high download speeds; + - added commands "PausePostProcess" and "UnpausePostProcess" to scheduler; + - unpack is now automatically immediately aborted if unrar reports CRC errors; + - unpack is now immediately aborted if unrar reports wrong password (works for + rar5 as well as for older formats); the unpack error status "PASSWORD" is + now set for older formats too (not only rar5); + - improved cleanup: + - disk cleanup is now not performed if unrar failed even if par-check + was successful; + - queue cleanup (for remaining par2-files) is now made more smarter: the + files are kept (parked) if they can be used by command "post-process + again" and are removed otherwise; + - improved scan-scripts: if the category of nzb-file is changed by the + scan-script the assigned post-processing scripts are now automatically reset + according to the new category; + - added missing new line character at the end of the help screen printed by + "nzbget -h"; + - better error reporting if a temp file could not be found; + - added news server name to message "Cancelling hanging download ..." to help + identifying problematic servers; + - added column "age" to history tab in web-interface; + - debug builds for Windows now print call stack on crash to the log-file, + which is very useful for debugging; + - additional parameters (env. vars) are now passed to scan scripts: + NZBNP_DUPEKEY, NZBNP_DUPESCORE, NZBNP_DUPEMODE; scan-scripts can now set + dupekey, dupemode and dupescore by printing new special commands; + - fixed potential crash which could happen in debug mode during program + restart; + - fixed: program could crash during restart if an extension script was + running; now all active scripts are terminated during restart; + - fixed: RPC-method "editqueue" with action "HistoryReturn" caused a crash if + the history item did not have any remaining (parked) files; + - fixed: RPC-method "saveconfig" did not work via XML-RPC (but worked via + JSON-RPC); + - fixed: a superfluous comma at the end of option "TaskX.Time" was interpreted + as an error or may cause a crash; + - fixed: relative destination paths (options "DestDir" and + "CategoryX.DestDir") caused failures during unrar; + - fixed: splitted .cbr-files were not properly joined; + - fixed: inner files (files listed in nzb) bigger than 2GB could not be + downloaded; + - fixed: cleanup may leave some files undeleted (Mac OSX only); + - fixed: compiler error if configured using parameter "--disable-gzip"; + - fixed: one log-message was printed only to global log but not to nzb-item + pp-log; + - fixed: par-check could fail on valid files (bug introduced in libpar2 0.3); + - fixed: scheduler tasks were not checked after wake up if the sleep time was + longer than 90 minutes; + - fixed: the "pause extra pars"-state was missing in the pause/resume-loop of + curses interface, key "P"; + - fixed: web interface showed an error box when trying to submit files with + extensions other than .nzb, although these files could be processed by a + scan-script; now the error is not shown if any scan-script is set in options; + +nzbget-13.0: + - reworked download queue: + - new dialog to build filters in web-interface with instant preview; + - queue now holds nzb-jobs instead of individual files (contained + within nzbs); + - this drastically improves performance when managing queue + containing big nzb-files on operations such as pause/unpause/move items; + - tested with queue of 30 nzb-files each 40-100GB size (total queue + size 1.5TB) - queue managing is fast even on slow device; + - limitation: individual files (contained within nzbs) now cannot + be moved beyond nzb borders (in older version it was possible to + move individual files freely and mix files from different nzbs, + although this feature was not supported in web-interface and + therefore was not much known); + - this change opens doors for further speed optimizations and integration + of download queue with post-processing queue and possibly url-queue; + - current download data such as remained size or size of paused files + is now internally automatically updated on related events (download + of article is completed, queue edited, etc.); + - this eliminates the need of calculating this data upon each + RPC-request (from web-interface) and greatly decrease CPU load + of processing RPC-requests when having large download queue + (and/or large nzb-files in queue); + - field "Priority" was removed from individual files; + - instead nzb-files (collections) now have field "Priority"; + - nzb-files now also have new fields "MinTime" and "MaxTime", which + are set when nzb-file is parsed and then kept; + - this eliminates the need of recalculation file statistics (min and + max priority, min and max time); + - removed action "FileSetPriority" from RPC-command "editqueue"; + - removed action "I" from remote command "--edit/-E" for individual + files (now it is allowed for groups only); + - removed few (not more necessary) checks from duplicate manager; + - merged post-processing queue into main download queue; + - changing the order of (pp-queued) items in the download queue + now also means changing the order of post-processing jobs; + - priorities of downloads are now respected when picking the next + queued post-processing job; + - the moving of download items in web-interface is now allowed for + downloads queued for post-processing; + - removed actions of remote command "--edit/-E" and of RPC-method + "editqueue" used to move post-processing jobs in the post-processing + queue (the moving of download items should be used instead); + - remote command "-E/--edit" and RPC-method "editqueue" now use NZBIDs + of groups to edit groups (instead of using ID of any file in the + group as in older versions); + - remote command "-L/--list" for groups (G) and group-view in + curses-frontend now print NZBIDs instead of "FirstID-LastID"; + - RPC-method "listgroups" returns NZBIDs in fields "FirstID" and "LastID", + which are usually used as arguments to "editqueue" (for compatibility + with existing third-party software); + - items queued for post-processing and not having any remaining files + now can be edited (to cancel post-processing), which was not possibly + before due to lack of "LastID" in empty groups; + - edit commands for download queue and post-processing queue are now + both use the same IDs (NZBIDs); + - merged url queue into main download queue; + - urls added to queue are now immediately shown in web-interface; + - urls can be reordered and deleted; + - when urls are fetched the downloaded nzb-files are put into queue at + the positions of their urls; + - this solves the problem with fetched nzb-files ordered differently + than the urls if the fetching of upper (position wise) urls were + completed after the lower urls; + - removed options "ReloadUrlQueue" and "ReloadPostQueue" since there + are no separate url- and post-queues anymore; + - nzb-files added via urls have new field "URL" which can be accessed + via RPC-methods "listgroups" and "history"; + - new env. var. "NZBNP_URL", "NZBNA_URL" and "NZBPP_URL" passed to + scan-, queue- and pp-scripts; + - removed remote command "--list U", urls are now shown as groups + by command "--list G"; + - RPC-method "urlqueue" is still supported for compatibility but + should not be used since the urls are now returned by method + "listgroups", the entries have new field "Kind" which can be + "NZB" or "URL"; + - added collecting of download volume statistics data per news server: + - in web-interface the data is shown as chart in "Statistics and + Status" dialog; + - new RPC-method "servervolumes" returns the collected data; + - new RPC-method "resetservervolume" to reset the custom counter; + - fast par-renamer now automatically detects and renames misnamed (obfuscated) + par2-files; + - for downloads not having any (obviously named) par2-files the critical + health is assumed 85% instead of 100% as the absense of par2-files suggests: + - this avoids the possibly false triggering of health-check action + (delete or pause) for downloads having misnamed (obfuscated) par2-files; + - combined with improved fast par-renamer this provides proper + processing of downloads with misnamed (obfuscated) par2-files; + - fast par-renamer now detects missing files (files listed in par2-files + but not present on disk): + - when checking for missing files the files whose extensions match + with option "ExtCleanupDisk" are ignored now (to avoid time consuming + restoring of files which will be deleted later anyway); + - added option "ParIgnoreExt" which lists files which do not trigger + par-repair if they are missing (similar to option "ExtCleanupDisk" + but those files are not deleted during cleanup); + - added new choice "Always" for option "ParCheck": + - it forces the par-check for every (even undamaged) download but + in contrast to choice "Force" only one par2-file is downloaded first; + - additional files are downloaded if needed; + - improved par-check for damaged collections with multiple par-sets and + having missing files: + - only orphaned files (not belonging to any par-set) are scanned + when looking for missing files; + - this greatly decrease the par-check time for big collections; + - eliminated the distinction between manual pause and soft-pause: + - there is only one pause register now; + - options "ParPauseQueue", "UnpackPauseQueue" and "ScriptPauseQueue" + do not change the state of the pause but instead are respected directly; + - RPC-methods "pausedownload2" and "resumedownload2" are aliases to + "pausedownload" and "resumedownload" (kept for compatibility); + - field "Download2Paused" of RPC-method "status" is an alias to + "DownloadPaused" (kept for compatibility); + - action "D2" of remote commands "--pause/-P" and "--unpause/-U" + is not supported anymore; + - implemented general scripts concept: + - the concept is a logical extension of the post-processing scripts + concept initially introduced in v11; + - the general scripts concept applies to all scripts used in the + program: scan-script, queue-script and scheduler-script (in + addition to post-processing scripts); + - option "NzbProcess" renamed to "ScanScript"; + - option "NzbAddedProcess" renamed to "QueueScript"; + - option "DefScript" and "CategoryX.DefScript" renamed to + "PostScript" and "CategoryX.PostScript" (options with old names + are recognized and automatically converted on first settings saving); + - new option "TaskX.Script"; + - old option "TaskX.Process" kept for scheduling of external + programs not related to nzbget (to avoid writing of intermediate + proxy scripts); + - scan-script, queue-script and scheduler-script now work similar + to post-processing scripts: + - scripts must be put into scripts-directory; + - scripts can be configured via web-interface and can have options; + - multiple scripts can be chosen for each scripts-option, all + chosen scripts are executed; + - program and script options are passed to the script as + env. variables; + - renamed default directory with scripts from "ppscripts" to "scripts"; + - script signature indicates the type of script (post-processing, + scan, queue or scheduler); + - one script can have mixed signature allowing it to be used for + multiple purposes (for example a notification script can send + a notification on both events: after adding to queue and after + post-processing); + - result of RPC-method "configtemplates" has new fields "PostScript", + "ScanScript", "QueueScript", "SchedulerScript" to indicate the + purpose of the script; + - queue-script (formerly NzbAddedProcess) has new parameter + "NZBNA_EVENT" indicating the reason of calling the script; + currently the script is called only after adding of files to + download queue and therefore the parameter is always set to + "NZB_ADDED" but the queue-script can be called on other events + in the future too; + - post-processing scripts now have two new parameters: + - env. var "NZBPP_STATUS" indicates the status of download including + the total status (SUCCESS, FAILURE, etc.) and the detail field + (for example in case of failures: PAR, UNPACK, etc.); + - env. var "NZBPP_TOTALSTATUS" is equal to the total status of + parameter "NZBPP_STATUS" and is provided for convenience (to avoid + parsing of "NZBPP_STATUS"); + - the new parameters provide a simple way for pp-scripts to determine + download status without a guess work needed in previous versions; + - parameters "NZBPP_PARSTATUS" and "NZBPP_UNPACKSTATUS" are now + considered deprecated (still passed for compatibility); + - updated script "EMail.py" to use new parameters "NZBPP_TOTALSTATUS" + and "NZBPP_STATUS" instead of "NZBPP_PARSTATUS" and "NZBPP_UNPACKSTATUS"; + - when changing category in web-interface the post-processing parameters + are now automatically updated according to new category settings: + - only parameters which are different in old and new category are changed; + - parameters which present in both or in neither categories are not changed; + - that ensures that only the relevant parameters are updated and + parameters which were manually changed by user remain they settings + when it make sense; + - in the "download details dialog" the new parameters are updated + on the postprocess-tab directly after changing of category and + can be controlled before saving; + - in the "edit multiple downloads dialog" the parameters are updated + individually for each download on saving; + - new action "CP" of remote command "--edit/-E" for groups to set + category and apply parameters; + - new action "GroupApplyCategory of RPC-method "editqueue" for + the same purpose; + - changed the way option "ContinuePartial" works: + - now the information about completed articles is stored in a + special file in QueueDir; + - when option "DirectWrite" is active no separate flag-files per + article are created in TempDir; + - the file contains additional information, which were not + stored/available before; + - improved per-server/per-nzb article completion statistics: + - the statistics are now available for active downloads in details + dialog (not only for history); + - the info on that page is constantly updated as long as the page + is active (unless refresh is disabled); + - download age info removed from details dialog to save place + (it is shown in the download list anyway); + - if backup news-servers start to be used for nzb-file a badge + appears in the download list showing the percentage of articles + downloaded from backup servers; + - click on the badge opens download details dialog directly on + the completion page; + - per-server/per-nzb article completion statistics are now + available via RPC-method "listgroups" for active downloads + (not only for "history"); + - improved RPC-API: + - RPC-method "listgroups" now returns info about post-processing + similar to info returned by method "postqueue"; + - RPC-method "postqueue" is obsolete now; + - web-interface requires less requests to NZBGet on each page + update and it is now easier for third-party developers to obtain + the info about download and post-processing status (no need to + merge download queue and post queue); + - RPC-method "listgroups" now returns new field "Status" making it + easier for third-party apps to determine the status of download entry; + - new field "Status" in RPC-method "history" to allow third-party + apps easier determine the status of an item without inspecting + status-fields of every processing step; + - changed web-interface to use new field "Status"; + - method "append" now returns id of added nzb-file or "0" on an error; + - this makes it easier for third-party apps to track added nzb-files; + - for backward compatibility with older software expecting a boolean + result the old version of method "append" is still supported; + - the new version of method "append" has a different signature + (order of parameters); + - parameter "content" can now be either nzb-file content (encoded + in base 64) or an URL; + - this makes the method "appendurl" obsolete (still supported + for compatibility); + - if an URL was added to queue the queue entry created for fetched + nzb-file has the same "NZBID" for easier tracking; + - added force-priorities: + - downloads with priorities equal to or greater than 900 are + downloaded and post-processed even if the program is in paused + state (force mode); + - in web-interface the combo for choosing priority has new entry + "force" (priority value 900); + - new fields "ForcedSizeLo", "ForcedSizeHi" and "ForcedSizeMB" + returned by RPC-method "status"; + - history items now preserve "NZBID" from queue items; that makes + the tracking of items across queue and history easier for + third-party apps; + - field "NZBID" returned by RPC-method "history" is now available + for history items of all kinds (NZB, URL, DUP); field "ID" is + deprecated and should not be used; + - post-processing scripts which move the whole download into a new + location can inform the program about new location using command + "[NZB] DIRECTORY=/new/path", allowing other scripts to process files further; + - added support for power management on windows to avoid pc going into + sleep mode during download or post-processing; + - apostrophe is not considered an invalid file name character anymore; + - adjusted modules initialization to avoid possible bugs due to delayed + thread starts; + - reorganized source code directory structure: created directory "daemon" + with several subdirectories and put all source code files there; + - added new option "PropagationDelay", which sets the minimum post age + to download; newer posts are kept on hold in download queue until + they get older than the defined delay, after that they are downloaded; + - download speeds above 1024 KB/s are now indicated in MB/s; + - data sizes above 1000 GB are now shown as TB in web-interface (instead of GB); + - splitted files are now joined automatically (again); + - adjusted modules initialization to avoid possible bugs due to delayed + thread starts; + - extended info printed by remote command "nzbget -B dump" (for debug + purposes); + - eliminated loop waiting time in queue coordinator on certain + conditions - may improve performance on very high speed connections; + - increased few wait intervals which were unnecessary too small; + - improved error reporting: added error check when closing article + file for writing and when deleting files or directories; + - when building nzbget if both OpenSSL and GnuTLS are available now + using OpenSSL by default (the preferred library can still be selected + with configure-parameter --with-tlslib=OpenSSL/GnuTLS); + - windows version is now configured to use OpenSSL instead of GnuTLS; + windows binaries provided on download page now use OpenSSL; + - column "age" in web-interface now shows minutes for recent posts + (instead of "0 h"); + - remote command "-B dump" now can be used also in release (non-debug) + versions and prints useful debug data as "INFO" instead of "DEBUG"; + - to detect daylight saving activation/deactivation the time zone + information is now checked every minute if a download is active or + once in 3 hours if the program is in stand-by; these delays should + work well with hibernation mode on synology); + - pp-script "EMail.py" now takes the status of previous pp-scripts + into account and report a failure if any of the scripts has failed; + - updated all links to go to new domain (nzbget.net); + - impoved error reporting if unpacker or par-renamer fail to move files; + - removed libpar2-patches from NZBGet source tree; the documentation + now suggests to use the libpar2 version maintained by Debian/Ubuntu + team, which already includes all necessary patches; + - removed patches to create libpar2 and libsigc++ project files for + Visual Studio on Windows, no one needed them anyway; + - fixed: the program could crash during cleanup if files with invalid + timestamps were found in the directory (windows only); + - fixed: RSS feed preview dialog displayed slightly incorrect post + ages because of the wrong time zone conversion; + - fixed: sometimes URLs were removed too early from the feed history + causing them to be detected as "new" and fetched again; if duplicate + check was not active the same nzb-files could be downloaded again; + - fixed: strange (damaged?) par2-files could cause a crash during par-renaming; + - fixed: damaged nzb-files containing multiple par-sets and not having + enough par-blocks could cause a crash during par-check; + - fixed: if during par-repair the downloaded extra par-files were damaged + and the repair was terminated with failure status the post-processing + scripts were executed twice sometimes; + - fixed: post-processing scripts were not executed in standalone mode + ("nzbget /path/to/file.nzb"); + - fixed: renaming or deleting of temporary files could fail, especially + when options "UnpackPauseQueue" and "ScriptPauseQueue" were not active + (windows only); + - fixed: per-server/per-nzb article completion statistics could be + inaccurate for nzb-files whose download were interrupted by reload/restart; + - fixed: after deleting servers from config file the program could crash + on start when loading server volume statistics data from disk; + - fixed: download speeds above approx. 70 MB/s were not indicated + correctly in web-interface and by RPC-method "status"; + - fixed: cancelling of active par-job sometimes didn't work; + - fixed: par-check could hang on renamed and splitted files; + - fixed: the program could crash during parsing of malformed nzb-files; + - fixed: errors during loading of queue from disk state may render the + already loaded parts useless too; now at least these parts of queue are used; + - fixed: queue was not locked during loading on program start and that + could cause problems; + - fixed: data sizes exactly equal to 10, 100, 1000 MB or GB were formatted + using 4 digits instead of 3 (one digit after decimal point too much); + - fixed: if post-processing step "move" failed, the command "post-process + again" did not try to move again; + - fixed: nzb-files were sometimes not deleted from NzbDir (option + "NzbCleanupDisk"); + - fixed: scheduler command "FetchFeed" did not work properly with + parameter "0" (fetch all feeds). + - fixed: port number was not sent in headers when downloading from URLs + which could cause issues with RSS for web-sites using non-standard http ports; + - fixed: queued nzb-files was not deleted from disk when deleting + download without history tracking; + +nzbget-12.0: + - added RSS feeds support: + - new options "FeedX.Name", "FeedX.URL", "FeedX.Filter", + "FeedX.Interval", "FeedX.PauseNzb", "FeedX.Category", + "FeedX.Priority" (section "Rss Feeds"); + - new option "FeedHistory" (section "Download Queue"); + - button "Preview Feed" on settings tab near each feed definition; + - new toolbar button "Feeds" on downloads tab with menu to + view feeds or fetch new nzbs from all feeds (the button is + visible only if there are feeds defined in settings); + - new dialog to see feed content showing status of each item (new, + fetched, backlog) with ability to manually fetch selected items; + - powerful filters for RSS feeds; + - new dialog to build filters in web-interface with instant preview; + - added download health monitoring: + - health indicates download status, whether the file is damaged + and how much; + - 100% health means no download errors occurred; 0% means all + articles failed; + - there is also a critical health which is calculated for each + nzb-file based on number and size of par-files; + - if during download the health goes down below 100% a health + badge appears near download name indicating the necessity of + par-repair; the indicator can be orange (repair may be possible) + or red (unrepairable) if the health goes down below critical health; + - new option "HealthCheck" to define what to do with unhealthy + (unrepairable) downloads (pause, delete, none); + - health and critical health are displayed in download-edit dialog; + health is displayed in history dialog; if download was aborted + (HealthCheck=delete) this is indicated in history dialog; + - health allows to determine download status for downloads which + have unpack and/or par-check disabled; for such downloads the + status in history is shown based on health: success (health=100%), + damaged (health > critical) or failure (health < critical); + - par-check is now automatically started for downloads having + health below 100%; this works independently of unpack (even if + unpack is disabled); + - for downloads having health less than critical health no par-check + is performed (it would fail); Instead the par-check status is + set to "failure" automatically saving time of actual par-check; + - new fields "Health" and "CriticalHealth" are returned by + RPC-Method "listgroups"; + - new fields "Health", "CriticalHealth", "Deleted" and "HealthDeleted" + are returned by RPC-Method "history"; + - new parameters "NZBPP_HEALTH" and "NZBPP_CRITICALHEALTH" are passed + to pp-scripts; + - added collecting of server usage statistical data for each download: + - number of successful and failed article downloads per news server; + - new page in history dialog shows collected statistics; + - new fields in RPC-method "history": ServerStats (array), + TotalArticles, SuccessArticles, FailedArticles; + - new env. vars passed to pp-scripts: NZBPP_TOTALARTICLES, + NZBPP_SUCCESSARTICLES, NZBPP_FAILEDARTICLES and per used news + server: NZBPP_SERVERX_SUCCESSARTICLES, NZBPP_SERVERX_FAILEDARTICLES; + - also new env.var HEALTHDELETED; + - added smart duplicates feature: + - mostly for use with RSS feeds; + - automatic detection of duplicate nzb-files to avoid download of + duplicates; + - nzb-files can be also manually marked as duplicates; + - if download fails - automatically choose another release (duplicate); + - if download succeeds all remaining duplicates are skipped (not downloaded); + - download items have new properties to tune duplicate handling + behavior: duplicate key, duplicate score and duplicate mode; + - if download was deleted by duplicate check its status in the + history is shown as "DUPE"; + - new actions "GroupSetDupeKey", "GroupSetDupeScore", "GroupSetDupeMode", + "HistorySetDupeKey", "HistorySetDupeScore", "HistorySetDupeMode", + "HistoryMarkBad" and "HistoryMarkGood" of RPC-command "editqueue"; + new actions "B" and "G" of command "--edit/-E" for history items + (subcommand "H"); + - when deleting downloads from queue there are three options now: + "move to history", "move to history as duplicate" and "delete + without history tracking"; + - new actions "GroupDupeDelete", "GroupFinalDelete" and + "HistorySetDupeBackup" in RPC-method "editqueue"; + - RPC-commands "listgroups", "postqueue" and "history" now return + more info about nzb-item (many new fields); + - removed option "MergeNzb" because it conflicts with duplicate + handling, items can be merged manually if necessary; + - automatic detection of exactly same nzb-files (same content) + coming from different sources (different RSS feeds etc.); + individual files (inside nzb-file) having extensions listed in + option "ExtCleanupDisk" are excluded from content comparison + (unless these are par2-files, which are never excluded); + - when history item expires (as defined by option "KeepHistory") + and the duplicate check is active (option "DupeCheck") the item + is not completely deleted from history; instead the amount of + stored data reduces to minimum required for duplicate check + (about 200 bytes vs 2000 bytes for full history item); + - such old history items are not shown in web-interface by default + (to avoid transferring of large amount of history items); + - new button "Hidden" in web-interface to show hidden history items; + the items are marked with badge "hidden"; + - RPC-method "editqueue" has now two actions to delete history + records: "HistoryDelete", "HistoryFinal"; action "HistoryDelete" + which has existed before now hides records, already hidden records + are ignored; + - added functions "Mark as Bad" and "Mark as Good" for history + items; + - duplicate properties (dupekey, dupescore and dupemode) can now + be viewed and changed in download-edit-dialog and + history-edit-dialog via new button "Dupe"; + - for full documentation see http://nzbget.net/RSS#Duplicates; + - created NZBGet.app - NZBGet is now a user friendly Mac OSX application + with easy installation and seamless integration into OS UI: + works in background, is controlled from a web-browser, few + important functions are accessible via menubar icon; + - better Windows package: + - unrar is included; + - several options are set to better defaults; + - all paths are set as relative paths to program directory; + the program can be started after installation without editing + anything in config; + - included two new batch-files: + - nzbget-start.bat - starts program in normal mode (dos box); + - nzbget-recovery-mode.bat - starts with empty password (dos box); + - both batch files open browser window with correct address; + - config-file template is stored in nzbget.conf.template; + - nzbget.conf is not included in the package. When the program is + started for the first time (using one of batch files) the template + config file is copied into nzbget.conf; + - updates will be easy in the future: to update the program all + files from newer archive must be extracted over old files. Since + the archive doesn't have nzbget.conf, the existing config is kept + unmodified. The template config file will be updated; + - added file README-WINDOWS.txt with small instructions; + - version string now includes revision number (like "r789"); + - added automatic updates: + - new button "Check for updates" on settings tab of web-interface, + in section "SYSTEM", initiates check and shows dialog allowing to + install new version; + - it is possible to choose between stable, testing and development + branches; + - this feature is for end-users using binary packages created and + updated by maintainers, who need to write an update script specific + for platform; + - the script is then called by NZBGet when user clicks on install-button; + - the script must download and install new version; + - for more info visit http://nzbget.net/Packaging; + - news servers can now be temporarily disabled via speed limit dialog + without reloading of the program: + - new option "ServerX.Active" to disable servers via settings; + - new option "ServerX.Name" to use for logging and in UI; + - changed the way how option "Unpack" works: + - instead of enabling/disabling the unpacker as a whole, it now + defines the initial value of post-processing parameter "Unpack" + for nzb-file when it is added to queue; + - this makes it now possible to disable Unpack globally but still + enable it for selected nzb-files; + - new option "CategoryX.Unpack" to set unpack on a per category basis; + - combined all footer buttons into one button "Actions" with menu: + - in download-edit-dialog: "Pause/Resume", "Delete" and "Cancel + Post-Processing"; + - in history-dialog: "Delete", "Post-Process Again" and "Download + Remaining Files (Return to Queue)"; + - DirectNZB headers X-DNZB-MoreInfo and X-DNZB-Details are now processed + when downloading URLs and the links "More Info" and "Details" are shown + in download-edit-dialog and in history-dialog in Actions menu; + - program can now be stopped via web-interface: new button "shutdown" + in section "SYSTEM"; + - added menu "View" to settings page which allows to switch to "Compact Mode" + when option descriptions are hidden; + - added confirmation dialog by leaving settings page if there are unsaved + changes; + - downloads manually deleted from queue are shown with status "deleted" + in the history (instead of "unknown"); + - all table columns except "Name" now have fixed widths to avoid annoying + layout changes especially during post-processing when long status messages + are displayed in the name-column; + - added filter buttons to messages tab (info, warning, etc.); + - added automatic par-renaming of extracted files if archive includes + par-files; + - added support for http redirects when fetching URLs; + - added new command "Download again" for history items; new action + "HistoryRedownload" of RPC-method "editqueue"; for controlling via command + line: new action "A" of subcommand "H" of command "--edit/-E"; + - download queue is now saved in a more safe way to avoid potential loss + of queue if the program crashes during saving of queue; + - destination directory for option "CategoryX.DestDir" is not checked/created + on program start anymore (only when a download starts for that category); + this helps when certain categories are configured for external disks, + which are not always connected; + - added new option "CategoryX.Aliases" to configure category name matching + with nzb-sites; especially useful with rss feeds; + - in RPC-Method "appendurl" parameter "addtop" adds nzb to the top of + the main download queue (not only to the top of the URL queue); + - new logo (thanks to dogzipp for the logo); + - added support for metatag "password" in nzb-files; + - pp-scripts which move files can now inform the program about new + location by printing text "[NZB] FINALDIR=/path/to/files"; the final + path is then shown in history dialog instead of download path; + - new env-var "NZBPP_FINALDIR" passed to pp-scripts; + - pp-scripts can now set post-processing parameters by printing + command "[NZB] NZBPR_varname=value"; this allows scripts which are + executed sooner to pass data for scripts executed later; + - added new option "AuthorizedIP" to set the list of IP-addresses which + may connect without authorization; + - new option "ParRename" to force par-renaming as a first post-processing + step (active by default); this saves an unpack attempt and is even more + useful if unpack is disabled; + - post-processing progress label is now automatically trimmed if it + doesn't fill into one line; this avoids layout breaking if the text + is too long; + - reversed the order of priorities in comboboxes in dialogs: the highest + priority - at the top, the lowest - at the bottom; + - small changes in button captions: edit dialogs called from settings + page (choose script, choose order, build rss filter) now have buttons + "Discard/Apply" instead of "Close/Save"; in all other dialogs button + "Close" renamed to "Cancel" unless it was the only button in dialog; + - small change in css: slightly reduced the max height of modal dialogs + to better work on notebooks; + - options "DeleteCleanupDisk" and "NzbCleanupDisk" are now active by + default (in the example config file); + - extended add-dialog with options "Add paused" and "Disable duplicate check"; + - source nzb-files are now deleted when download-item leaves queue and + history (option "NzbCleanupDisk"); + - when deleting downloads from queue the messages about deleted + individual files are now printed as "detail" instead of "info"; + - failed article downloads are now logged as "detail" instead of + "warning" to reduce number of warnings for downloads removed from + server (DMCA); one warning is printed for a file with a summary of + number of failed downloads for the file; + - tuned algorithm calculating maximum threads limit to allow more + threads for backup server connections (related to option "TreadLimit" + removed in v11); this may sometimes increase speed when backup servers + were used; + - by adding nzb-file to queue via RPC-methods "append" and "appendurl" + the actual format of the file is checked and if nzb-format is detected + the file is added even if it does not have .nzb extension; + - added new option "UrlForce" to allow URL-downloads (including fetching + of RSS feeds and nzb-files from feeds) even if download is paused; + the option is active by default; + - destination directory for option "DestDir" is not checked/created on + program start anymore (only when a download starts); this helps when + DestDir is mounted to a network drive which is not available on program start; + - added special handling for files ".AppleDouble" and ".DS_Store" during + unpack to avoid problems on NAS having support for AFP protocol (used + on Mac OSX); + - history records with failed script status are now shown as "PP-FAILURE" + in history list (instead of just "FAILURE"); + - option "DiskSpace" now checks space on "InterDir" in addition to + "DestDir"; + - support for rar-archives with non-standard extensions is now limited + to file extensions consisting of digits; this is to avoid extracting + of rar-archives having non-rar extensions on purpose (example: .cbr); + - if option "ParRename" is disabled (not recommended) unpacker does + not initiate par-rename anymore, instead the full par-verify is + performed then; + - for external script the exec-permissions are now added automatically; + this makes the installation of pp-scripts and other scripts easier; + - option "InterDir" is now active by default; + - when option "InterDir" is used the intermediate destination directory + names now include unique numbers to avoid several downloads with same + name to use the same directory and interfere with each other; + - when option "UnpackCleanupDisk" is active all archive files are now + deleted from download directory without relying on output printed by + unrar; this solves issues with non-ascii-characters in archive file + names on some platforms and especially in combination with rar5; + - improved handling of non-ascii characters in file names on windows; + - added support for rar5-format when checking signatures of archives + with non-standard file extensions; + - small restructure in settings order: + - combined sections "REMOTE CONTROL" and "PERMISSIONS" into one + section with name "SECURITY"; + - moved sections "CATEGORIES" and "RSS FEEDS" higher in the + section list; + - improved par-check: if main par2-file is corrupted and can not be + loaded other par2-files are downloaded and then used as replacement + for main par2-file; + - if unpack did not find archive files the par-check is not requested + anymore if par-rename was already done; + - better handling of obfuscated nzb-files containing multiple files + with same names; removed option "StrictParName" which was not working + good with obfuscated files; if more par-files are required for repair + the files with strict names are tried first and then other par-files; + - added new scheduler commands "ActivateServer", "DeactivateServer" and + "FetchFeed"; combined options "TaskX.DownloadRate" and "TaskX.Process" + into one option "TaskX.Param", also used by new commands; + - added status filter buttons to history page; + - if unpack fails with write error (usually because of not enough space + on disk) this is shown as status "Unpack: space" in web-interface; + this unpack-status is handled as "success" by duplicate handling + (no download of other duplicate); also added new unpack-status "wrong + password" (only for rar5-archives); env.var. NZBPP_UNPACKSTATUS has + two new possible values: 3 (write error) and 4 (wrong password); + updated pp-script "EMail.py" to support new unpack-statuses; + - fixed a potential seg. fault in a commonly used function; + - added new option "TimeCorrection" to adjust conversion from system + time to local time (solves issues with scheduler when using a + binary compiled for other platform); + - NZBIDs are now generated with more care avoiding numbering holes + possible in previous versions; + - fixed: invalid "Offset" passed to RPC-method "editqueue" or command + line action "-E/--edit" could crash the program; + - fixed: crash after downloading of an URL (happen only on certain systems); + - fixed: restoring of settings didn't work for multi-sections (servers, + categories, etc.) if they were empty; + - fixed: choosing local files didn't work in Opera; + - fixed: certain characters printed by pp-scripts could crash the + program; + - fixed: malformed nzb-file could cause a memory leak; + - fixed: when a duplicate file was detected in collection it was + automatically deleted (if option DupeCheck is active) but the + total size of collection was not updated; + - when deleting individual files the total count of files in collection + was not updated; + - fixed: when multiple nzb-files were added via URL (rss including) at + the same time the info about category and priority could get lost for + some of files; + - fixed: if unpack fails the created destination directory was not + automatically removed (only if option "InterDir" was active); + - fixed scrolling to the top of page happening by clicking on items in + downloads/history lists and on action-buttons in edit-download and + history dialogs; + - fixed potential buffer overflow in remote client; + - improved error reporting when creation of temporary output file fails; + - fixed: when deleting download, if all remaining queued files are + par2-files the disk cleanup should not be performed, but it was + sometimes; + - fixed a potential problem in incorrect using of one library function. + nzbget-11.0: - - reworked concept of post-processing scripts: - - multiple scripts can be assigned to each nzb-file; + - reworked concept of post-processing scripts: + - multiple scripts can be assigned to each nzb-file; - all assigned scripts are executed after the nzb-file is - downloaded and internally processed (unpack, repair); - - option is obsolete; + downloaded and internally processed (unpack, repair); + - option is obsolete; - new option sets directory where all pp-scripts must - be stored; + be stored; - new option sets the default list of pp-scripts to - be assigned to nzb-file when it's added to queue; + be assigned to nzb-file when it's added to queue; - new option to set the default list of - pp-scripts on a category basis; + pp-scripts on a category basis; - the execution order of pp-scripts can be set using new option - ; - - there are no separate configuration files for pp-scripts; + ; + - there are no separate configuration files for pp-scripts; - configuration options and pp-parameters are defined in the - pp-scripts; + pp-scripts; - script configuration options are saved in nzbget configuration - file (nzbget.conf); + file (nzbget.conf); - changed parameters list of RPC-methods and - ; + ; - new RPC-method returns configuration - descriptions for the program and for all pp-scripts; - - configuration of all scripts can be done in web-interface; + descriptions for the program and for all pp-scripts; + - configuration of all scripts can be done in web-interface; - the pp-scripts assigned to a particular nzb-file can be viewed and changed in web-interface on page in the - edit download dialog; + edit download dialog; - option renamed to (the old - name is still recognized); + name is still recognized); - new option to define the location of template configuration file (in previous versions it must be always - stored in ); - - history dialog shows status of every script; - - the old example post-processing script replaced with two new scripts: - - EMail.py - sends E-Mail notification; + stored in ); + - history dialog shows status of every script; + - the old example post-processing script replaced with two new scripts: + - EMail.py - sends E-Mail notification; - Logger.py - saves the full post-processing log of the job into - file _postprocesslog.txt; + file _postprocesslog.txt; - both pp-scripts are written in python and work on Windows too - (in addition to Linux, Mac, etc.); - - added possibility to set post-processing parameters for history items: + (in addition to Linux, Mac, etc.); + - added possibility to set post-processing parameters for history items: - pp-parameters can now be viewed and changed in history dialog - in web-interface; - - useful before post-processing again; - - new action in RPC-method ; + in web-interface; + - useful before post-processing again; + - new action in RPC-method ; - new action in remote command <--edit/-E> for history items - (subcommand ); + (subcommand ); - added new feature which creates new download from - selected files of source download; + selected files of source download; - new command in web-interface in edit download dialog - on page ; - - new action in remote command <--edit/-E>; - - new action in JSON-/XML-RPC method ; - - added support for manual par-check: + on page ; + - new action in remote command <--edit/-E>; + - new action in JSON-/XML-RPC method ; + - added support for manual par-check: - if option is set to and a damaged download is detected the program downloads all par2-files but doesn't perform par-check; the user must perform par-check/repair - manually then (possibly on another, faster computer); + manually then (possibly on another, faster computer); - old values of option renamed to - and respectively; - - when set to all par2-files are always downloaded; + and respectively; + - when set to all par2-files are always downloaded; - removed option since its functionality is now - covered by option ; + covered by option ; - result of par-check can now have new value ; + necessary>; - field in RPC-method can have new value - ; + ; - parameter for pp-script can have new value - <4 = manual repair necessary>; + <4 = manual repair necessary>; - when download is resumed in web-interface the option - is respected and all par2-files are resumed (not only main par2-file); + is respected and all par2-files are resumed (not only main par2-file); - automatic deletion of backup-source files after successful par-repair; important when repairing renamed rar-files since this could cause - failure during unpack; + failure during unpack; - par-checker and renamer now add messages into the log of pp-item (like unpack- and pp-scripts-messages); these message now appear in - the log created by scripts Logger.py and EMail.py; + the log created by scripts Logger.py and EMail.py; - when a nzb-file is added via web-interface or via remote call the file is now put into incoming nzb-directory (option "NzbDir") and then scanned; this has two advantages over the old behavior when the - file was parsed directly in memory: - - the file serves as a backup for troubleshootings; + file was parsed directly in memory: + - the file serves as a backup for troubleshootings; - the file is processed by nzbprocess-script (if defined in - option "NzbProcess") making the pre-processing much easier; + option "NzbProcess") making the pre-processing much easier; - new env-var parameters are passed to NzbProcess-script: NZBNP_NZBNAME, - NZBNP_CATEGORY, NZBNP_PRIORITY, NZBNP_TOP, NZBNP_PAUSED; + NZBNP_CATEGORY, NZBNP_PRIORITY, NZBNP_TOP, NZBNP_PAUSED; - new commands for use in NzbProcess-scripts: "[NZB] TOP=1" to add nzb - to the top of queue and "[NZB] PAUSED=1" to add nzb-file in paused state; - - reworked post-processor queue: + to the top of queue and "[NZB] PAUSED=1" to add nzb-file in paused state; + - reworked post-processor queue: - only one job is created for each nzb-file; no more separate - jobs are created for par-collections within one nzb-file; + jobs are created for par-collections within one nzb-file; - option removed; a post-processing script is called only once per nzb-file, this behavior cannot be altered - anymore; + anymore; - with a new feature individual par-collections can be - processed separately in a more effective way than before - - improved unicode (utf8) support: - - non-ascii characters are now correctly transferred via JSON-RPC; - - correct displaying of nzb-names and paths in web-interface; + processed separately in a more effective way than before + - improved unicode (utf8) support: + - non-ascii characters are now correctly transferred via JSON-RPC; + - correct displaying of nzb-names and paths in web-interface; - it is now possible to use non-ascii characters on settings page - for option values (such as paths or category names); - - improved unicode support in XML-RPC and JSON-RPC; + for option values (such as paths or category names); + - improved unicode support in XML-RPC and JSON-RPC; - if username and password are defined for a news-server the authentication is now forced (in previous versions the authentication was performed only if requested by server); needed for servers supporting both anonymous (restricted) and authorized (full access) - accounts; + accounts; - added option to automatically delete unwanted files - (with specified extensions or names) after successful par-check or unpack; - - improvement in JSON-/XML-RPC: + (with specified extensions or names) after successful par-check or unpack; + - improvement in JSON-/XML-RPC: - all ID fields including NZBID are now persistent and remain - their values after restart; + their values after restart; - this allows for third-party software to identify nzb-files by - ID; + ID; - method now returns ID of NZB-file in the field - ; + ; - in versions up to 0.8.0 the field was used to identify history items in the edit-commands , , ; since version 9 field @@ -120,101 +1009,101 @@ nzbget-11.0: existed and had the same value as field for compatibility with version 0.8.0; the compatibility is not provided anymore; this change was needed to provide a consistent using of field - across all RPC-methods; + across all RPC-methods; - added support for rar-files with non-standard extensions (such as - .001, etc.); + .001, etc.); - added functions to backup and restore settings from web-interface; when restoring it's possible to choose what sections to restore (for example only news servers settings or only settings of a - certain pp-script) or restore the whole configuration; + certain pp-script) or restore the whole configuration; - new option "ControlUsername" to define login user name (if you don't - like default username "nzbget"); + like default username "nzbget"); - if a communication error occurs in web-interface, it retries multiple - times before giving up with an error message; + times before giving up with an error message; - the maximum number of download threads are now managed automatically taking into account the number of allowed connections to news servers; - removed option ; + removed option ; - pp-scripts terminated with unknown status are now considered failed - (status=FAILURE instead of status=UNKNOWN); + (status=FAILURE instead of status=UNKNOWN); - new parameter (env. var) is passed to pp_scripts and - contains an internal ID of NZB-file; + contains an internal ID of NZB-file; - improved thread synchronisation to avoid (short-time) lockings of - the program during creation of destination files; + the program during creation of destination files; - more detailed error message if a directory could not be created (, , etc.); the message includes error text reported - by OS such as or similar; + by OS such as or similar; - when unpacking the unpack start time is now measured after receiving of unrar copyright message; this provides better unpack time estimation in a case when user uses unpack-script to do some things before executing unrar (for example sending Wake-On-Lan message to the destination NAS); it works with unrar only, it's not possible - with 7-Zip because it buffers printed messages; + with 7-Zip because it buffers printed messages; - when the program is reloaded, a message with version number is - printed like on start; + printed like on start; - configuration can now be saved in web-interface even if there were no changes made but if obsolete or invalid options were detected in - the config file; the saving removes invalid entries from config file; + the config file; the saving removes invalid entries from config file; - option can now be set to en empty value to disable authentication; useful if nzbget works behind other web-server with - its own authentication; + its own authentication; - when deleting downloads via web-interface a proper hint regarding deleting of already downloaded files from disk depending on option - is displayed; + is displayed; - if a news-server returns empty or bad article (this may be caused by errors on the news server), the program tries again from the same or other servers (in previous versions the article was marked as - failed without other download attempts); + failed without other download attempts); - when a nzb-file whose name ends with ".queued" is added via web- - interface the ".queued"-part is automatically removed; - - small improvement in multithread synchronization of download queue; - - added link to catalog of pp-scripts to web-interface; - - updated forum URL in about dialog in web-interface; + interface the ".queued"-part is automatically removed; + - small improvement in multithread synchronization of download queue; + - added link to catalog of pp-scripts to web-interface; + - updated forum URL in about dialog in web-interface; - small correction in a log-message: removed from message - ; + ; - removed option "ProcessLogKind"; scripts should use prefixes ([INFO], - [DETAIL], etc); messages printed without prefixes are added as [INFO]; + [DETAIL], etc); messages printed without prefixes are added as [INFO]; - removed option "AppendNzbDir"; if it was disabled that caused problems - in par-checker and unpacker; the option is now assumed always active; + in par-checker and unpacker; the option is now assumed always active; - removed option "RenameBroken"; it caused problems in par-checker (the option existed since early program versions before the par-check was added); - configure-script now defines "SIGCHLD_HANDLER" by default on all systems including BSD; this eliminates the need of configure- parameter "--enable-sigchld-handler" on 64-Bit BSD; the trade-off: - 32-Bit BSD now requires "--disable-sigchld-handler"; + 32-Bit BSD now requires "--disable-sigchld-handler"; - improved configure-script: defining of symbol "FILE_OFFSET_BITS=64", required on some systems, is not necessary anymore; - fixed: in the option "NzbAddedProcess" the env-var parameter with nzb-name was passed in "NZBNA_NAME", should be "NZBNA_NZBNAME"; the old parameter name "NZBNA_NAME" is still supported for - compatibility; + compatibility; - fixed: download time in statistics were incorrect if the computer - was put into standby (thanks Frank Kuypers for the patch); + was put into standby (thanks Frank Kuypers for the patch); - fixed: when option was active and the download after unpack contained rar-file with the same name as one of original files (sometimes happen with included subtitles) the original rar-file was kept with name <.rar_duplicate1> even if the option - was active; + was active; - fixed: failed to read download queue from disk if post-processing - queue was not empty; + queue was not empty; - fixed: when a duplicate file was detected during download the - program could hang; + program could hang; - fixed: symbol must be defined in project settings; - defining it in didn't work properly (Windows only); + defining it in didn't work properly (Windows only); - fixed: crash when adding malformed nzb-files with certain - structure (Windows only); + structure (Windows only); - fixed: by deleting of a partially downloaded nzb-file from queue, when the option was active, the file <_brokenlog.txt> was not deleted preventing the directory from - automatic deletion; + automatic deletion; - fixed: if an error occurs when a RPC-client or web-browser - communicates with nzbget the program could crash; + communicates with nzbget the program could crash; - fixed: if the last file of collection was detected as duplicate after the download of the first article the file was deleted from queue (that's OK) but the post-processing was not triggered (that's a bug); - - fixed: support for splitted files (.001, .002, etc.) were broken. - + - fixed: support for splitted files (.001, .002, etc.) were broken. + nzbget-10.2: - fixed potential segfault which could happen with file paths longer than 1024 characters; @@ -389,146 +1278,157 @@ nzbget-10.0: and in configuration options were not displayed properly and could be discarded on saving; -nzbget-9.0: - - changed version naming scheme by removing the leading zero: current - version is now called 9.0 instead of 0.9.0 (it's really the 9th major - version of the program); - - added built-in web-interface: - - completely new designed and written from scratch; - - doesn't require a separate web-server; - - doesn't require PHP; - - 100% Javascript application; the built-in web-server hosts only - static files; the javascript app communicates with NZBGet via - JSON-RPC; - - very efficient usage of server resources (CPU and memory); - - easy installation. Since neither a separate web-server nor PHP - are needed the installation of new web-interface is very easy. - Actually it is performed automatically when you "make install" - or "ipkg install nzbget"; - - modern look: better layout, popup dialogs, nice animations, - hi-def icons; - - built-in phone-theme (activates automatically); - - combined view for "currently downloading", "queued", "currently - processing" and "queued for processing"; - - renaming of nzb-files; - - multiselect with multiedit or merge of downloads; - - fast paging in the lists (downloads, history, messages); - - search box for filtering in the lists (downloads, history, messages) - and in settings; - - adding nzb-files to download queue was improved in several ways: - - add multiple files at once. The "select files dialog" allows - to select multiple files; - - add files using drag and drop. Just drop the files from your - file manager directly into the web-browser; - - add files via URLs. Put the URL and NZBGet downloads the - nzb-file and adds it to download queue automatically; - - the priority of nzb-file can now be set when adding local-files - or URLs; - - the history can be cleared completely or selected items can be removed; - - file mode is now nzb-file related; - - added the ability to queue URLs: - - the program automatically downloads nzb-files from given URLs - and put them to download queue. - - when multiple URLs are added in a short time, they are put - into a special URL-queue. - - the number of simultaneous URL-downloads are controlled via - new option UrlConnections. - - with the new option ReloadUrlQueue can be controlled if the URL-queue - should be reloaded after the program is restarted (if the URL-queue - was not empty). - - new switch <-U> for remote-command <--append/-A> to queue an URL. - - new subcommand <-U> in the remote command <--list/-L> prints the - current URL-queue. - - if URL-download fails, the URL is moved into history. - - with subcommand <-R> of command <--edit> the failed URL can be - returned to URL-queue for redownload. - - the remote command <--list/-L> for history can now print the infos - for URL history items. - - new XML/JSON-RPC command to add an URL or multiple - URLs for download. - - new XML/JSON-RPC command returns the items from the - URL-queue. - - the XML/JSON-RPC command was extended to provide - infos about URL history items. - - the URL-queue obeys the pause-state of download queue. - - the URL-downloads support HTTP and HTTPS protocols; - - added new field to nzb-info-object. - - it is initially set to the cleaned up name of the nzb-file. - - the renaming of the group changes this field. - - all RPC-methods related to nzb-object return the new field, the - old field is now deprecated. - - the option now checks the -field instead of - (the latter is not changed when the nzb is renamed). - - new env-var-parameter for post-processing script; - - added options and for remote command <--edit/-E>. With these - options the name of group or file can be used in edit-command instead - of file ID; - - added support for regular expressions (POSIX ERE Syntax) in remote - commands <--list/-L> and <--edit/-E> using new subcommands and ; - - improved performance of RPC-command ; - - added new command to RPC-method to set the - order of individual files in the group; - - added gzip-support to built-in web-server (including RPC); - - added processing of http-request in RPC-server for better - support of cross domain requests; - - renamed example configuration file and postprocessing script to make - the installation easier; - - improved the automatic installation () to install all - necessary files (not only the binary as it was before); - - improved handling of configuration errors: the program now does not - terminate on errors but rather logs all of them and uses default option values; - - added new XML/JSON-RPC methods , and ; - - with active option the NZB considered completed even if - there are paused non-par-files (the paused non-par-files are treated the - same way as paused par-files): as a result the reprocessable script is called; - - added subcommand to remote command <-S/--scan> to scan synchronously - (wait until scan completed); - - added parameter to XML/JSON-RPC method ; - - the command in web-interface now waits for completing of scan - before reporting the status; - - added remote command <--reload/-O> and JSON/XML-RPC method to - reload configuration from disk and reintialize the program; the reload - can be performed from web-interface; - - JSON/XML-RPC method extended with parameter ; - - categories available in web-interface are now configured in program - configuration file (nzbget.conf) and can be managed via web-interface - on settings page; - - updated descriptions in example configuration file; - - changes in configuration file: - - renamed options , and to - , and to avoid confusion - with news-server options , and - ; - - the old option names are still recognized and are automatically - renamed when the configuration is saved from web-interface; - - also renamed option <$MAINDIR> to ; - - extended remote command <--append/-A> with optional parameters: - - - adds the file/URL to the top of queue; - -

- pauses added files; - - - sets category for added nzb-file/URL; - - - sets nzb filename for added URL; - - the old switches <--category/-K> and <--top/-T> are deprecated - but still supported for compatibility; - - renamed subcommand of command <--edit/-E> to (the old - subcommand is still supported for compatibility); - - added new option to setup a script called after - a nzb-file is added to queue; +nzbget-9.1: + - added full par-scan feature needed to par-check/repair files which + were renamed after creation of par-files: + - new option to activate full par-scan (always or automatic); + the automatic full par-scan activates if missing files are detected + during par-check, this avoids unnecessary full scan for normal + (not renamed) par sets; + - improved the post-processing script to better handle renamed rar-files; + - replaced a browser error message when trying to add local files in + IE9 with a better message dialog; + +nzbget-9.0: + - changed version naming scheme by removing the leading zero: current + version is now called 9.0 instead of 0.9.0 (it's really the 9th major + version of the program); + - added built-in web-interface: + - completely new designed and written from scratch; + - doesn't require a separate web-server; + - doesn't require PHP; + - 100% Javascript application; the built-in web-server hosts only + static files; the javascript app communicates with NZBGet via + JSON-RPC; + - very efficient usage of server resources (CPU and memory); + - easy installation. Since neither a separate web-server nor PHP + are needed the installation of new web-interface is very easy. + Actually it is performed automatically when you "make install" + or "ipkg install nzbget"; + - modern look: better layout, popup dialogs, nice animations, + hi-def icons; + - built-in phone-theme (activates automatically); + - combined view for "currently downloading", "queued", "currently + processing" and "queued for processing"; + - renaming of nzb-files; + - multiselect with multiedit or merge of downloads; + - fast paging in the lists (downloads, history, messages); + - search box for filtering in the lists (downloads, history, messages) + and in settings; + - adding nzb-files to download queue was improved in several ways: + - add multiple files at once. The "select files dialog" allows + to select multiple files; + - add files using drag and drop. Just drop the files from your + file manager directly into the web-browser; + - add files via URLs. Put the URL and NZBGet downloads the + nzb-file and adds it to download queue automatically; + - the priority of nzb-file can now be set when adding local-files + or URLs; + - the history can be cleared completely or selected items can be removed; + - file mode is now nzb-file related; + - added the ability to queue URLs: + - the program automatically downloads nzb-files from given URLs + and put them to download queue. + - when multiple URLs are added in a short time, they are put + into a special URL-queue. + - the number of simultaneous URL-downloads are controlled via + new option UrlConnections. + - with the new option ReloadUrlQueue can be controlled if the URL-queue + should be reloaded after the program is restarted (if the URL-queue + was not empty). + - new switch <-U> for remote-command <--append/-A> to queue an URL. + - new subcommand <-U> in the remote command <--list/-L> prints the + current URL-queue. + - if URL-download fails, the URL is moved into history. + - with subcommand <-R> of command <--edit> the failed URL can be + returned to URL-queue for redownload. + - the remote command <--list/-L> for history can now print the infos + for URL history items. + - new XML/JSON-RPC command to add an URL or multiple + URLs for download. + - new XML/JSON-RPC command returns the items from the + URL-queue. + - the XML/JSON-RPC command was extended to provide + infos about URL history items. + - the URL-queue obeys the pause-state of download queue. + - the URL-downloads support HTTP and HTTPS protocols; + - added new field to nzb-info-object. + - it is initially set to the cleaned up name of the nzb-file. + - the renaming of the group changes this field. + - all RPC-methods related to nzb-object return the new field, the + old field is now deprecated. + - the option now checks the -field instead of + (the latter is not changed when the nzb is renamed). + - new env-var-parameter for post-processing script; + - added options and for remote command <--edit/-E>. With these + options the name of group or file can be used in edit-command instead + of file ID; + - added support for regular expressions (POSIX ERE Syntax) in remote + commands <--list/-L> and <--edit/-E> using new subcommands and ; + - improved performance of RPC-command ; + - added new command to RPC-method to set the + order of individual files in the group; + - added gzip-support to built-in web-server (including RPC); + - added processing of http-request in RPC-server for better + support of cross domain requests; + - renamed example configuration file and postprocessing script to make + the installation easier; + - improved the automatic installation () to install all + necessary files (not only the binary as it was before); + - improved handling of configuration errors: the program now does not + terminate on errors but rather logs all of them and uses default option values; + - added new XML/JSON-RPC methods , and ; + - with active option the NZB considered completed even if + there are paused non-par-files (the paused non-par-files are treated the + same way as paused par-files): as a result the reprocessable script is called; + - added subcommand to remote command <-S/--scan> to scan synchronously + (wait until scan completed); + - added parameter to XML/JSON-RPC method ; + - the command in web-interface now waits for completing of scan + before reporting the status; + - added remote command <--reload/-O> and JSON/XML-RPC method to + reload configuration from disk and reintialize the program; the reload + can be performed from web-interface; + - JSON/XML-RPC method extended with parameter ; + - categories available in web-interface are now configured in program + configuration file (nzbget.conf) and can be managed via web-interface + on settings page; + - updated descriptions in example configuration file; + - changes in configuration file: + - renamed options , and to + , and to avoid confusion + with news-server options , and + ; + - the old option names are still recognized and are automatically + renamed when the configuration is saved from web-interface; + - also renamed option <$MAINDIR> to ; + - extended remote command <--append/-A> with optional parameters: + - - adds the file/URL to the top of queue; + -

- pauses added files; + - - sets category for added nzb-file/URL; + - - sets nzb filename for added URL; + - the old switches <--category/-K> and <--top/-T> are deprecated + but still supported for compatibility; + - renamed subcommand of command <--edit/-E> to (the old + subcommand is still supported for compatibility); + - added new option to setup a script called after + a nzb-file is added to queue; - added debug messages for speed meter; - improved the startup script so it can be directly used in - without modifications; - - fixed: after renaming of a group, the new name was not displayed - by remote commands <-L G> and <-C in curses mode>; - - fixed incompatibility with OpenSLL 1.0 (thanks to OpenWRT team - for the patch); - - fixed: RPC-method could return wrong results if - the log was filtered with options ; - - fixed: free disk space calculated incorrectly on some OSes; - - fixed: unrar failure was not always properly detected causing the - post-processing to delete not yet unpacked rar-files; - - fixed compilation error on recent linux versions; - - fixed compilation error on older systems; + without modifications; + - fixed: after renaming of a group, the new name was not displayed + by remote commands <-L G> and <-C in curses mode>; + - fixed incompatibility with OpenSLL 1.0 (thanks to OpenWRT team + for the patch); + - fixed: RPC-method could return wrong results if + the log was filtered with options ; + - fixed: free disk space calculated incorrectly on some OSes; + - fixed: unrar failure was not always properly detected causing the + post-processing to delete not yet unpacked rar-files; + - fixed compilation error on recent linux versions; + - fixed compilation error on older systems; -nzbget-0.8.0: +nzbget-0.8.0: - added priorities; new action for remote command <--edit/-E> to set priorities for groups or individual files; new actions and of RPC-command ; remote command @@ -561,624 +1461,624 @@ nzbget-0.8.0: to better support BSD without breaking the compatibility with certain Linux systems; - corrected the address of Free Software Foundation in copyright notice. - -nzbget-0.7.0: - - added history: new option , new remote subcommand for - commands (list history entries) and (delete history entries, - return history item, postprocess history item), new RPC-command - and subcommands , , for - command ; - - added support for JSON-P (extension of JSON-RPC); - - changed the result code returning status for postprocessing script - from <1> to <94> (needed to show the proper script status in history); - - improved the detection of new files in incoming nzb directory: now the - scanner does not rely on system datum, but tracks the changing of file - sizes during a last few () seconds instead; - - improvements in example postprocessing script: 1) if download contains - only par2-files the script do not delete them during cleanup; - 2) if download contains only nzb-files the script moves them to incoming - nzb-directory for further download; - - improved formatting of groups and added time info in curses output mode; - - added second pause register, which is independent of main pause-state and - therfore is intended for usage from external scripts; - that allows to pause download without interfering with options - and and scheduler tasks - and - they all work with first (default) pause register; - new subcommand for commands <--pause/-P> and <--unpause/-U>; - new RPC-command and ; - existing RPC-commands und renamed to and - ; - new field in result struct for RPC-command ; - existing fields and renamed to - and ; - old RPC-commands and fields still exist for compatibility; - the status output of command <--list/-L> indicates the state of second - pause register; - key

in curses-frontend can unpause second pause-register; - - nzbprocess-script (option ) can now set category and - post-processing parameters for nzb-file; - - redesigned server pool and par-checker to avoid using of semaphores - (which are very platform specific); - - added subcommand to remote commands <--pause/-P> and <--unpause/-U> to - pause/unpause the scanning of incoming nzb-directory; - - added commands and for scheduler option - ; - - added remote commands and for XML-/JSON-RPC; - - command now not only pauses the post-processing - queue but also pauses the current post-processing job (par-job or - script-job); - however the script-job can be paused only after the next line printed to - screen; - - improved error reporting while parsing nzb-files; - - added field to NZBInfo; the field is now returned by XML-/JSON-RPC - methods , and ; - - improvements in configure script; - - added support for platforms without IPv6 (they do not have ); - - debug-messages generated on early stages during initializing are now - printed to screen/log-file; - - messages about obsolete options are now printed to screen/log-file; - - imporved example postprocessing script: added support for external - configuration file, postprocessing parameters and configuration via - web-interface; - - option now can contain parameters which must be passed - to the script; - - added pausing/resuming for post-processor queue; - added new modifier to remote commands <--pause/-P> and <--unpause/-U>; - added new commands and to XML-/JSON-RPC; - extended output of remote command <--list/-L> to indicate paused state - of post-processor queue; extended command of XML-/JSON-RPC - with field ; - - changed the command line syntax for requesting of post-processor queue - from <-O> to <-L O> for consistency with other post-queue related - commands (<-P O>, <-U O> and <-E O>); - - improved example post-processing script: added support for delayed - par-check (try unrar first, par-repair if unrar failed); - - added modifier to command <-E/--edit> for editing of - post-processor-queue; - following subcommands are supported: <+/-offset>, , , ; - subcommand supports deletion of queued post-jobs and active job as well; - deletion of active job means the cancelling of par-check/repair or - terminating of post-processing-script (including child processes of the - script); - updated remote-server to support new edit-subcommands in XML/JSON-RPC; - - extended the syntax of option in two ways: - 1) it now accepts multiple comma-separated values; - 2) an asterix as hours-part means ; - - added svn revision number to version string (commands <-v> and <-V>, - startup log entry); - svn revision is automatically read from svn-repository on each build; - - added estimated remaining time and better distinguishing of server state - in command <--list/-L>; - - added new return code (93) for post-processing script to indicate - successful processing; that results in cleaning up of download queue - if option is active; - - added readonly options , and for usage - in processing scripts (options are available as environment variables - , and ); - - renamed ParStatus constant to for a consistence with - ScriptStatus constant , that also affects the results of - RPC-command ; - - added a new return code <95/POSTPROCESS_NONE> for post-processing scripts - for cases when pp-script skips all post-processing work (typically upon - a user's request via a pp-parameter); - modified the example post-processing script to return the new code - instead of a error code when a pp-parameter was set to ; - - added field to result of RPC-Command and fields - and for command ; - - in and output-modes the download speed is now printed - with one decimal digit when the speed is lower than 10 KB/s; - - improvement in example post-processing script: added check for existence - of and command ; - - added shell batch file for windows (nzbget-shell.bat); - thanks to orbisvicis (orbisvicis@users.sourceforge.net) for the script; - - added debian style init script (nzbgetd); - thanks to orbisvicis (orbisvicis@users.sourceforge.net) for the script; - - added the returning of a proper HTTP error code if the authorization was - failed on RPC-calls; - thanks to jdembski (jdembski@users.sourceforge.net) for the patch; - - changed the sleep-time during the throttling of bandwidth from 200ms to - 10ms in order to achieve better uniformity; - - modified example postprocessing script to not use the command , - which is not always available; - thanks to Ger Teunis for the patch; - - improved example post-processing script: added the check for existence - of destination directory to return a proper ERROR-code (important for - reprocessing of history items); - - by saving the queue to disk now using relative paths for the list of - compeled files to reduce the file's size; - - eliminated few compiler warnings on GCC; - - fixed: when option was specified and nzbget was - started as root, the lockfile was not removed; - - fixed: nothing was downloaded when the option was set to <0>; - - fixed: base64 decoding function used by RPC-method sometimes - failed, in particular when called from Ruby-language; - - fixed: JSON-RPC-commands failed, if parameters were placed before method - name in the request; - - fixed: RPC-method did not work properly on Posix systems - (it worked only on Windows); - - fixed compilation error when using native curses library on OpenSolaris; - - fixed linking error on OpenSolaris when using GnuTLS; - - fixed: option did not work; - - fixed: seg. fault in service mode on program start (Windows only); - - fixed: environment block was not passed correctly to child process, - what could result in seg faults (windows only); - - fixed: returning the postprocessing exit code <92 - par-check all - collections> when there were no par-files results in endless calling - of postprocessing script; - - fixed compatibility issues with OS/2. - -nzbget-0.6.0: - - added scheduler; new options , , - , and ; - - added support for postprocess-parameters; new subcommand of remote - command to add/modify pp-parameter for group (nzb-file); new - XML-/JSON-RPC-subcommand of method for - the same purpose; updated example configuration file and example - postprocess-script to indicate new method of passing arguments via - environment variables; - - added subcommands , and to command line switch <-L/--list>, - which prints list of files, groups or only status info respectively; - extended binary communication protocol to transfer nzb-infos in addition - to file-infos; - - added new subcommand to edit-command for merging of two (or more) - groups (useful after adding pars from a separate nzb-file); - - added option to automatically merge nzb-files with the same - filename (useful by adding pars from a different source); - - added script-processing of files in incoming directory to allow automatic - unpacking and queueing of compressed nzb-files; new option ; - - added the printing of post-process-parameters for groups in command - <--list G>; - - added the printing of nzbget version into the log-file on start; - - added option to automatically delete already downloaded - files from disk if nzb-file was deleted from queue (the download was - cancelled); - - added option to define the max time allowed for par-repair; - - added command <--scan/-S> to execute the scan of nzb-directory on remote - server; - - changed the method to pass arguments to postprocess/nzbprocess: now using - environment variables (old method is still supported for compatibility with - existing scripts); - - added the passing of nzbget-options to postprocess/nzbprocess scripts as - environment variables; - - extended the communication between nzbget and post-process-script: - collections are now detected even if parcheck is disabled; - - added support for delayed par-check/repair: post-process-script can request - par-check/repair using special exit codes to repair current collection or - all collections; - - implemented the normalizing of option names and values in option list; the - command <-p> also prints normalized names and values now; that makes the - parsing of output of command <-p> for external scripts easier; - - replaced option with new option which is now - used by all scripts (PostProcess, NzbProcess, TaskX.Process); - - improved entering to paused state on connection errors (do not retry failed - downloads if pause was activated); - - improved error reporting on decoding failures; - - improved compatibility of yenc-decoder; - - improved the speed of deleting of groups from download queue (by avoiding - the saving of queue after the deleting of each individual file); - - updated configure-script for better compatibility with FreeBSD; - - cleaning up of download queue (option ) and deletion of - source nzb-file (option ) after par-repair now works also - if par-repair was cancelled (option ); since required - par-files were already downloaded the repair in an external tool is - possible; - - added workaround to avoid hangs in child processes (by starting of - postprocess or nzbprocess), observed on uClibC based systems; - - fixed: TLS/SSL didn't work in standalone mode; - - fixed compatibility issues with Mac OS X; - - fixed: not all necessary par2-files were unpaused on first request for - par-blocks (although harmless, because additional files were unpaused - later anyway); - - fixed small memory leak appeared if process-script could not be started; - - fixed: configure-script could not detect the right syntax for function - on OpenSolaris. - - fixed: files downloaded with disabled decoder (option decode=no) sometimes - were malformed and could not be decoded; - - fixed: empty string parameters did not always work in XML-RPC. - -nzbget-0.5.1: - - improved the check of server responses to prevent unnecessary retrying - if the article does not exist on server; - - fixed: seg.fault in standalone mode if used without specifying the - category (e.g. without switch <-K>); - - fixed: download speed indicator could report not-null values in - standby-mode (when paused); - - fixed: parameter in JSON/XML-RPC was not properly decoded by - server, making the setting of a nested category (containing slash or - backslash character) via nzbgetweb not possible; - -nzbget-0.5.0: - - added TLS/SSL-support for encrypted communication with news-servers; - - added IPv6-support for communication with news-servers as well as for - communication between nzbget-server and nzbget-client; - - added support for categories to organize downloaded files; - - new option to create the subdirectory for each category; - - new switch <-K> for usage with switch <-A> to define a category during - the adding a file to download queue; - - new command in switch <-E> to change the category of nzb-file in - download queue; the already downloaded files are automatically moved to new - directory if the option is active; - - new parameter in XML-/JSON-RPC-command to allow the - changing of category via those protocols; - - new parameter in a call to post-process-script with category name; - - scanning of subdirectories inside incoming nzb-directory to automatically - assign category names; nested categories are supported; - - added option to connect to servers, that do not accept - -command; - - added example post-process script for unraring of downloaded files - (POSIX only); - - added options and useful on slow CPUs; - - added option to delete source nzb-file after successful - download and parcheck; - - switch <-P> can now be used together with switches <-s> and <-D> to start - server/daemon in paused state; - - changed the type of messages logged in a case of connection errors from - to to provide better error reporting; - - now using OS-specific line-endings in log-file and brokenlog-file: LF on - Posix and CRLF on Windows; - - added detection of adjusting of system clock to correct uptime/download - time (for NAS-devices, that do not have internal clock and set time from - internet after booting, while nzbget may be already running); - - added the printing of stack on segmentation faults (if configured with - <--enable-debug>, POSIX only); - - added option for better debugging on Linux in a case of abnormal - program termination; - - fixed: configure-script could not automatically find libsigc++ on 64-bit - systems; - - few other small fixes; - -nzbget-0.4.1: - - to avoid accidental deletion of file in curses-frontend the key - now must be pressed in uppercase; - - options and in news-server's configuration are now - optional; - - added the server's name to the detail-log-message, displayed on the start - of article's download; - - added the option to help to post-process-scripts, which - make par-check/-repair on it's own; - - improved download-speed-meter: it uses now a little bit less cpu and - calculates the speed for the last 30 seconds (instead of 5 seconds), - providing better accuracy; Thanks to ydrol - for the patch; - - reduced CPU-usage in curses-outputmode; Thanks to ydrol for the patch - ; - - fixed: line-endings in windows-style (CR-LF) in config-file were not - read properly; - - fixed: trailing spaces in nzb-filenames (before the file's extension) - caused errors on windows. Now they will be trimmed; - - fixed: XML-RPC and JSON-RPC did not work on Big-Endian-CPUs (ARM, PPC, etc), - preventing the using of web-interface; - - fixed: umask-option did not allow to enable write-permissions for - and ; - - fixed: in curses-outputmode the remote-client showed on first screen-update - only one item of queue; - - fixed: edit-commands with negative offset did not work via XML-RPC - (but worked via JSON-RPC); - - fixed incompatibility issues with gcc 4.3; Thanks to Paul Bredbury - for the patch; - - fixed: segmentation fault if a file listed in nzb-file does not have any - segments (articles); - -nzbget-0.4.0: - - added the support for XML-RPC and JSON-RPC to easier control the server - from other applications; - - added web-interface - it is available for download from NZBGet-project's - home page as a separate package "web-interface"; - - added the automatic cleaning up of the download queue (deletion of unneeded - paused par-files) after successful par-check/repair - new - option ; - - added option to allow to filter the (not so important) - log-messages from articles' downloads (they have now the type - instead of ); - - added the gathering of progress-information during par-check; it is - available via XML-RPC or JSON-RPC; it is also showed in web-interface; - - improvements in internal decoder: added support for yEnc-files without - ypart-statement (sometimes used for small files); added support for - UU-format; - - removed support for uulib-decoder (it did not work well anyway); - - replaced the option with the option - ; - - added detection of errors and - (special case for NNTPCache-server) to consider them as connect-errors - (and therefore not count as retries); - - added check for incomplete articles (also mostly for NNTPCache-server) to - differ such errors from CrcErrors (better error reporting); - - improved error-reporting on moving of completed files from tmp- to - dst-directory and added code to move files across drives if renaming fails; - - improved handling of nzb-files with multiple collections in par-checker; - - improved the parchecker: added the detection and processing of files - splitted after parring; - - added the queueing of post-process-scripts and waiting for script's - completion before starting of a next job in postprocessor (par-job or - script) to provide more balanced cpu utilization; - - added the redirecting of post-process-script's output to log; new option - to specify the default message-kind for unformatted - log-messages; - - added the returning of script-output by command via XML-RPC - and JSON-RPC; the script-output is also showed in web-interface; - - added the saving and restoring of the post-processor-queue (if server was - stopped before all items were processed); new option ; - - added new parameter to postprocess-script to indicate if any of par-jobs - for the same nzb-file failed; - - added remote command (switch O/--post) to request the post-processor-queue - from server; - - added remote command (switch -W/--write) to write messages to server's log; - - added option to automatically pause the download on low disk - space; - - fixed few incompatibility-issues with unslung-platform on nslu2 (ARM); - - fixed: articles with trailing text after binary data caused the decode - failures and the reporting of CRC-errors; - - fixed: dupecheck could cause seg.faults when all articles for a file failed; - - fixed: by dupe-checking of files contained in nzb-file the files with the - same size were ignored (not deleted from queue); - - updated libpar2-patch for msvc to fix a segfault in libpar2 (windows only); - - fixed: by registering the service on windows the fullpath to nzbget.exe - was not always added to service's exename, making the registered service - unusable; - - fixed: the pausing of a group could cause the start of post-processing for - that group; - - fixed: too many info-messages could be printed during - par-check (appeared on posix only); - -nzbget-0.3.1: - - Greatly reduced the memory consumption by keeping articles' info on disk - until the file download starts; - - Implemented decode-on-the-fly-technique to reduce disk-io; downloaded - and decoded data can also be saved directly to the destination file - (without any intermediate files at all); this eliminates the necessity - of joining of articles later (option "DirectWrite"); - - Improved communication with news-servers: connections are now keeped open - until all files are downloaded (or server paused); this eliminates the - need for establishing of connections and authorizations for each - article and improves overal download speed; - - Significantly better download speed is now possible on fast connection; - it was limited by delays on starting of new articles' downloads; - the synchronisation mechanism was reworked to fix this issue; - - Download speed meter is much more accurate, especially on fast connections; - this also means better speed throttling; - - Speed optimisations in internal decoder (up to 25% faster); - - CRC-calculation can be bypassed to increase performance on slow CPUs - (option "CrcCheck"); - - Improved parsing of artcile's subject for better extracting of filename - part from it and implemented a fallback-option if the parsing was incorrect; - - Improved dupe check for files from the same nzb-request to detect reposted - files and download only the best from them (if option "DupeCheck" is on); - - Articles with incorrect CRC can be treated as "possibly recoverable errors" - and relaunched for download (option "RetryOnCrcError"), it is useful if - multiple servers are available; - - Improved error-check for downloaded articles (crc-check and check for - received message-id) decreases the number of broken files; - - Extensions in curses-outputmode: added group-view-mode (key "G") to show - items in download queue as groups, where one group represents all files - from the same nzb-file; the editing of queue works also in group-mode - (for all files in this group): pause/unpause/delete/move of groups; - - Other extensions in curses-outputmode: key "T" toggles timestamps in log; - added output of statistical data: uptime, download-time, average session - download speed; - - Edit-command accepts more than one ID or range of IDs. - E.g: "nzbget -E P 2,6-10,33-39"; The switch "-I" is not used anymore; - - Move-actions in Edit-command affect files in a smart order to guarantee - that the relative order of files in queue is not changed after the moving; - - Extended syntax of edit-command to edit groups (pause/unpause/delete/move - of groups). E.g: "nzbget -E G P 2"; - - Added option "DaemonUserName" to set the user that the daemon (POSIX only) - normally runs at. This allows nzbget daemon to be launched in rc.local - (at boot), and download items as a specific user id; Thanks to Thierry - MERLE for the patch; - - Added option "UMask" to specify permissions for newly created files and dirs - (POSIX only); - - Communication protocol used between server and client was revised to define - the byte order for transferred data. This allows hosts with different - endianness to communicate with each other; - - Added options "CursesNzbName", "CursesGroup" and "CursesTime" to define - initial state of curses-outputmode; - - Added option "UpdateInterval" to adjust update interval for Frontend-output - (useful in remote-mode to reduce network usage); - - Added option "WriteBufferSize" to reduce disk-io (but it could slightly - increase memory usage and therefore disabled by default); - - List-command prints additional statistical info: uptime, download-time, - total amount of downloaded data and average session download speed; - - The creation of necessary directories on program's start was extended - with automatic creation of all parent directories or error reporting - if it was not possible; - - Printed messages are now translated to oem-codepage to correctly print - filenames with non-english characters (windows only); - - Added remote-command "-V (--serverversion)" to print the server's version; - - Added option "ThreadLimit" to prevent program from crash if it wants to - create too many threads (sometimes could occur in special cases); - - Added options "NzbDirInterval" and "NzbDirFileAge" to adjust interval and - delay by monitoring of incoming-directory for new nzb-files; - - Fixed error on parsing of nzb-files containing percent and other special - characters in their names (bug appeared on windows only); - - Reformated sample configuration file and changed default optionnames - from lowercase to MixedCase for better readability; - - Few bugs (seg faults) were fixed. - -nzbget-0.3.0: - - The download queue now contains newsgroup-files to be downloaded instead of - nzb-jobs. By adding a new job, the nzb-file is immediately parsed and each - newsgroup-file is added to download queue. Each file can therefore be - managed separately (paused, deleted or moved); - - Current queue state is saved after every change (file is completed or the - queue is changed - entries paused, deleted or moved). The state is saved on - disk using internal format, which allows fast loading on next program start - (no need to parse xml-files again); - - The remaining download-size is updated after every article is completed to - indicate the correct remaining size and time for total files in queue; - - Downloaded articles, which are saved in temp-directory, can be reused on - next program start, if the file was not completed (option "continuepartial" - in config-file); - - Along with uulib the program has internal decoder for yEnc-format. This - decoder was necessary, because uulib is so slow, that it prevents using of - the program on not so powerful systems like linux-routers (MIPSEL CPU 200 - MHz). The new decoder is very fast. It is controlled over option "decoder" - in config-file; - - The decoder can be completely disabled. In this case all downloaded articles - are saved in unaltered form and can be joined with an external program; - UUDeview is one of them; - - If download of article fails, the program attempts to download it again so - many times, what the option "retries" in config-file says. This works even - if no servers with level higher than "0" defined. After each retry the next - server-level is used, if there are no more levels, the program switches to - level "0" again. The pause between retries can be set with config-option - "retryinterval"; - - If despite of a stated connection-timeout (it can be changed via - config-option "connectiontimeout") connection hangs, the program tries to - cancel the connection (after "terminatetimeout" seconds). If it doesn't - work the download thread is killed and the article will be redownloaded in - a new thread. This ensures, that there are no long-time hanging connections - and all articles are downloaded, when a time to rejoin file comes; - - Automatic par-checking and repairing. Only reuired par-files are downloaded. - The program uses libpar2 and does not require any external tools. The big - advantage of library is, that it allows to continue par-check after new - par-blocks were downloaded. This were not possible with external - par2cmdline-tool; - - There is a daemon-mode now (command-line switch "-D" (--daemon)). In this - mode a lock-file (default location "/tmp/nzbget.lock", can be changed via - option "lockfile") contains PID of daemon; - - The format of configuration-file was changed from xml to more common - text-format. It allows also using of variables like - "tempdir=${MAINDIR}/tmp"; - - Any option of config-file can be overwritten via command-line switch - "-o" (--option). This includes also the definition of servers. - This means that the program can now be started without a configuration-file - at all (all required options must be passed via command-line); - - The command-line switches were revised. The dedicated switches to change - options in config-file were eliminated, since any option can now be changed - via switch "-o" (--option); - - If the name of configuration-file was not passed via command-line the - program search it in following locations: "~/.nzbget", "/etc/nzbget.conf", - "/usr/etc/nzbget.conf", "/usr/local/etc/nzbget.conf", - "/opt/etc/nzbget.conf"; - - The new command-line switch "-n" (--noconfigfile) prevents the loading of - a config-file. All required config-options must be passed via command-line - (switch "-o" (--option)); - - To start the program in server mode either "-s" (--server) or - "-D" (--daemon) switch must be used. If the program started without any - parameters it prints help-screen. There is no a dedicated switch to start - in a standalone mode. If switches "-s" and "-D" are omitted and none of - client-request-switches used the standalone mode is default. This usage - of switches is more common to programs like "wget". To add a file to - server's download queue use switch "-A" (--append) and a name of nzb-file - as last command-line parameter; - - There is a new switch "-Q" (--quit) to gracefully stop server. BTW the - SIGKIL-signal is now handled appropriately, so "killall nzbget" is also OK, - where "killall -9 nzbget" terminates server immediately (helpful if it - hangs, but it shouldn't); - - With new switch "-T" (--top) the file will be added to the top of download - queue. Use it with switch "-A" (--append); - - The download queue can be edited via switch "-E" (--edit). It is possible - to pause, unpause, delete and move files in queue. The IDs of file(s) - to be affected are passed via switch "-I" (fileid), either one ID or a - range in a form "IDForm-IDTo". This also means, that every file in queue - have ID now; - - The switch "-L" (--list) prints IDs of files consequently. It prints also - name, size, percentage of completing and state (paused or not) of each file. - Plus summary info: number of files, total remaining size and size of - paused files, server state (paused or running), number of threads on - server, current speed limit; - - With new switch "-G" (--log) the last N lines printed to server's - screen-log, can be printed on client. The max number of lines which can - be returned from servers depends on option "logbuffersize"; - - The redesigned Frontends (known as outputmodes "loggable", "colored" and - "curses") can connect to (remote) server and behave as if you were running - server-instance of program itself (command-line switch "-C" (--connect)). - The log-output updates constantly and even all control-functions in - ncurses-mode works: pause/unpause server, set download rate limit, edit of - queue (pause/unpause, delete, move entries). The number of connected - clients is not limited. The "outputmode" on a client can be set - independently from server. The client-mode is especially useful if the - server runs as a daemon; - - The writing to log-file can be disabled via option "createlog". - The location of log-file controls the option "log-file"; - - Switch "-p" (--printconfig) prints the name of configuration file being - used and all option/value-pairs, taking into account all used - "-o" (--option) - switches; - - The communication protocol between server and client was optimized to - minimize the size of transferred data. Instead of fixing the size for - Filenames in data-structures to 512 bytes only in fact used data - are transferred; - - Extensions in ncurses-outputmode: scrolling in queue-list works better, - navigation in queue with keys Up, Down, PgUp, PgDn, Home, End. - Keys to move entries are "U" (move up), "N" (move down), "T" (move to top), - "B" (move to bottom). "P" to pause/unpause file. The size, percentage - of completing and state (paused or not) for every file is printed. - The header of queue shows number of total files, number of unpaused - files and size for all and unpaused files. Better using of screen estate - space � no more empty lines and separate header for status (total seven - lines gain). The messages are printed on several lines (if they not fill - in one line) without trimming now; - - configure.ac-file updated to work with recent versions of autoconf/automake. - There are new configure-options now: "--disable-uulib" to compile the - program without uulib; "--disable-ncurses" to disable ncurses-support - (eliminates necessity of ncurses-libs), useful on embedded systems with - little resources; "--disable-parcheck" to compile without par-check; - - The algorithm for parsing of nzb-files now uses XMLReader instead of - DOM-Parser to minimize memory usage (no mor needs to build complete DOM-tree - in memory). Thanks to Thierry MERLE for - the patch; - - The log-file contains now thread-ID for all entry-types and additionally - for debug-entries: filename, line number and function's name of source - code, where the message was printed. Debug-messages can be disabled in - config-file (option "debugtarget") like other messages; - - The program is now compatible with windows. Project file for MS Visual - C++ 2005 is included. Use "nzbget -install" and "nzbget -remove" to - install/remove NZBGet-Service. Servers and clients can run on diferrent - operating systems; - - Improved compatibility with POSIX systems; Tested on: - - Linux Debian 3.1 on x86; - - Linux BusyBox with uClibc on MIPSEL; - - PC-BSD 1.4 (based on FreeBSD 6.2) on x86; - - Solaris 10 on x86; - - Many memory-leaks and thread issues were fixed; - - The program was thoroughly worked over. Almost every line of code was - revised. - -nzbget-0.2.3 - - Fixed problem with losing connection to newsserver after too long idle time - - Added functionality for dumping lots of debug info - -nzbget-0.2.2 - - Added Florian Penzkofers fix for FreeBSD, exchanging base functionality in - SingleServerPool.cpp with a more elegant solution - - Added functionality for forcing answer to reloading queue upon startup of - server - + use -y option to force from command-line - + use "reloadqueue" option in nzbget.cfg to control behavior - - Added nzbget.cfg options to control where info, warnings and errors get - directed to (either screen, log or both) - - Added option "createbrokenfilelog" in nzbget.cfg - -nzbget-0.2.1 - - Changed and extended the TCP client/server interface - - Added timeout on sockets which prevents certain types of nzbget hanging - - Added Kristian Hermansen's patch for renaming broken files - -nzbget-0.2.0 - - Moved 0.1.2-alt4 to a official release as 0.2.0 - - Small fixes - -nzbget-0.1.2-alt4 - - implemented tcp/ip communication between client & server (removing the - rather defunct System V IPC) - - added queue editing functionality in server-mode - -nzbget-0.1.2-alt1 - - added new ncurses frontend - - added server/client-mode (using System V IPC) - - added functionality for queueing download requests - -nzbget-0.1.2 - - performance-improvements - - commandline-options - - fixes - -nzbget-0.1.1 - - new output - - fixes - -nzbget-0.1.0a - - compiling-fixes - -nzbget-0.1.0 - - initial release + +nzbget-0.7.0: + - added history: new option , new remote subcommand for + commands (list history entries) and (delete history entries, + return history item, postprocess history item), new RPC-command + and subcommands , , for + command ; + - added support for JSON-P (extension of JSON-RPC); + - changed the result code returning status for postprocessing script + from <1> to <94> (needed to show the proper script status in history); + - improved the detection of new files in incoming nzb directory: now the + scanner does not rely on system datum, but tracks the changing of file + sizes during a last few () seconds instead; + - improvements in example postprocessing script: 1) if download contains + only par2-files the script do not delete them during cleanup; + 2) if download contains only nzb-files the script moves them to incoming + nzb-directory for further download; + - improved formatting of groups and added time info in curses output mode; + - added second pause register, which is independent of main pause-state and + therfore is intended for usage from external scripts; + that allows to pause download without interfering with options + and and scheduler tasks + and - they all work with first (default) pause register; + new subcommand for commands <--pause/-P> and <--unpause/-U>; + new RPC-command and ; + existing RPC-commands und renamed to and + ; + new field in result struct for RPC-command ; + existing fields and renamed to + and ; + old RPC-commands and fields still exist for compatibility; + the status output of command <--list/-L> indicates the state of second + pause register; + key

in curses-frontend can unpause second pause-register; + - nzbprocess-script (option ) can now set category and + post-processing parameters for nzb-file; + - redesigned server pool and par-checker to avoid using of semaphores + (which are very platform specific); + - added subcommand to remote commands <--pause/-P> and <--unpause/-U> to + pause/unpause the scanning of incoming nzb-directory; + - added commands and for scheduler option + ; + - added remote commands and for XML-/JSON-RPC; + - command now not only pauses the post-processing + queue but also pauses the current post-processing job (par-job or + script-job); + however the script-job can be paused only after the next line printed to + screen; + - improved error reporting while parsing nzb-files; + - added field to NZBInfo; the field is now returned by XML-/JSON-RPC + methods , and ; + - improvements in configure script; + - added support for platforms without IPv6 (they do not have ); + - debug-messages generated on early stages during initializing are now + printed to screen/log-file; + - messages about obsolete options are now printed to screen/log-file; + - imporved example postprocessing script: added support for external + configuration file, postprocessing parameters and configuration via + web-interface; + - option now can contain parameters which must be passed + to the script; + - added pausing/resuming for post-processor queue; + added new modifier to remote commands <--pause/-P> and <--unpause/-U>; + added new commands and to XML-/JSON-RPC; + extended output of remote command <--list/-L> to indicate paused state + of post-processor queue; extended command of XML-/JSON-RPC + with field ; + - changed the command line syntax for requesting of post-processor queue + from <-O> to <-L O> for consistency with other post-queue related + commands (<-P O>, <-U O> and <-E O>); + - improved example post-processing script: added support for delayed + par-check (try unrar first, par-repair if unrar failed); + - added modifier to command <-E/--edit> for editing of + post-processor-queue; + following subcommands are supported: <+/-offset>, , , ; + subcommand supports deletion of queued post-jobs and active job as well; + deletion of active job means the cancelling of par-check/repair or + terminating of post-processing-script (including child processes of the + script); + updated remote-server to support new edit-subcommands in XML/JSON-RPC; + - extended the syntax of option in two ways: + 1) it now accepts multiple comma-separated values; + 2) an asterix as hours-part means ; + - added svn revision number to version string (commands <-v> and <-V>, + startup log entry); + svn revision is automatically read from svn-repository on each build; + - added estimated remaining time and better distinguishing of server state + in command <--list/-L>; + - added new return code (93) for post-processing script to indicate + successful processing; that results in cleaning up of download queue + if option is active; + - added readonly options , and for usage + in processing scripts (options are available as environment variables + , and ); + - renamed ParStatus constant to for a consistence with + ScriptStatus constant , that also affects the results of + RPC-command ; + - added a new return code <95/POSTPROCESS_NONE> for post-processing scripts + for cases when pp-script skips all post-processing work (typically upon + a user's request via a pp-parameter); + modified the example post-processing script to return the new code + instead of a error code when a pp-parameter was set to ; + - added field to result of RPC-Command and fields + and for command ; + - in and output-modes the download speed is now printed + with one decimal digit when the speed is lower than 10 KB/s; + - improvement in example post-processing script: added check for existence + of and command ; + - added shell batch file for windows (nzbget-shell.bat); + thanks to orbisvicis (orbisvicis@users.sourceforge.net) for the script; + - added debian style init script (nzbgetd); + thanks to orbisvicis (orbisvicis@users.sourceforge.net) for the script; + - added the returning of a proper HTTP error code if the authorization was + failed on RPC-calls; + thanks to jdembski (jdembski@users.sourceforge.net) for the patch; + - changed the sleep-time during the throttling of bandwidth from 200ms to + 10ms in order to achieve better uniformity; + - modified example postprocessing script to not use the command , + which is not always available; + thanks to Ger Teunis for the patch; + - improved example post-processing script: added the check for existence + of destination directory to return a proper ERROR-code (important for + reprocessing of history items); + - by saving the queue to disk now using relative paths for the list of + compeled files to reduce the file's size; + - eliminated few compiler warnings on GCC; + - fixed: when option was specified and nzbget was + started as root, the lockfile was not removed; + - fixed: nothing was downloaded when the option was set to <0>; + - fixed: base64 decoding function used by RPC-method sometimes + failed, in particular when called from Ruby-language; + - fixed: JSON-RPC-commands failed, if parameters were placed before method + name in the request; + - fixed: RPC-method did not work properly on Posix systems + (it worked only on Windows); + - fixed compilation error when using native curses library on OpenSolaris; + - fixed linking error on OpenSolaris when using GnuTLS; + - fixed: option did not work; + - fixed: seg. fault in service mode on program start (Windows only); + - fixed: environment block was not passed correctly to child process, + what could result in seg faults (windows only); + - fixed: returning the postprocessing exit code <92 - par-check all + collections> when there were no par-files results in endless calling + of postprocessing script; + - fixed compatibility issues with OS/2. + +nzbget-0.6.0: + - added scheduler; new options , , + , and ; + - added support for postprocess-parameters; new subcommand of remote + command to add/modify pp-parameter for group (nzb-file); new + XML-/JSON-RPC-subcommand of method for + the same purpose; updated example configuration file and example + postprocess-script to indicate new method of passing arguments via + environment variables; + - added subcommands , and to command line switch <-L/--list>, + which prints list of files, groups or only status info respectively; + extended binary communication protocol to transfer nzb-infos in addition + to file-infos; + - added new subcommand to edit-command for merging of two (or more) + groups (useful after adding pars from a separate nzb-file); + - added option to automatically merge nzb-files with the same + filename (useful by adding pars from a different source); + - added script-processing of files in incoming directory to allow automatic + unpacking and queueing of compressed nzb-files; new option ; + - added the printing of post-process-parameters for groups in command + <--list G>; + - added the printing of nzbget version into the log-file on start; + - added option to automatically delete already downloaded + files from disk if nzb-file was deleted from queue (the download was + cancelled); + - added option to define the max time allowed for par-repair; + - added command <--scan/-S> to execute the scan of nzb-directory on remote + server; + - changed the method to pass arguments to postprocess/nzbprocess: now using + environment variables (old method is still supported for compatibility with + existing scripts); + - added the passing of nzbget-options to postprocess/nzbprocess scripts as + environment variables; + - extended the communication between nzbget and post-process-script: + collections are now detected even if parcheck is disabled; + - added support for delayed par-check/repair: post-process-script can request + par-check/repair using special exit codes to repair current collection or + all collections; + - implemented the normalizing of option names and values in option list; the + command <-p> also prints normalized names and values now; that makes the + parsing of output of command <-p> for external scripts easier; + - replaced option with new option which is now + used by all scripts (PostProcess, NzbProcess, TaskX.Process); + - improved entering to paused state on connection errors (do not retry failed + downloads if pause was activated); + - improved error reporting on decoding failures; + - improved compatibility of yenc-decoder; + - improved the speed of deleting of groups from download queue (by avoiding + the saving of queue after the deleting of each individual file); + - updated configure-script for better compatibility with FreeBSD; + - cleaning up of download queue (option ) and deletion of + source nzb-file (option ) after par-repair now works also + if par-repair was cancelled (option ); since required + par-files were already downloaded the repair in an external tool is + possible; + - added workaround to avoid hangs in child processes (by starting of + postprocess or nzbprocess), observed on uClibC based systems; + - fixed: TLS/SSL didn't work in standalone mode; + - fixed compatibility issues with Mac OS X; + - fixed: not all necessary par2-files were unpaused on first request for + par-blocks (although harmless, because additional files were unpaused + later anyway); + - fixed small memory leak appeared if process-script could not be started; + - fixed: configure-script could not detect the right syntax for function + on OpenSolaris. + - fixed: files downloaded with disabled decoder (option decode=no) sometimes + were malformed and could not be decoded; + - fixed: empty string parameters did not always work in XML-RPC. + +nzbget-0.5.1: + - improved the check of server responses to prevent unnecessary retrying + if the article does not exist on server; + - fixed: seg.fault in standalone mode if used without specifying the + category (e.g. without switch <-K>); + - fixed: download speed indicator could report not-null values in + standby-mode (when paused); + - fixed: parameter in JSON/XML-RPC was not properly decoded by + server, making the setting of a nested category (containing slash or + backslash character) via nzbgetweb not possible; + +nzbget-0.5.0: + - added TLS/SSL-support for encrypted communication with news-servers; + - added IPv6-support for communication with news-servers as well as for + communication between nzbget-server and nzbget-client; + - added support for categories to organize downloaded files; + - new option to create the subdirectory for each category; + - new switch <-K> for usage with switch <-A> to define a category during + the adding a file to download queue; + - new command in switch <-E> to change the category of nzb-file in + download queue; the already downloaded files are automatically moved to new + directory if the option is active; + - new parameter in XML-/JSON-RPC-command to allow the + changing of category via those protocols; + - new parameter in a call to post-process-script with category name; + - scanning of subdirectories inside incoming nzb-directory to automatically + assign category names; nested categories are supported; + - added option to connect to servers, that do not accept + -command; + - added example post-process script for unraring of downloaded files + (POSIX only); + - added options and useful on slow CPUs; + - added option to delete source nzb-file after successful + download and parcheck; + - switch <-P> can now be used together with switches <-s> and <-D> to start + server/daemon in paused state; + - changed the type of messages logged in a case of connection errors from + to to provide better error reporting; + - now using OS-specific line-endings in log-file and brokenlog-file: LF on + Posix and CRLF on Windows; + - added detection of adjusting of system clock to correct uptime/download + time (for NAS-devices, that do not have internal clock and set time from + internet after booting, while nzbget may be already running); + - added the printing of stack on segmentation faults (if configured with + <--enable-debug>, POSIX only); + - added option for better debugging on Linux in a case of abnormal + program termination; + - fixed: configure-script could not automatically find libsigc++ on 64-bit + systems; + - few other small fixes; + +nzbget-0.4.1: + - to avoid accidental deletion of file in curses-frontend the key + now must be pressed in uppercase; + - options and in news-server's configuration are now + optional; + - added the server's name to the detail-log-message, displayed on the start + of article's download; + - added the option to help to post-process-scripts, which + make par-check/-repair on it's own; + - improved download-speed-meter: it uses now a little bit less cpu and + calculates the speed for the last 30 seconds (instead of 5 seconds), + providing better accuracy; Thanks to ydrol + for the patch; + - reduced CPU-usage in curses-outputmode; Thanks to ydrol for the patch + ; + - fixed: line-endings in windows-style (CR-LF) in config-file were not + read properly; + - fixed: trailing spaces in nzb-filenames (before the file's extension) + caused errors on windows. Now they will be trimmed; + - fixed: XML-RPC and JSON-RPC did not work on Big-Endian-CPUs (ARM, PPC, etc), + preventing the using of web-interface; + - fixed: umask-option did not allow to enable write-permissions for + and ; + - fixed: in curses-outputmode the remote-client showed on first screen-update + only one item of queue; + - fixed: edit-commands with negative offset did not work via XML-RPC + (but worked via JSON-RPC); + - fixed incompatibility issues with gcc 4.3; Thanks to Paul Bredbury + for the patch; + - fixed: segmentation fault if a file listed in nzb-file does not have any + segments (articles); + +nzbget-0.4.0: + - added the support for XML-RPC and JSON-RPC to easier control the server + from other applications; + - added web-interface - it is available for download from nzbget-project's + home page as a separate package "web-interface"; + - added the automatic cleaning up of the download queue (deletion of unneeded + paused par-files) after successful par-check/repair - new + option ; + - added option to allow to filter the (not so important) + log-messages from articles' downloads (they have now the type + instead of ); + - added the gathering of progress-information during par-check; it is + available via XML-RPC or JSON-RPC; it is also showed in web-interface; + - improvements in internal decoder: added support for yEnc-files without + ypart-statement (sometimes used for small files); added support for + UU-format; + - removed support for uulib-decoder (it did not work well anyway); + - replaced the option with the option + ; + - added detection of errors and + (special case for NNTPCache-server) to consider them as connect-errors + (and therefore not count as retries); + - added check for incomplete articles (also mostly for NNTPCache-server) to + differ such errors from CrcErrors (better error reporting); + - improved error-reporting on moving of completed files from tmp- to + dst-directory and added code to move files across drives if renaming fails; + - improved handling of nzb-files with multiple collections in par-checker; + - improved the parchecker: added the detection and processing of files + splitted after parring; + - added the queueing of post-process-scripts and waiting for script's + completion before starting of a next job in postprocessor (par-job or + script) to provide more balanced cpu utilization; + - added the redirecting of post-process-script's output to log; new option + to specify the default message-kind for unformatted + log-messages; + - added the returning of script-output by command via XML-RPC + and JSON-RPC; the script-output is also showed in web-interface; + - added the saving and restoring of the post-processor-queue (if server was + stopped before all items were processed); new option ; + - added new parameter to postprocess-script to indicate if any of par-jobs + for the same nzb-file failed; + - added remote command (switch O/--post) to request the post-processor-queue + from server; + - added remote command (switch -W/--write) to write messages to server's log; + - added option to automatically pause the download on low disk + space; + - fixed few incompatibility-issues with unslung-platform on nslu2 (ARM); + - fixed: articles with trailing text after binary data caused the decode + failures and the reporting of CRC-errors; + - fixed: dupecheck could cause seg.faults when all articles for a file failed; + - fixed: by dupe-checking of files contained in nzb-file the files with the + same size were ignored (not deleted from queue); + - updated libpar2-patch for msvc to fix a segfault in libpar2 (windows only); + - fixed: by registering the service on windows the fullpath to nzbget.exe + was not always added to service's exename, making the registered service + unusable; + - fixed: the pausing of a group could cause the start of post-processing for + that group; + - fixed: too many info-messages could be printed during + par-check (appeared on posix only); + +nzbget-0.3.1: + - Greatly reduced the memory consumption by keeping articles' info on disk + until the file download starts; + - Implemented decode-on-the-fly-technique to reduce disk-io; downloaded + and decoded data can also be saved directly to the destination file + (without any intermediate files at all); this eliminates the necessity + of joining of articles later (option "DirectWrite"); + - Improved communication with news-servers: connections are now keeped open + until all files are downloaded (or server paused); this eliminates the + need for establishing of connections and authorizations for each + article and improves overal download speed; + - Significantly better download speed is now possible on fast connection; + it was limited by delays on starting of new articles' downloads; + the synchronisation mechanism was reworked to fix this issue; + - Download speed meter is much more accurate, especially on fast connections; + this also means better speed throttling; + - Speed optimisations in internal decoder (up to 25% faster); + - CRC-calculation can be bypassed to increase performance on slow CPUs + (option "CrcCheck"); + - Improved parsing of artcile's subject for better extracting of filename + part from it and implemented a fallback-option if the parsing was incorrect; + - Improved dupe check for files from the same nzb-request to detect reposted + files and download only the best from them (if option "DupeCheck" is on); + - Articles with incorrect CRC can be treated as "possibly recoverable errors" + and relaunched for download (option "RetryOnCrcError"), it is useful if + multiple servers are available; + - Improved error-check for downloaded articles (crc-check and check for + received message-id) decreases the number of broken files; + - Extensions in curses-outputmode: added group-view-mode (key "G") to show + items in download queue as groups, where one group represents all files + from the same nzb-file; the editing of queue works also in group-mode + (for all files in this group): pause/unpause/delete/move of groups; + - Other extensions in curses-outputmode: key "T" toggles timestamps in log; + added output of statistical data: uptime, download-time, average session + download speed; + - Edit-command accepts more than one ID or range of IDs. + E.g: "nzbget -E P 2,6-10,33-39"; The switch "-I" is not used anymore; + - Move-actions in Edit-command affect files in a smart order to guarantee + that the relative order of files in queue is not changed after the moving; + - Extended syntax of edit-command to edit groups (pause/unpause/delete/move + of groups). E.g: "nzbget -E G P 2"; + - Added option "DaemonUserName" to set the user that the daemon (POSIX only) + normally runs at. This allows nzbget daemon to be launched in rc.local + (at boot), and download items as a specific user id; Thanks to Thierry + MERLE for the patch; + - Added option "UMask" to specify permissions for newly created files and dirs + (POSIX only); + - Communication protocol used between server and client was revised to define + the byte order for transferred data. This allows hosts with different + endianness to communicate with each other; + - Added options "CursesNzbName", "CursesGroup" and "CursesTime" to define + initial state of curses-outputmode; + - Added option "UpdateInterval" to adjust update interval for Frontend-output + (useful in remote-mode to reduce network usage); + - Added option "WriteBufferSize" to reduce disk-io (but it could slightly + increase memory usage and therefore disabled by default); + - List-command prints additional statistical info: uptime, download-time, + total amount of downloaded data and average session download speed; + - The creation of necessary directories on program's start was extended + with automatic creation of all parent directories or error reporting + if it was not possible; + - Printed messages are now translated to oem-codepage to correctly print + filenames with non-english characters (windows only); + - Added remote-command "-V (--serverversion)" to print the server's version; + - Added option "ThreadLimit" to prevent program from crash if it wants to + create too many threads (sometimes could occur in special cases); + - Added options "NzbDirInterval" and "NzbDirFileAge" to adjust interval and + delay by monitoring of incoming-directory for new nzb-files; + - Fixed error on parsing of nzb-files containing percent and other special + characters in their names (bug appeared on windows only); + - Reformated sample configuration file and changed default optionnames + from lowercase to MixedCase for better readability; + - Few bugs (seg faults) were fixed. + +nzbget-0.3.0: + - The download queue now contains newsgroup-files to be downloaded instead of + nzb-jobs. By adding a new job, the nzb-file is immediately parsed and each + newsgroup-file is added to download queue. Each file can therefore be + managed separately (paused, deleted or moved); + - Current queue state is saved after every change (file is completed or the + queue is changed - entries paused, deleted or moved). The state is saved on + disk using internal format, which allows fast loading on next program start + (no need to parse xml-files again); + - The remaining download-size is updated after every article is completed to + indicate the correct remaining size and time for total files in queue; + - Downloaded articles, which are saved in temp-directory, can be reused on + next program start, if the file was not completed (option "continuepartial" + in config-file); + - Along with uulib the program has internal decoder for yEnc-format. This + decoder was necessary, because uulib is so slow, that it prevents using of + the program on not so powerful systems like linux-routers (MIPSEL CPU 200 + MHz). The new decoder is very fast. It is controlled over option "decoder" + in config-file; + - The decoder can be completely disabled. In this case all downloaded articles + are saved in unaltered form and can be joined with an external program; + UUDeview is one of them; + - If download of article fails, the program attempts to download it again so + many times, what the option "retries" in config-file says. This works even + if no servers with level higher than "0" defined. After each retry the next + server-level is used, if there are no more levels, the program switches to + level "0" again. The pause between retries can be set with config-option + "retryinterval"; + - If despite of a stated connection-timeout (it can be changed via + config-option "connectiontimeout") connection hangs, the program tries to + cancel the connection (after "terminatetimeout" seconds). If it doesn't + work the download thread is killed and the article will be redownloaded in + a new thread. This ensures, that there are no long-time hanging connections + and all articles are downloaded, when a time to rejoin file comes; + - Automatic par-checking and repairing. Only reuired par-files are downloaded. + The program uses libpar2 and does not require any external tools. The big + advantage of library is, that it allows to continue par-check after new + par-blocks were downloaded. This were not possible with external + par2cmdline-tool; + - There is a daemon-mode now (command-line switch "-D" (--daemon)). In this + mode a lock-file (default location "/tmp/nzbget.lock", can be changed via + option "lockfile") contains PID of daemon; + - The format of configuration-file was changed from xml to more common + text-format. It allows also using of variables like + "tempdir=${MAINDIR}/tmp"; + - Any option of config-file can be overwritten via command-line switch + "-o" (--option). This includes also the definition of servers. + This means that the program can now be started without a configuration-file + at all (all required options must be passed via command-line); + - The command-line switches were revised. The dedicated switches to change + options in config-file were eliminated, since any option can now be changed + via switch "-o" (--option); + - If the name of configuration-file was not passed via command-line the + program search it in following locations: "~/.nzbget", "/etc/nzbget.conf", + "/usr/etc/nzbget.conf", "/usr/local/etc/nzbget.conf", + "/opt/etc/nzbget.conf"; + - The new command-line switch "-n" (--noconfigfile) prevents the loading of + a config-file. All required config-options must be passed via command-line + (switch "-o" (--option)); + - To start the program in server mode either "-s" (--server) or + "-D" (--daemon) switch must be used. If the program started without any + parameters it prints help-screen. There is no a dedicated switch to start + in a standalone mode. If switches "-s" and "-D" are omitted and none of + client-request-switches used the standalone mode is default. This usage + of switches is more common to programs like "wget". To add a file to + server's download queue use switch "-A" (--append) and a name of nzb-file + as last command-line parameter; + - There is a new switch "-Q" (--quit) to gracefully stop server. BTW the + SIGKIL-signal is now handled appropriately, so "killall nzbget" is also OK, + where "killall -9 nzbget" terminates server immediately (helpful if it + hangs, but it shouldn't); + - With new switch "-T" (--top) the file will be added to the top of download + queue. Use it with switch "-A" (--append); + - The download queue can be edited via switch "-E" (--edit). It is possible + to pause, unpause, delete and move files in queue. The IDs of file(s) + to be affected are passed via switch "-I" (fileid), either one ID or a + range in a form "IDForm-IDTo". This also means, that every file in queue + have ID now; + - The switch "-L" (--list) prints IDs of files consequently. It prints also + name, size, percentage of completing and state (paused or not) of each file. + Plus summary info: number of files, total remaining size and size of + paused files, server state (paused or running), number of threads on + server, current speed limit; + - With new switch "-G" (--log) the last N lines printed to server's + screen-log, can be printed on client. The max number of lines which can + be returned from servers depends on option "logbuffersize"; + - The redesigned Frontends (known as outputmodes "loggable", "colored" and + "curses") can connect to (remote) server and behave as if you were running + server-instance of program itself (command-line switch "-C" (--connect)). + The log-output updates constantly and even all control-functions in + ncurses-mode works: pause/unpause server, set download rate limit, edit of + queue (pause/unpause, delete, move entries). The number of connected + clients is not limited. The "outputmode" on a client can be set + independently from server. The client-mode is especially useful if the + server runs as a daemon; + - The writing to log-file can be disabled via option "createlog". + The location of log-file controls the option "log-file"; + - Switch "-p" (--printconfig) prints the name of configuration file being + used and all option/value-pairs, taking into account all used + "-o" (--option) - switches; + - The communication protocol between server and client was optimized to + minimize the size of transferred data. Instead of fixing the size for + Filenames in data-structures to 512 bytes only in fact used data + are transferred; + - Extensions in ncurses-outputmode: scrolling in queue-list works better, + navigation in queue with keys Up, Down, PgUp, PgDn, Home, End. + Keys to move entries are "U" (move up), "N" (move down), "T" (move to top), + "B" (move to bottom). "P" to pause/unpause file. The size, percentage + of completing and state (paused or not) for every file is printed. + The header of queue shows number of total files, number of unpaused + files and size for all and unpaused files. Better using of screen estate + space - no more empty lines and separate header for status (total seven + lines gain). The messages are printed on several lines (if they not fill + in one line) without trimming now; + - configure.ac-file updated to work with recent versions of autoconf/automake. + There are new configure-options now: "--disable-uulib" to compile the + program without uulib; "--disable-ncurses" to disable ncurses-support + (eliminates necessity of ncurses-libs), useful on embedded systems with + little resources; "--disable-parcheck" to compile without par-check; + - The algorithm for parsing of nzb-files now uses XMLReader instead of + DOM-Parser to minimize memory usage (no mor needs to build complete DOM-tree + in memory). Thanks to Thierry MERLE for + the patch; + - The log-file contains now thread-ID for all entry-types and additionally + for debug-entries: filename, line number and function's name of source + code, where the message was printed. Debug-messages can be disabled in + config-file (option "debugtarget") like other messages; + - The program is now compatible with windows. Project file for MS Visual + C++ 2005 is included. Use "nzbget -install" and "nzbget -remove" to + install/remove nzbget-Service. Servers and clients can run on diferrent + operating systems; + - Improved compatibility with POSIX systems; Tested on: + - Linux Debian 3.1 on x86; + - Linux BusyBox with uClibc on MIPSEL; + - PC-BSD 1.4 (based on FreeBSD 6.2) on x86; + - Solaris 10 on x86; + - Many memory-leaks and thread issues were fixed; + - The program was thoroughly worked over. Almost every line of code was + revised. + +nzbget-0.2.3 + - Fixed problem with losing connection to newsserver after too long idle time + - Added functionality for dumping lots of debug info + +nzbget-0.2.2 + - Added Florian Penzkofers fix for FreeBSD, exchanging base functionality in + SingleServerPool.cpp with a more elegant solution + - Added functionality for forcing answer to reloading queue upon startup of + server + + use -y option to force from command-line + + use "reloadqueue" option in nzbget.cfg to control behavior + - Added nzbget.cfg options to control where info, warnings and errors get + directed to (either screen, log or both) + - Added option "createbrokenfilelog" in nzbget.cfg + +nzbget-0.2.1 + - Changed and extended the TCP client/server interface + - Added timeout on sockets which prevents certain types of nzbget hanging + - Added Kristian Hermansen's patch for renaming broken files + +nzbget-0.2.0 + - Moved 0.1.2-alt4 to a official release as 0.2.0 + - Small fixes + +nzbget-0.1.2-alt4 + - implemented tcp/ip communication between client & server (removing the + rather defunct System V IPC) + - added queue editing functionality in server-mode + +nzbget-0.1.2-alt1 + - added new ncurses frontend + - added server/client-mode (using System V IPC) + - added functionality for queueing download requests + +nzbget-0.1.2 + - performance-improvements + - commandline-options + - fixes + +nzbget-0.1.1 + - new output + - fixes + +nzbget-0.1.0a + - compiling-fixes + +nzbget-0.1.0 + - initial release diff --git a/content/usr/local/share/doc/nzbget/README b/content/usr/local/share/doc/nzbget/README index 7d16e95..bcbe2ef 100644 --- a/content/usr/local/share/doc/nzbget/README +++ b/content/usr/local/share/doc/nzbget/README @@ -4,7 +4,7 @@ This is a short documentation. For more information please visit NZBGet home page at - http://nzbget.sourceforge.net + http://nzbget.net Contents -------- @@ -85,15 +85,11 @@ And the following libraries are optional: - libcurses (usually part of commercial systems) or (better) - libncurses (http://invisible-island.net/ncurses) - - - for par-check and -repair (enabled by default): - - libpar2 (http://parchive.sourceforge.net) - - libsigc++ (http://libsigc.sourceforge.net) - + - for encrypted connections (TLS/SSL): - - GnuTLS (http://www.gnu.org/software/gnutls) - or - OpenSSL (http://www.openssl.org) + or + - GnuTLS (http://www.gnu.org/software/gnutls) - for gzip support in web-server and web-client (enabled by default): - zlib (http://www.zlib.net) @@ -151,13 +147,13 @@ You may run configure with additional arguments: if you can not use curses/ncurses. --disable-parcheck - to make without parcheck-support. Use this option - if you can not use libpar2 or libsigc++. + if you have troubles when compiling par2-module. - --with-tlslib=(GnuTLS, OpenSSL) - to select which TLS/SSL library + --with-tlslib=(OpenSSL, GnuTLS) - to select which TLS/SSL library should be used for encrypted server connections. --disable-tls - to make without TLS/SSL support. Use this option if - you can not neither GnuTLS nor OpenSSL. + you can not neither OpenSSL nor GnuTLS. --disable-gzip - to make without gzip support. Use this option if you can not use zlib. @@ -168,37 +164,13 @@ You may run configure with additional arguments: Optional package: par-check --------------------------- NZBGet can check and repair downloaded files for you. For this purpose -it uses library par2 (libpar2), which needs sigc++ on its part. +it uses library par2. -The libpar2 and libsigc++ (version 2 or later) must be installed on your -system. On most linux distributions these libraries are available as packages. -If you do not have these packages you can compile them yourself. -Following configure-parameters may be usefull: +For your convenience the source code of libpar2 is integrated into +NZBGet’s source tree and is compiled automatically when you make NZBGet. - --with-libpar2-includes - --with-libpar2-libraries - --with-libsigc-includes - --with-libsigc-libraries - -The library libsigc++ must be installed first, since libpar2 requires it. - -If you use nzbget on a very slow computer like NAS-device, it may be good to -limit the time allowed for par-repair (option "ParTimeLimit" in nzbget -configuration file). This feature requires a patched version of libpar2. -To compile that version download the original source code of libpar2 -(version 0.2) and apply patches "libpar2-0.2-bugfixes.patch" and -"libpar2-0.2-cancel.patch", provided with nzbget: - - cd libpar2-0.2 - cp ~/nzbget/libpar2-0.2-*.patch . - patch < libpar2-0.2-bugfixes.patch - patch < libpar2-0.2-cancel.patch - ./configure - make - make install - -If you are not able to use libpar2 or libsigc++ or do not want them you can -make nzbget without support for par-check using option "--disable-parcheck": +In a case errors occur during this process the inclusion of par2-module +can be disabled using configure option "--disable-parcheck": ./configure --disable-parcheck @@ -206,7 +178,7 @@ Optional package: curses ------------------------- For curses-outputmode you need ncurses or curses on your system. If you do not have one of them you can download and compile ncurses yourself. -Following configure-parameters may be usefull: +Following configure-parameters may be useful: --with-libcurses-includes --with-libcurses-libraries @@ -219,14 +191,14 @@ make the program without support for curses using option "--disable-curses": Optional package: TLS ------------------------- To enable encrypted server connections (TLS/SSL) you need to build the program -with TLS/SSL support. NZBGet can use two libraries: GnuTLS or OpenSSL. +with TLS/SSL support. NZBGet can use two libraries: OpenSSL or GnuTLS. Configure-script checks which library is installed and use it. If both are -avialable it gives the precedence to GnuTLS. You may override that with -the option --with-tlslib=(GnuTLS, OpenSSL). For example to build whith OpenSSL: +available it gives the precedence to OpenSSL. You may override that with +the option --with-tlslib=(OpenSSL, GnuTLS). For example to build with GnuTLS: - ./configure --with-tlslib=OpenSSL + ./configure --with-tlslib= GnuTLS -Following configure-parameters may be usefull: +Following configure-parameters may be useful: --with-libtls-includes --with-libtls-libraries @@ -247,28 +219,14 @@ NZBGet is developed using MS Visual C++ 2005. The project file and solution are provided. If you use MS Visual C++ 2005 Express you need to download and install Platform SDK. -To compile the program with par-check-support you also need the following -libraries: - - - libsigc++ (http://libsigc.sourceforge.net) - - libpar2 (http://parchive.sourceforge.net) - -Download these libaries, then use patch-files provided with NZBGet to create -preconfigured project files and solutions for each library. -Look at http://gnuwin32.sourceforge.net/packages/patch.htm for info on how -to use patch-files, if you do not familiar with this technique. - -To compile the program with TLS/SSL support you also need the library: - +To compile the program with TLS/SSL support you need either OpenSSL or GnuTLS: + - OpenSSL (http://www.openssl.org) + or - GnuTLS (http://www.gnu.org/software/gnutls) -Download a precompiled version of GnuTLS from http://josefsson.org/gnutls4win -and create lib-file as described there in section "Using the GnuTLS DLL from -your Visual Studio program". - -After libsigc++ and libpar2 are compiled in static libraries (.lib), the -library for GnuTLS is created and include- and libraries-paths are configured -in MS Visual C++ 2005 you should be able to compile NZBGet. +Also required are: + - Regex (http://gnuwin32.sourceforge.net/packages/regex.htm) + - Zlib (http://gnuwin32.sourceforge.net/packages/zlib.htm) ===================================== 6. Configuration @@ -387,9 +345,18 @@ It prints something like: [1] nzbname\filename1.rar (50.00 MB) [2] nzbname\filename1.r01 (50.00 MB) + [3] another-nzb\filename3.r01 (100.00 MB) + [4] another-nzb\filename3.r02 (100.00 MB) + +This is the list of individual files listed within nzb-file. To print +the list of nzb-files (without content) add G-modifier to the list command: + + [1] nzbname (4.56 GB) + [2] another-nzb (4.20 GB) -The numbers in square braces are ID's of files in queue. They can be used -in edit-command. For example to move file with ID 2 to the top of queue: +The numbers in square braces are ID's of files or groups in queue. +They can be used in edit-command. For example to move file with +ID 2 to the top of queue: nzbget -E T 2 @@ -402,8 +369,8 @@ or to delete files from queue: nzbget -E D 3 10-15 20-21 16 The edit-command has also a group-mode which affects all files from the -same nzb-request. You need to pass one ID of any file in the group. For -example to delete all files from the first nzb-request: +same nzb-file. You need to pass an ID of the group. For example to delete +the whole group 1: nzbget -E G D 1 @@ -444,10 +411,10 @@ Post processing scripts After the download of nzb-file is completed nzbget can call post-processing scripts, defined in configuration file. -Example post-processing scripts are provided in directory "ppscripts". +Example post-processing scripts are provided in directory "scripts". To use the scripts copy them into your local directory and set options -, and . +, and . For information on writing your own post-processing scripts please visit NZBGet web site. @@ -469,13 +436,14 @@ and port defined in NZBGet configuration file in options "ControlIP" and http://localhost:6789/ -For login credentials type username "nzbget" (predefined and not changeable) -and the password from the option "ControlPassword" (default is tegbzn6789). +For login credentials type username and the password defined by +options "ControlUsername" (default "nzbget") and "ControlPassword" +(default "tegbzn6789"). In a case your browser forget credentials, to prevent typing them each time, there is a workaround - use URL in the form: - http://localhost:6789/nzbget:password/ + http://localhost:6789/username:password/ Please note, that in this case the password is saved in a bookmark or in browser history in plain text and is easy to find by persons having @@ -494,6 +462,32 @@ Bo Cordes Petersen (placebodk@users.sourceforge.net) until 2005. In 2007 the abandoned project was overtaken by Andrey Prygunkov. Since then the program has been completely rewritten. +NZBGet distribution archive includes additional components +written by other authors: + +PAR2: + Peter Brian Clements + +PAR2 library API: + Francois Lesueur + +jQuery: + John Resig + The Dojo Foundation + +Bootstrap: + Twitter, Inc + +Raphaël: + Dmitry Baranovskiy + Sencha Labs + +Elycharts: + Void Labs s.n.c. + +iconSweets: + Yummygum + ===================================== 9. Copyright ===================================== @@ -507,21 +501,13 @@ The complete content of license is provided in file COPYING. Additional exemption: compiling, linking, and/or using OpenSSL is allowed. -Binary distribution for Windows contains code from the following libraries: - - - libpar2 (http://parchive.sourceforge.net) - - libsigc++ (http://libsigc.sourceforge.net) - - GnuTLS (http://www.gnu.org/software/gnutls) - -libpar2 is distributed under GPL; libsigc++ and GnuTLS - under LGPL. - ===================================== 10. Contact ===================================== If you encounter any problem, feel free to use the forum - nzbget.sourceforge.net/forum + nzbget.net/forum or contact me at diff --git a/content/usr/local/share/nzbget/nzbget.conf b/content/usr/local/share/nzbget/nzbget.conf index d94afa5..e4289cc 100644 --- a/content/usr/local/share/nzbget/nzbget.conf +++ b/content/usr/local/share/nzbget/nzbget.conf @@ -21,8 +21,8 @@ # Root directory for all tasks. # -# On POSIX you can use "~" as alias for home directory (e.g. "~/download"). -# On Windows use absolute paths (e.g. "C:\Download"). +# On POSIX you can use "~" as alias for home directory (e.g. "~/downloads"). +# On Windows use absolute paths (e.g. "C:\Downloads"). MainDir=/home/xbian/ # Destination directory for downloaded files. @@ -48,14 +48,22 @@ DestDir=${MainDir}/downloads # files are put directly to destination directory (option ). InterDir= -# Directory to monitor for incoming nzb-jobs. +# Directory for incoming nzb-files. # -# Can have subdirectories. -# A nzb-file queued from a subdirectory will be automatically assigned to -# category with the directory-name. +# If a new nzb-file is added to queue via web-interface or RPC-API, it +# is saved into this directory and then processed by pre-processing +# script (option ). +# +# This directory is also monitored for new nzb-files. If a new file +# is found it is added to download queue. The directory can have +# sub-directories. A nzb-file queued from a subdirectory is automatically +# assigned to category with sub-directory-name. NzbDir=${MainDir}/torrents -# Directory to store download queue. +# Directory to store program state. +# +# This directory is used to save download queue, history, information +# about fetched RSS feeds, statistics, etc. QueueDir=${MainDir}/incomplete # Directory to store temporary files. @@ -70,10 +78,9 @@ TempDir=${MainDir}/incomplete # it is also used to serve JSON-/XML-RPC requests. WebDir=/usr/local/share/nzbget/webui -# Directory with post-processing scripts. +# Directory with post-processing and other scripts. # -# NOTE: For information on writing post-processing scripts visit -# http://nzbget.sourceforge.net/Post-processing_scripts. +# NOTE: For information on writing scripts visit http://nzbget.net/Extension_scripts. ScriptDir=/etc/nzbget/ppscripts # Lock-file for daemon-mode, POSIX only. @@ -84,7 +91,7 @@ LockFile=/run/nzbget.lock # Where to store log file, if it needs to be created. # -# NOTE: See also option . +# NOTE: See also option . LogFile=/var/log/nzbget.log # Configuration file template. @@ -94,7 +101,7 @@ LogFile=/var/log/nzbget.log # # Do not put here your actual configuration file (typically stored # in your home directory or in /etc/nzbget.conf) but instead the unchanged -# example configuration file (installed to +# example configuration file (typically installed to # /usr/local/share/nzbget/nzbget.conf). # # Example: /usr/local/share/nzbget/nzbget.conf. @@ -112,6 +119,17 @@ ConfigTemplate=/usr/local/share/nzbget/nzbget.conf # change the name of Server3 to Server2. Otherwise it will not be properly # read from the config file. Server number doesn't affect its priority (level). +# Use this news server (yes, no). +# +# Set to "no" to temporary disable the server. +Server1.Active=yes + +# Name of news server. +# +# The name is used in UI and for logging. It can be any string, you +# may even leave it empty. +Server1.Name= + # Level (priority) of news server (0-99). # # The servers are ordered by their level. NZBGet first tries to download @@ -163,7 +181,7 @@ Server1.Encryption=no # chooses the cipher automatically. To achieve the best performance # however you can manually select a faster cipher. # -# See http://nzbget.sourceforge.net/Choosing_a_cipher for details. +# See http://nzbget.net/Choosing_a_cipher for details. # # NOTE: One of the fastest cipher is RC4, it also provides strong 128 bit # encryption. To select it use the cipher string "RC4-MD5" (if NZBGet was @@ -173,7 +191,7 @@ Server1.Encryption=no # NOTE: You may get a TLS handshake error if the news server does # not support the chosen cipher. You can also get an error "Could not # select cipher for TLS" if the cipher string is not valid. -Server1.Cipher= +Server1.Cipher=RC4-MD5 # Maximum number of simultaneous connections to this server (0-999). Server1.Connections=4 @@ -200,7 +218,7 @@ Server1.Connections=4 ############################################################################## -### REMOTE CONTROL ### +### SECURITY ### # IP on which NZBGet server listen and which clients use to contact NZBGet. # @@ -228,7 +246,7 @@ ControlPort=9092 # # Set to empty value to disable user name check (check only password). # -# NOTE: this option was added in NZBGet 11. Older versions used predefined +# NOTE: This option was added in NZBGet 11. Older versions used predefined # not changeable user name "nzbget". Third-party tools or web-sites written # for older NZBGet versions may not have an option to define user name. In # this case you should set option to the default value @@ -257,9 +275,16 @@ SecureCert= # Full path to key file for encrypted communication. SecureKey= - -############################################################################## -### PERMISSIONS ### +# IP-addresses allowed to connect without authorization. +# +# Comma separated list of privileged IPs for easy access to NZBGet +# built-in web-server (web-interface and RPC). +# +# Example: 127.0.0.1,192.168.178.2. +# +# NOTE: Do not use this option if the program works behind another +# web-server because all requests will have the address of this server. +AuthorizedIP= # User name for daemon-mode, POSIX only. # @@ -284,183 +309,275 @@ UMask=1000 ############################################################################## -### INCOMING NZBS ### +### CATEGORIES ### -# Create subdirectory with category-name in destination-directory (yes, no). -AppendCategoryDir=yes +# This section defines categories available in web-interface. -# How often incoming-directory (option ) must be checked for new -# nzb-files (seconds). +# Category name. # -# Value "0" disables the check. -NzbDirInterval=5 +# Each nzb-file can be assigned to a category. +# Category name is passed to post-processing script and can be used by it +# to perform category specific processing. +Category1.Name=Movies -# How old nzb-file should at least be for it to be loaded to queue (seconds). +# Destination directory for this category. # -# NZBGet checks if nzb-file was not modified in last few seconds, defined by -# this option. That safety interval prevents the loading of files, which -# were not yet completely saved to disk, for example if they are still being -# downloaded in web-browser. -NzbDirFileAge=60 +# If this option is empty, then the default destination directory +# (option ) is used. In this case if the option +# is active, the program creates a subdirectory with category name within +# destination directory. +Category1.DestDir= -# Automatic merging of nzb-files with the same filename (yes, no). +# Unpack downloaded nzb-files (yes, no). # -# A typical scenario: you put nzb-file into incoming directory, NZBGet adds -# file to queue. You find out, that the file doesn't have par-files. You -# find required par-files, put nzb-file with the par-files into incoming -# directory, NZBGet adds it to queue as a separate group. You want the second -# file to be merged with the first for parchecking to work properly. With -# option "MergeNzb" NZBGet can merge files automatically. You only need to -# save the second file under the same filename as the first one. -MergeNzb=no +# For more information see global option . +Category1.Unpack=yes -# Set path to program, that must be executed before a nzb-file is added -# to queue. -# -# This program is called each time a new file is found in incoming -# directory (option ) or a file is received via RPC (web-interface, -# command "nzbget --append", etc.). -# -# Example: ~/nzbprocess.sh. -# -# That program can unpack archives which were put in incoming directory, make -# filename cleanup, change nzb-name, category, priority and post-processing -# parameters of the nzb-file or do other things. -# -# INFO FOR DEVELOPERS: -# NZBGet passes following arguments to nzbprocess-program as environment -# variables: -# NZBNP_DIRECTORY - path to directory, where file is located. It is a directory -# specified by the option or a subdirectory; -# NZBNP_FILENAME - name of file to be processed; -# NZBNP_NZBNAME - nzb-name (without path but with extension); -# NZBNP_CATEGORY - category of nzb-file; -# NZBNP_PRIORITY - priority of nzb-file; -# NZBNP_TOP - flag indicating that the file will be added to the top -# of queue: 0 or 1; -# NZBNP_PAUSED - flag indicating that the file will be added as -# paused: 0 or 1. -# -# In addition to these arguments NZBGet passes all -# nzbget.conf-options to nzbprocess-program as environment variables. These -# variables have prefix "NZBOP_" and are written in UPPER CASE. For Example -# option "ParRepair" is passed as environment variable "NZBOP_PARREPAIR". -# The dots in option names are replaced with underscores, for example -# "SERVER1_HOST". For options with predefined possible values (yes/no, etc.) -# the values are passed always in lower case. -# -# The nzbprocess-script can change nzb-name, category, priority, -# post-processing parameters and top-/paused-flags of the nzb-file -# by printing special messages into standard output (which is processed -# by NZBGet). -# -# To change nzb-name use following syntax: -# echo "[NZB] NZBNAME=my download"; -# -# To change category: -# echo "[NZB] CATEGORY=my category"; -# -# To change priority: -# echo "[NZB] PRIORITY=signed_integer_value"; +# Default list of post-processing scripts. # -# for example: to set priority higher than normal: -# echo "[NZB] PRIORITY=50"; +# For more information see global option . +Category1.PostScript= + +# List of aliases. # -# another example: use a negative value for "lower than normal" priority: -# echo "[NZB] PRIORITY=-100"; +# When a nzb-file is added from URL, RSS or RPC the category name +# is usually supplied by nzb-site or by application accessing +# NZBGet. Using Aliases you can match their categories with your owns. # -# Although priority can be any integer value, the web-interface operates -# with five predefined priorities: -# -100 - very low priority; -# -50 - low priority; -# 0 - normal priority (default); -# 50 - high priority; -# 100 - very high priority. +# Separate aliases with commas or semicolons. Use wildcard-characters +# * and ? for pattern matching. # -# To assign post-processing parameters: -# echo "[NZB] NZBPR_myvar=my value"; +# Example: TV - HD, TV - SD, TV* +Category1.Aliases= + +Category2.Name=Series +Category3.Name=Music +Category4.Name=Software + + +############################################################################## +### RSS FEEDS ### + +# Name of RSS Feed. # -# The prefix "NZBPR_" will be removed. In this example a post-processing -# parameter with name "myvar" and value "my value" will be associated -# with nzb-file. +# The name is used in UI and for logging. It can be any string. +#Feed1.Name=my feed + +# Address (URL) of RSS Feed. # -# To change top-flag (nzb-file will be added to the top of queue): -# echo "[NZB] TOP=1"; +# Example: https://myindexer.com/api?apikey=3544646bfd1c535a9654645609800901&t=search&q=game. # -# To change paused-flag (nzb-file will be added in paused state): -# echo "[NZB] PAUSED=1"; +# NOTE: When the feed is fetched for the very first time all existing +# items are ignored. The items found on subsequentional fetches are processed. +#Feed1.URL= + +# Filter rules for items. # -# The nzbprocess-script can delete processed file, rename it or move somewhere. -# After the calling of the script the file will be either added to queue -# (if it was an nzb-file) or renamed by adding the extension ".processed". +# Use filter to ignore unwanted items in the feed. In its simplest version +# the filter is a space separated list of words which must be present in +# the item title. # -# NOTE: Files with extensions ".processed", ".queued" and ".error" are skipped -# during the directory scanning. +# Example: linux debian dvd. # -# NOTE: Files with extension ".nzb_processed" are not passed to -# NzbProcess-script before adding to queue. This feature allows -# NzbProcess-script to prevent the scanning of nzb-files extracted from -# archives, if they were already processed by the script. +# MORE INFO: +# NOTE: This is a short documentation, for more information visit +# http://nzbget.net/RSS. +# +# Feed filter consists of rules - one rule per line. Each rule defines +# a search string and a command, which must be performed if the search +# string matches. There are five kinds of rule-commands: Accept, +# Reject, Require, Options, Comment. +# +# NOTE: Since options in the configuration file can not span multiple +# lines, the lines (rules) must be separated with %-character (percent). +# +# Definition of a rule: +# [A:|A(options):|R:|Q:|O(options):|#] search-string +# +# A - declares Accept-rule. Rules are accept-rules by default, the +# "A:" can be imitted. If the feed item matches to the rule the +# item is considered good and no further rules are checked. +# R - declares Reject-rule. If the feed item matches to the rule the +# item is considered bad and no further rules are checked. +# Q - declares Require-rule. If the feed item DOES NOT match to the rule +# the item is considered bad and no further rules are checked. +# O - declares Options-rule. If the feed item matches to the rule the +# options declared in the rule are set for the item. The item is +# neither accepted nor rejected via this rule but can be accepted +# later by one of Accept-rules. In this case the item will have its +# options already set (unless the Accept-rule overrides them). +# # - lines starting with # are considered comments and are ignored. You +# can use comments to explain complex rules or to temporary disable +# rules for debugging. +# +# Options allow to set properties on nzb-file. It's a comma-separated +# list of property names with their values. +# +# Definition of an option: +# name:value +# +# Options can be defined using long option names or short names: +# category (cat, c) - set category name, value is a string; +# pause (p) - add nzb in paused or unpaused state, possible +# values are: yes (y), no (n); +# priority (pr, r) - set priority, value is a signed integer number; +# priority+ (pr+, r+) - increase priority, value is a signed integer number; +# dupescore (ds, s) - set duplicate score, value is a signed integer number; +# dupescore+ (ds+, s+) - increase duplicate score, value is a signed integer number; +# dupekey (dk, k) - set duplicate key, value is a string; +# dupekey+ (dk+, k+) - add to duplicate key, value is a string; +# dupemode (dm, m) - set duplicate check mode, possible values +# are: score (s), all (a), force (f); +# rageid - generate duplicate key using this rageid +# (integer number) and season/episode numbers; +# series - generate duplicate key using series identifier +# (any unique string) and season/episode numbers. +# +# Examples of option definitions: +# Accept(category:my series, pause:yes, priority:100): my show 1080p; +# Options(c:my series, p:y, r:100): 1080p; +# Options(s:1000): 1080p; +# Options(k+:1080p): 1080p; +# Options(dupemode:force): BluRay. +# +# Rule-options override values set in feed-options. +# +# The search-string is similar to used in search engines. It consists of +# search terms separated with spaces. Every term is checked for a feed +# item and if they all succeed the rule is considered matching. +# +# Definition of a term: +# [+|-][field:][command]param +# +# + - declares a positive term. Terms are positive by default, +# the "+" can be omitted; +# - - declares a negative term. If the term succeed the feed +# item is ignored; +# field - field to which apply the term. If not specified +# the default field "title" is used; +# command - a special character defining how to interpret the +# parameter (followed after the command): +# @ - search for string "param". This is default command, +# the "@" can be omitted; +# $ - "param" defines a regular expression (using POSIX Extended +# Regular Expressions syntax); +# = - equal; +# < - less than; +# <= - equal or less than; +# > - greater than; +# >= - equal or greater than; +# param - parameter for command. +# +# Commands @ and $ are for use with text fields (title, filename, category, +# link, description, dupekey). Commands =, <, <=, > and >= are for use +# with numeric fields (size, age, imdbid, rageid, season, episode, priority, +# dupescore). +# +# Only fields title, filename and age are always present. The availability of +# other fields depend on rss feed provider. +# +# Any newznab attribute (encoded as "newznab:attr" in the RSS feed) can +# be used as search field with prefix "attr-", for example "attr-genre". +# +# Text search (Command @) supports supports wildcard characters * (matches +# any number of any characters), ? (matches any one character) +# and # (matches one digit). +# Text search is by default performed against words (word-search mode): the +# field content is separated into words and then each word is checked +# against pattern. If the search pattern starts and ends with * (star) +# the search is performed against the whole field content +# (substring-search mode). If the search pattern contains word separator +# characters (except * and ?) the search is performed on the whole +# field (the word-search would be obviously never successful in this +# case). Word separators are: !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~. +# +# Field "size" can have suffixes "K" or "KB" for kilobytes, "M" or "MB" +# for megabytes and "G" or "GB" for gigabytes. Field "age" can have +# suffixes "m" for minutes, "h" for hours and "d" for days. If suffix +# is not specified default is days. +# +# Examples (the trailing ; or . is not part of filter): +# 1) A: s01* -category:anime; +# 2) my show WEB-DL; +# 3) *my?show* WEB-DL size:<1.8GB age:>2h; +# 4) R: size:>9GB; +# 5) Q: HDTV. # -# NOTE: Files added via RPC calls in particular from web-interface are -# saved into incoming nzb-directory and then processed by the script. -NzbProcess= +# NOTE: This is a short documentation, for more information visit +# http://nzbget.net/RSS. +#Feed1.Filter= -# Set path to program, that must be executed after a nzb-file is added -# to queue. +# How often to check for new items (minutes). # -# This program is called each time a new nzb-file is added to queue. +# Value "0" disables the automatic check of this feed. +#Feed1.Interval=15 + +# Add nzb-files as paused (yes, no). +#Feed1.PauseNzb=no + +# Category for added nzb-files. # -# Example: ~/nzbaddedprocess.sh. +# NOTE: Feed providers may include category name within response when nzb-file +# is downloaded. If you want to use the providers category leave the option empty. +#Feed1.Category= + +# Priority for added nzb-files (number). # -# That program can modify the files in download queue (for example -# delete or pause all nfo, sfv, sample files) or do something else. +# Priority can be any integer value. The web-interface however operates +# with only six predefined priorities: -100 (very low priority), -50 +# (low priority), 0 (normal priority, default), 50 (high priority), +# 100 (very high priority) and 900 (force priority). Downloads with +# priorities equal to or greater than 900 are downloaded and +# post-processed even if the program is in paused state (force mode). +#Feed1.Priority=0 + + +############################################################################## +### INCOMING NZBS ### + +# Create subdirectory with category-name in destination-directory (yes, no). +AppendCategoryDir=yes + +# How often incoming-directory (option ) must be checked for new +# nzb-files (seconds). # -# INFO FOR DEVELOPERS: -# NZBGet passes following arguments to nzbaddedprocess-program as environment -# variables: -# NZBNA_NZBNAME - name of nzb-group. This name can be used in calls -# to nzbget edit-command using subswitch "-GN name"; -# NZBNA_FILENAME - filename of the nzb-file. If the file was added -# from nzb-directory this is the fullname with path. -# If the file was added via web-interface it contains -# only filename without path; -# NZBNA_CATEGORY - category of nzb-file (if assigned); -# NZBNA_LASTID - the id of the last file in the nzb-file. This ID can -# be used with calls to nzbget edit-command; -# NZBNA_PRIORITY - priority (default is 0). +# Value "0" disables the check. # -# In addition to these arguments NZBGet passes all -# nzbget.conf-options to nzbaddedprocess-program as environment variables. These -# variables have prefix "NZBOP_" and are written in UPPER CASE. For Example -# option "ParRepair" is passed as environment variable "NZBOP_PARREPAIR". -# The dots in option names are replaced with underscores, for example -# "SERVER1_HOST". For options with predefined possible values (yes/no, etc.) -# the values are passed always in lower case. +# NOTE: nzb-files are processed by scan and queue scripts. See +# options and . +NzbDirInterval=900 + +# How old nzb-file should at least be for it to be loaded to queue (seconds). # -# Examples: -# 1) pausing nzb-file using file-id: -# "$NZBOP_APPBIN" -c "$NZBOP_CONFIGFILE" -E G P $NZBNA_LASTID; -# 2) setting category using nzb-name: -# "$NZBOP_APPBIN" -c "$NZBOP_CONFIGFILE" -E GN K "my cat" "$NZBNA_NZBNAME"; -# 3) pausing files with extension "nzb": -# "$NZBOP_APPBIN" -c "$NZBOP_CONFIGFILE" -E FR P "$NZBNA_NZBNAME/.*\.nzb"; -NzbAddedProcess= +# NZBGet checks if nzb-file was not modified in last few seconds, defined by +# this option. That safety interval prevents the loading of files, which +# were not yet completely saved to disk, for example if they are still being +# downloaded in web-browser. +NzbDirFileAge=60 -# Check for duplicate files (yes, no). +# Check for duplicate titles (yes, no). # # If this option is enabled the program checks by adding of a new nzb-file: -# 1) if nzb-file contains duplicate entries. This check aims on detecting -# of reposted files (if first file was not fully uploaded). -# If the program find two files with identical names, only the -# biggest of these files will be added to queue; -# 2) if download queue already contains file with the same name; -# 3) if destination file on disk already exists. -# In last two cases: if the file exists it will not be added to queue. -# -# If this option is disabled, all files are downloaded and duplicate files -# are renamed to "filename_duplicate1". -# Existing files are never deleted or overwritten. +# 1) if history contains the same title (see below) with success status +# the nzb-file is not added to queue; +# 2) if download queue already contains the same title the nzb-file is +# added to queue for backup (if firt file fails); +# 3) if nzb-file contains duplicate entries. This helps to find errors +# in bad nzb-files. +# +# "Same title" means the nzb file name is same or the duplicate key is +# same. Duplicate keys are set by fetching from RSS feeds using title +# identifier fields provided by RSS provider (imdbid or rageid/season/episode). +# +# If duplicates were detected only one of them is downloaded. If download +# fails another duplicate is tried. If download succeeds all remaining +# duplicates are deleted from queue. +# +# NOTE: For automatic duplicate handling option must be +# set to "Delete" or "None". If it is set to "Pause" you will need to +# manually unpause another duplicate (if any exists in queue). +# +# NOTE: For more info on duplicates see http://nzbget.net/RSS. DupeCheck=yes @@ -475,76 +592,112 @@ SaveQueue=yes # Reload download queue on start, if it exists (yes, no). ReloadQueue=yes -# Reload url-queue on start, if it exists (yes, no). +# Continue download of partially downloaded files (yes, no). # -# For this option to work the options and must -# be also enabled. -ReloadUrlQueue=yes - -# Reload Post-processor-queue on start, if it exists (yes, no). +# If active the current state (the info about what articles were already +# downloaded) is saved every second and is reloaded after restart. This is +# about files included in download jobs (usually rar-files), not about +# download-jobs (nzb-files) itself. Download-jobs are always +# continued regardless of that option. # -# For this option to work the options and must -# be also enabled. -ReloadPostQueue=yes +# Disabling this option may slighlty reduce disk access and is +# therefore recommended on fast connections. +ContinuePartial=yes -# Reuse articles saved in temp-directory from previous program start (yes, no). +# Propagation delay to your news servers (minutes). # -# This allows to continue download of file, if program was exited before -# the file was completed. -ContinuePartial=yes +# The option sets minimum post age for nzb-files. Very recent files +# are not downloaded to avoid download failures. The files remain +# on hold in the download queue until the propagation delay expires, +# after that they are downloaded. +PropagationDelay=0 # Decode articles (yes, no). # # yes - decode articles using internal decoder (supports yEnc and UU formats); -# no - the articles will not be decoded and joined. Useful for debugging to -# look at article's source text. +# no - articles will not be decoded/joined. Useful to look at article's source text. +# +# NOTE: This option is primary for debugging purposes. You should not +# disable it. Decode=yes +# Memory limit for article cache (megabytes). +# +# Article cache helps to improve performance. First the amount of disk +# operations can be significantly reduced. Second the created files are +# less fragmented, which again speeds up the post-processing (unpacking). +# +# The article cache works best with option which can +# effectively use even small cache (like 50 MB). +# +# If option is disabled the cache should be big enough to +# hold all articles of one file (typically up to 200 MB, sometimes even +# 500 MB). Otherwise the articles are written into temporary directory +# when the cache is full, which degrades performance. +# +# Value "0" disables article cache. +# +# In 32 bit mode the maximum allowed value is 1900. +# +# NOTE: Also see option . +ArticleCache=0 + # Write decoded articles directly into destination output file (yes, no). # -# Files are posted to Usenet within artilce bodies. Each file typically -# requires hundreds of articles. -# -# When option is disabled, the program downloads all articles -# into temporary directory and then combine them into destination file. -# -# With this option enabled the program at first creates the output -# destination file with required size (total size of all articles), -# then writes on the fly decoded articles directly to the file -# without creating of any temporary files. -# -# This may improve performance but depends on OS and file system ability to -# instantly create large files without initializing them with nulls. Such -# files are called sparse files and are supported by modern file systems -# like EXT3 on Linux or NTFS on Windows. -# -# Using of this option reduces disk operations but may produce more fragmented -# files (depends on disk driver), which may slow down the post-processing. -# It's recommended to test how the option behave on your platform to find the -# best setting. -# -# INFO: a particular test on a Linux router with EXT3-partition showed that -# activating of this option results in up to 20% better performance during -# downloading. -# -# NOTE: For test try to download few big nzb-collections (each 4GB or more) -# and measure the time used for downloading and post-processing (use timestamps -# in a log-file to determine when the post-processing was ended). -# -# NOTE: When option is enabled the temporary directory (option -# ) must be located on the same partition with destination directory -# (option DestDir>) for better performance. If option is disabled -# it's better to use different drives for temporary and destination directories. -# -# NOTE: If both options and are enabled, -# the program still creates empty article-files in temp-directory. They are used -# by the option to check if a certain article was downloaded. -# To minimize disk-io it is recommended to disable option , -# if is enabled. Especially on a fast connections (where you -# would want to activate ) it should not be a problem to redownload -# an interrupted file. +# Files are posted to Usenet in multiple pieces (articles). Each file +# typically consists of hundreds of articles. +# +# When option is disabled and the article cache (option +# ) is not active or is full the program saves downloaded +# articles into temporary directory and later reads them all to write +# again into the destination file. +# +# When option is enabled the program at first creates the +# output destination file with required size (total size of all articles), +# then writes the articles directly to this file without creating of any +# temporary files. If article cache (option ) is active +# the downloaded articles are saved into cache first and are written +# into the destination file when the cache flushes. This happen when +# all articles of the file are downloaded or when the cache becomes +# full to 90%. +# +# The direct write relies on the ability of file system to create +# empty files without allocating the space on the drive (sparse files), +# which most modern file systems support including EXT3, EXT4 +# and NTFS. The notable exception is HFS+ (default file system on OSX). +# +# The direct write usually improves performance by reducing the amount +# of disk operations but may produce more fragmented files when used +# without article cache. DirectWrite=yes +# Memory limit for per article write buffer (kilobytes). +# +# When downloaded articles are written into disk the OS collects +# data in the internal buffer before flushing it into disk. This option +# controls the size of this buffer per connection/download thread. +# +# Larger buffers decrease the amount of disk operations and help +# producing less fragmented files speeding up the post-processing +# (unpack). +# +# To calculate the maximum memory required for all download threads multiply +# WriteBuffer by number of connections configured in section +# "NEWS-SERVERS". The option sets the limit, the actual buffer can be +# smaller if the article size (typically about 500 KB) is below the limit. +# +# Write-buffer is managed by OS (system libraries) and therefore +# the effect of the option is highly OS-dependent. +# +# Recommended value for computers with enough memory: 1024. +# +# Value "0" disables the setting of buffer size. In this case a buffer +# of default size (OS and compiler specific) is used, which is usually +# too small (1-4 KB) and therefore not optimal. +# +# NOTE: Also see option . +WriteBuffer=0 + # Check CRC of downloaded and decoded articles (yes, no). # # Normally this option should be enabled for better detecting of download @@ -571,8 +724,13 @@ Retries=3 # Set the interval between retries (seconds). RetryInterval=10 -# Set connection timeout (seconds). -ConnectionTimeout=60 +# Set connection timeout for article downloading (seconds). +ArticleTimeout=60 + +# Set connection timeout for URL fetching (seconds). +# +# This includes fetching of nzb-files via URLs and fetching of RSS feeds. +UrlTimeout=60 # Timeout until a download-thread should be killed (seconds). # @@ -582,8 +740,9 @@ TerminateTimeout=600 # Set the maximum download rate on program start (kilobytes/sec). # -# Value "0" means no speed control. # The download rate can be changed later via remote calls. +# +# Value "0" means no speed control. DownloadRate=0 # Accurate speed rate calculation (yes, no). @@ -604,106 +763,101 @@ DownloadRate=0 # download speed on a particular system. AccurateRate=no -# Set the size of memory buffer used by writing the articles (bytes). -# -# Bigger values decrease disk-io, but increase memory usage. -# Value "0" causes an OS-dependent default value to be used. -# With value "-1" (which means "max/auto") the program sets the size of -# buffer according to the size of current article (typically less than 500K). -# -# NOTE: The value must be written in bytes, do not use postfixes "K" or "M". -# -# NOTE: To calculate the memory usage multiply WriteBufferSize by max number -# of connections, configured in section "NEWS-SERVERS". -# -# NOTE: Typical article's size not exceed 500000 bytes, so using bigger values -# (like several megabytes) will just waste memory. -# -# NOTE: For desktop computers with large amount of memory value "-1" (max/auto) -# is recommended, but for computers with very low memory (routers, NAS) -# value "0" (default OS-dependent size) could be better alternative. -# -# NOTE: Write-buffer is managed by OS (system libraries) and therefore -# the effect of the option is highly OS-dependent. -WriteBufferSize=0 - # Pause if disk space gets below this value (megabytes). # +# Disk space is checked for directories pointed by option and +# option . +# # Value "0" disables the check. -# Only the disk space on the drive with is checked. -# The drive with is not checked. DiskSpace=250 -# Delete already downloaded files from disk, if the download of nzb-file was -# cancelled (nzb-file was deleted from queue) (yes, no). +# Delete already downloaded files from disk when nzb-file is deleted +# (yes, no). # -# NOTE: NZBGet does not delete files in a case if all remaining files in -# queue are par-files. That prevents the accidental deletion if the option -# is disabled or if the program was interrupted during -# parcheck and later restarted without reloading of post queue (option -# disabled). +# This option defines if downloaded files must be deleted when: +# 1) download of nzb-file is cancelled (deleted from queue); +# 2) history record with failure-status (par-failure or unpack-failure) +# is deleted from history. DeleteCleanupDisk=no +# Delete source nzb-file when it is not needed anymore (yes, no). +# +# Enable this option for automatic deletion of source nzb-file from +# incoming directory when the program doesn't require it anymore (the +# nzb-file has been deleted from queue and history). +NzbCleanupDisk=yes + # Keep the history of downloaded nzb-files (days). # -# Value "0" disables the history. +# After download and post-processing the items are added to history where +# their status can be checked and they can be post-processed again if +# neccessary. +# +# After expiring of defined period: +# +# If option is active the items become hidden and the amount +# of data kept is significantly reduced (for better performance), only +# fields necessary for duplicate check are kept. The item remain in the +# hidden history (forever); # -# NOTE: When a collection having paused files is added to history all remaining -# files are moved from download queue to a list of parked files. It holds files -# which could be required later if the collection will be moved back to -# download queue for downloading of remaining files. The parked files still -# consume some amount of memory and disk space. If the collection was downloaded -# and successfully par-checked or postprocessed it is recommended to discard the -# unneeded parked files before adding the collection to history. For par2-files -# that can be achieved with the option . +# If option is NOT active the items are removed from history. +# +# Value "0" disables history. Duplicate check will not work. KeepHistory=7 +# Keep the history of outdated feed items (days). +# +# After fetching of an RSS feed the information about included items (nzb-files) +# is saved to disk. This allows to detect new items on next fetch. Feed +# providers update RSS feeds constantly. Since the feed length is limited +# (usually 100 items or less) the old items get pushed away by new +# ones. When an item is not present in the feed anymore it's not necessary +# to keep the information about this item on the disk. +# +# If option is set to "0", the outdated items are deleted from history +# immediately. +# +# Otherwise the items are held in the history for defined number of +# days. Keeping of items for few days helps in situations when feed provider +# has technical issues and may response with empty feeds (or with missing +# items). When the technical issue is fixed the items may reappear in the +# feed causing the program to redownload items if they were not found in +# the feed history. +FeedHistory=7 + # Maximum number of simultaneous connections for nzb URL downloads (0-999). # # When NZB-files are added to queue via URL, the program downloads them # from the specified URL. The option limits the maximal number of connections # used for this purpose, when multiple URLs were added at the same time. -UrlConnections=4 - +UrlConnections=2 -############################################################################## -### CATEGORIES ### - -# This section defines categories available in web-interface. - -# Category name. +# Force URL-downloads even if download queue is paused (yes, no). # -# Each nzb-file can be assigned to a category. -# Category name is passed to post-processing script and can be used by it -# to perform category specific processing. -Category1.Name=Movies - -# Destination directory for this category. -# -# If this option is empty, then the default destination directory -# (option ) is used. In this case if the option -# is active, the program creates a subdirectory with category name within -# destination directory. -Category1.DestDir= - -# Default list of post-processing scripts. -# -# For more information see global option . -Category1.DefScript= - -Category2.Name=Series -Category3.Name=Music -Category4.Name=Software +# If option is active the URL-downloads (such as appending of nzb-files +# via URL or fetching of RSS feeds and nzb-files from feeds) are performed +# even if download is in paused state. +UrlForce=yes ############################################################################## ### LOGGING ### -# Create log file (yes, no). -CreateLog=yes +# How to use log file (none, append, reset, rotate). +# +# none - do not write into log file; +# append - append to the existing log file or create it; +# reset - delete existing log file on program start and create a new one; +# rotate - create new log file for each day, delete old files, +# see option . +WriteLog=rotate + +# Log file rotation period (days). +# +# Defines how long to keep old log-files, when log rotation is active +# (option is set to "rotate"). +RotateLog=3 -# Delete log file upon server start (only in server-mode) (yes, no). -ResetLog=no # How error messages must be printed (screen, log, both, none). ErrorTarget=both @@ -741,6 +895,22 @@ CreateBrokenLog=yes # newsserver etc. DumpCore=no +# Local time correction (hours or minutes). +# +# The option allows to adjust timestamps when converting system time to +# local time and vice versa. The conversion is used when printing messages +# to the log-file and by option "TaskX.Time" in the scheduler settings. +# +# The option is usually not needed if the time zone is set up correctly. +# However, sometimes, especially when using a binary compiled on onother +# platform (cross-compiling) the conversion between system and local time +# may not work properly and requires adjustment. +# +# Values in the range -24..+24 are interpreted as hours, other values as minutes. +# Example 1: set time correction to one hour: TimeCorrection=1; +# Example 2: set time correction to one hour and a half: TimeCorrection=90. +TimeCorrection=0 + # See also option in section "PATHS" @@ -785,18 +955,14 @@ UpdateInterval=200 ############################################################################## ### SCHEDULER ### -# This section defines scheduler commands. -# For each command create a set of options , , -# and . -# The following example shows how to throttle downloads in the daytime -# by 100 KB/s and download at full speed overnights: - # Time to execute the command (HH:MM). # # Multiple comma-separated values are accepted. # Asterix as hours-part means "every hour". # # Examples: "08:00", "00:00,06:00,12:00,18:00", "*:00", "*:00,*:30". +# +# NOTE: Also see option . #Task1.Time=08:00 # Week days to execute the command (1-7). @@ -808,68 +974,123 @@ UpdateInterval=200 # Examples: "1-7", "1-5", "5,6", "1-5, 7". #Task1.WeekDays=1-7 -# Command to be executed (DownloadRate, PauseDownload, UnpauseDownload, PauseScan, -# UnpauseScan, Process). +# Command to be executed (PauseDownload, UnpauseDownload, PausePostProcess, +# UnpausePostProcess, PauseScan, UnpauseScan, DownloadRate, Script, Process, +# ActivateServer, DeactivateServer, FetchFeed). # # Possible commands: -# DownloadRate - sets download rate in KB/s; -# PauseDownload - pauses download; -# UnpauseDownload - resumes download; -# PauseScan - pauses scan of incoming nzb-directory; -# UnpauseScan - resumes scan of incoming nzb-directory; -# Process - executes external program. -#Task1.Command=DownloadRate - -# Download rate to be set if the command is "DownloadRate" (kilobytes/sec). +# PauseDownload - pause download; +# UnpauseDownload - resume download; +# PausePostProcess - pause post-processing; +# UnpausePostProcess - resume post-processing; +# PauseScan - pause scan of incoming nzb-directory; +# UnpauseScan - resume scan of incoming nzb-directory; +# DownloadRate - set download rate limit; +# Script - execute one or multiple scheduler scripts. The scripts +# must be written specially for NZBGet; +# Process - execute an external (any) program; +# ActivateServer - activate news-server; +# DeactivateServer - deactivate news-server; +# FetchFeed - fetch RSS feed. +# +# On start the program checks all tasks and determines current state +# for download-pause, scan-pause, download-rate and active servers. +#Task1.Command=PauseDownload + +# Parameters for the command if needed. +# +# Some scheduler commands require additional parameters: +# DownloadRate - download rate limit to be set (kilobytes/sec). +# Example: 1000; +# Script - list of scheduler scripts to execute. The scripts in +# the list must be separated with commas or semicolons. Only +# filenames without path must be used. All scripts must be +# stored in directory pointed by option . For +# more info see below; +# Process - path to the program to execute and its parameters. +# Example: /home/user/fetch.sh. +# If filename or any parameter contains spaces it +# must be surrounded with single quotation +# marks. If filename/parameter contains single quotation marks, +# each of them must be replaced with two single quotation +# marks and the resulting filename/parameter must be +# surrounded with single quotation marks. +# Example: '/home/user/download/my scripts/task process.sh' 'world''s fun'. +# In this example one parameter (world's fun) is passed +# to the script (task process.sh). +# ActivateServer - comma separated list of news server ids or server names. +# Example: 1,3. +# Example: my news server 1, my news server 2. +# NOTE: server names should not have commas. +# DeactivateServer - see ActivateServer. +# FetchFeed - comma separated list of RSS feed ids or feed names. +# Example: 1,3. +# Example: bookmarks feed, another feed. +# NOTE: feed names should not have commas. +# NOTE: use feed id "0" to fetch all feeds. # -# Value "0" means no speed control. +# INFO FOR DEVELOPERS: +# The rest of the description is for command "Script". # -# If the option is not set to "DownloadRate" this option -# is ignored and can be omitted. -#Task1.DownloadRate=100 - -# Path to the program to execute if the command is "Process". +# NOTE: This is a short documentation, for more information visit +# http://nzbget.net/Extension_scripts. # -# Example: /home/user/fetch-nzb.sh. +# NZBGet passes following arguments to scheduler script as environment +# variables: +# NZBSP_TASKID - id number of scheduler Task. # -# If the option is not set to "Process" this option -# is ignored and can be omitted. +# In addition to these arguments NZBGet passes all nzbget.conf-options +# as environment variables. These variables have prefix "NZBOP_" and +# are written in UPPER CASE. For Example option "ParRepair" is passed as +# environment variable "NZBOP_PARREPAIR". The dots in option names are +# replaced with underscores, for example "SERVER1_HOST". For options +# with predefined possible values (yes/no, etc.) the values are passed +# always in lower case. # -# NOTE: It's allowed to add parameters to command line. If filename or -# any parameter contains spaces it must be surrounded with single quotation -# marks. If filename/parameter contains single quotation marks, each of them -# must be replaced with two single quotation marks and the resulting filename/ -# parameter must be surrounded with single quotation marks. -# Example: '/home/user/download/my scripts/task process.sh' 'world''s fun'. -# In this example one parameter (world's fun) is passed to the script -# (task process.sh). -#Task1.Process=/home/user/script.sh +# NOTE: This is a short documentation, for more information visit +# http://nzbget.net/Extension_scripts. +#Task1.Param= #Task2.Time=20:00 #Task2.WeekDays=1-7 -#Task2.Command=DownloadRate -#Task2.DownloadRate=0 +#Task2.Command=UnpauseDownload +#Task2.Param= ############################################################################## ### PAR CHECK/REPAIR ### -# Whether and how par-verification must be performed (auto, force, manual). +# Whether and how par-verification must be performed (auto, always, force, manual). # # Auto - par-check is performed when needed. One par2-file is always # downloaded. Additional par2-files are downloaded if needed # for repair. Repair is performed if the option # is enabled; +# Always - check every download (even undamaged). One par2-file is +# always downloaded. Additional par2-files are downloaded +# if needed for repair. Repair is performed if the option +# is enabled; # Force - force par-check for every download (even undamaged). All # par2-files are always downloaded. Repair is performed if # the option is enabled; # Manual - par-check is skipped. One par2-file is always # downloaded. If a damaged download is detected, all # par2-files are downloaded but neithet par-check nor par-repair -# take place. The download can be then repaired manually -# (possibly on another, faster computer). +# take place. The download can be then repaired manually, +# eventually on another faster computer. ParCheck=auto +# Check for renamed and missing files (yes, no). +# +# Par-rename restores original file names using information stored +# in par2-files. It also detects missing files (files listed in +# par2-files but not present on disk). When enabled the par-rename is +# performed as the first step of post-processing for every nzb-file. +# +# Par-rename is very fast and is highly recommended, especially if +# unpack is disabled. +ParRename=yes + # Automatic par-repair after par-verification (yes, no). # # If option is set to "Auto" or "Force" this option defines @@ -878,8 +1099,8 @@ ParCheck=auto # may take too much resources and time on a slow computers. ParRepair=yes -# What files should be scanned during par-verification (limited, -# full, auto). +# What files should be scanned during par-verification (auto, limited, +# full). # # Limited - scan only files belonging to the par-set; # Full - scan all files in the directory. This helps if the @@ -887,35 +1108,73 @@ ParRepair=yes # Auto - a limited scan is performed first. If the par-checker # detects missing files, it scans other files in the # directory until all required files are found. -# -# NOTE: for par-check/repair NZBGet uses library libpar2. The last and -# widely used version 0.2 of the library has few bugs, sometimes causing -# a crash of the program. This is especially true when using "full" or -# "auto" par-scan. NZBGet is supplied with patches addressing these -# issues. Please apply the patches to libpar2 and recompile it. ParScan=limited -# Use only par2-files with matching names (yes, no). +# Quick file verification during par-check (yes, no). # -# If par-check needs extra par-blocks it looks for paused par2-files -# in the download queue. These par2-files should have the same base name -# as the main par2-file, currently loaded in par-checker. Sometimes extra -# par2-files have non-matching names (especially if they were uploaded -# by a different poster). Normally par-checker does not use these files, but -# you can allow it to use them by setting to "no". -# There is a small side effect then: if NZB-file contains more than one -# collection of files (with different par-sets), par-checker may download -# par2-files from a wrong collection and will need to unpause other -# par2-files until all required files are downloaded. This increases the -# traffic (but not harm the par-check). +# If the option is active the files are quickly verified using +# checksums calculated during download; quick verification is very fast +# because it doesn't require the reading of files from disk, NZBGet +# knows checksums of downloaded files and quickly compares them with +# checksums stored in the par-file. # -# NOTE: Par-checker always uses only par-files added from the same NZB-file -# and the option does not change this behavior. -StrictParName=yes +# If the option is disabled the files are verified as usual. That's +# slow. Use this if the quick verification doesn't work properly. +ParQuick=yes -# Maximum allowed time for par-repair (minutes). +# Memory limit for par-repair buffer (megabytes). # -# Value "0" means unlimited. +# Set the amount of RAM that the par-checker may use during repair. Having +# the buffer as big as the total size of all damaged blocks allows for +# the optimal repair speed. The option sets the maximum buffer size, the +# allocated buffer can be smaller. +# +# If you have a lot of RAM set the option to few hundreds (MB) for the +# best repair performance. +ParBuffer=16 + +# Number of threads to use during par-repair (0-99). +# +# On multi-core CPUs for the best speed set the option to the number of +# logical cores (physical cores + hyper-threading units). If you want +# to utilize the CPU to 100% you may need to add one or two additional threads +# to compensate for wait intervals used for thread synchronization. +# +# On single-core CPUs use only one thread. +# +# Set to '0' to automatically use all available CPU cores (may not +# work on old or exotic platforms). +ParThreads=0 + +# Files to ignore during par-check. +# +# List of file extensions or file names to ignore by par-rename and +# par-check. The entries must be separated with commas. The entries +# can be file extensions or any text the file name may end with. +# +# If par-rename or par-check detect missing or damaged files they +# will ignore files matching this option and will not initiate +# repair. This avoids time costing repair for unimportant files. +# +# NOTE: Files matching the option are ignored as well. +# +# Example: .sfv, .nzb, .nfo +ParIgnoreExt=.sfv, .nzb, .nfo + +# What to do if download health drops below critical health (delete, +# pause, none). +# +# Delete - delete nzb-file from queue. If option +# is active the already downloaded files will be deleted too; +# Pause - pause nzb-file; +# None - do nothing (continue download). +# +# NOTE: For automatic duplicate handling option must be set to "Delete" +# or "None". If it is set to "Pause" you will need to manually unpause +# another duplicate (if any exists in queue). See also option . +HealthCheck=delete + +# Maximum allowed time for par-repair (minutes). # # If you use NZBGet on a very slow computer like NAS-device, it may be good to # limit the time allowed for par-repair. NZBGet calculates the estimated time @@ -928,14 +1187,12 @@ StrictParName=yes # set to a value smaller than 5 minutes, the comparison is made after the first # whole minute. # +# Value "0" means unlimited. +# # NOTE: The option limits only the time required for repairing. It doesn't # affect the first stage of parcheck - verification of files. However the # verification speed is constant, it doesn't depend on files integrity and # therefore it is not necessary to limit the time needed for the first stage. -# -# NOTE: This option requires an extended version of libpar2 (the original -# version doesn't support the cancelling of repairing). Please refer to -# NZBGet's README for info on how to apply the patch to libpar2. ParTimeLimit=0 # Pause download queue during check/repair (yes, no). @@ -955,18 +1212,17 @@ ParPauseQueue=yes # from download queue after successful check/repair. ParCleanupQueue=yes -# Delete source nzb-file after successful check/repair (yes, no). -# -# Enable this option for automatic deletion of nzb-file from incoming directory -# after successful check/repair. -NzbCleanupDisk=no - # Files to delete after successful check/repair. # # List of file extensions or file names to delete after successful -# check/repair. The entries must be separated with commas. The entries +# download. The entries must be separated with commas. The entries # can be file extensions or any text the file name may end with. # +# Files or extensions listed here are also ignored by par-rename +# and par-check. +# +# NOTE: See also option . +# # Example: .par2, .sfv ExtCleanupDisk=.par2, .sfv, _brokenlog.txt @@ -976,12 +1232,19 @@ ExtCleanupDisk=.par2, .sfv, _brokenlog.txt # Unpack downloaded nzb-files (yes, no). # +# Each download (nzb-file) has a post-processing parameter "Unpack". The option +# is the default value assigned to this pp-parameter of the download +# when it is added to queue. +# +# When nzb-file is added to queue it can have a category assigned to it. In this +# case the option overrides the global option . +# # If the download is damaged and could not be repaired using par-files # the unpacking is not performed. # -# If the option is disabled the program will try to unpack +# If the option is set to "Auto" the program tries to unpack # downloaded files first. If the unpacking fails the par-check/repair -# is performed and the unpack will be executed again. +# is performed and the unpack is executed again. Unpack=yes # Pause download queue during unpack (yes, no). @@ -1013,11 +1276,10 @@ SevenZipCmd=7z ############################################################################## -### POST-PROCESSING SCRIPTS ### +### EXTENSION SCRIPTS ### # Default list of post-processing scripts to execute after the download -# of nzb-file is completed and possibly par-checked/repaired and unpacked, -# depending on other options. +# of nzb-file is completed and possibly par-checked/repaired and unpacked. # # The scripts in the list must be separated with commas or semicolons. Only # filenames without path must be used. All scripts must be stored in directory @@ -1026,26 +1288,26 @@ SevenZipCmd=7z # Example: Cleanup.sh, Move.sh, EMail.py. # # Each download (nzb-file) has its own list of post-processing scripts. The option -# is the default value assigned to download when it is added to +# is the default value assigned to download when it is added to # queue. The list of post-processing scripts for a particular download can be # changed in the edit dialog in web-interface or using remote command "--edit/-E". # # When nzb-file is added to queue it can have a category assigned to it. In this -# case the option (if not empty) overrides the -# global option . +# case the option (if not empty) overrides the +# global option . # # NOTE: The script execution order is controlled by option , not -# by their order in option . +# by their order in option . # -# NOTE: Changing options and doesn't affect +# NOTE: Changing options and doesn't affect # already queued downloads. # # NOTE: For the list of interesting post-processing scripts see -# http://nzbget.sourceforge.net/Catalog_of_post-processing_scripts. +# http://nzbget.net/Catalog_of_post-processing_scripts. # # INFO FOR DEVELOPERS: # NOTE: This is a short documentation, for more information visit -# http://nzbget.sourceforge.net/Post-processing_scripts. +# http://nzbget.net/Extension_scripts. # # NZBGet passes following arguments to post-processing script as environment # variables: @@ -1055,19 +1317,48 @@ SevenZipCmd=7z # If download was renamed, this parameter reflects the new name; # NZBPP_NZBFILENAME - name of processed nzb-file. It includes file extension and also # may include full path; +# NZBPP_FINALDIR - final destination path if set by one of previous pp-scripts; # NZBPP_CATEGORY - category assigned to nzb-file (can be empty string); -# NZBPP_PARSTATUS - result of par-check: -# 0 = not checked: par-check is disabled or nzb-file does -# not contain any par-files; -# 1 = checked and failed to repair; -# 2 = checked and successfully repaired; -# 3 = checked and can be repaired but repair is disabled; -# 4 = par-check needed but skipped (option ParCheck=manual); -# NZBPP_UNPACKSTATUS - result of unpack: -# 0 = unpack is disabled or was skipped due to nzb-file -# properties or due to errors during par-check; -# 1 = unpack failed; -# 2 = unpack successful. +# NZBPP_TOTALSTATUS - total status of nzb-file: +# SUCCESS - everything OK; +# WARNING - download is damaged but probably can +# be repaired; user intervention is +# required; +# FAILURE - download has failed or a serious error +# occurred during post-processing (unpack, par); +# DELETED - download was deleted; post-processing +# scripts are usually not called in this case; +# however it's possible to force calling +# scripts with command "post-process again"; +# NZBPP_STATUS - complete status info for nzb-file: it consists +# of total status and status detail separated with +# slash, for example: "FAILURE/UNPACK"; for possible +# status details see documentation on web site; +# NZBPP_SCRIPTSTATUS - summary status of the scripts executed before the +# current one: +# NONE - no other scripts were executed yet or all +# of them have ended with exit code "NONE"; +# SUCCESS - all other scripts have ended with exit +# code "SUCCESS" ; +# FAILURE - at least one of the script has failed; +# NZBPP_HEALTH - download health: an integer value in the range +# from 0 (all articles failed) to 1000 (all articles +# successfully downloaded); +# NZBPP_CRITICALHEALTH - critical health for this nzb-file: an integer +# value in the range 0-1000. The critical health +# is calculated based on number and size of +# par-files. If nzb-file doesn't have any par-files +# the critical health is 1000 (100.0%). If a half +# of nzb-file were par-files its critical health +# would be 0. If NZBPP_HEALTH goes down below +# NZBPP_CRITICALHEALTH the download becomes unrepairable; +# NZBPP_TOTALARTICLES - number of articles in nzb-file; +# NZBPP_SUCCESSARTICLES - number of successfully downloaded articles; +# NZBPP_FAILEDARTICLES - number of failed articles; +# NZBPP_SERVERX_SUCCESSARTICLES - number of successfully downloaded +# articles from ServerX (X is replaced with server +# number, for example NZBPP_SERVER1_SUCCESSARTICLES); +# NZBPP_SERVERX_FAILEDARTICLES - number of failed articles from ServerX. # # If the script defines own options they are also passed as environment # variables. These variables have prefix "NZBPO_" in their names. For @@ -1087,6 +1378,31 @@ SevenZipCmd=7z # "SERVER1_HOST". For options with predefined possible values (yes/no, etc.) # the values are passed always in lower case. # +# If the script moves files it can inform the program about new location +# by printing special message into standard output (which is processed +# by NZBGet): +# echo "[NZB] DIRECTORY=/path/to/moved/files"; +# or: +# echo "[NZB] FINALDIR=/path/to/moved/files"; +# +# Command "DIRECTORY" changes the destiantion path of the download and +# affects the scripts executed after the current script as well as the +# program code itself, for example the command "Post-process again" +# will work on new location. Command "FINALDIR" just sets a separate +# property of the download and should be used when the files are moved +# into an existing directory containg other files to avoid the processing +# of those files by other scripts. +# +# To assign post-processing parameters: +# echo "[NZB] NZBPR_myvar=my value"; +# +# The prefix "NZBPR_" will be removed. In this example a post-processing +# parameter with name "myvar" and value "my value" will be associated +# with nzb-file. +# +# To inform NZBGet about bad download: +# echo "[NZB] MARK=BAD"; +# # Return value: NZBGet processes the exit code returned by the script: # 93 - post-process successful (status = SUCCESS); # 94 - post-process failed (status = FAILURE); @@ -1098,8 +1414,198 @@ SevenZipCmd=7z # All other return codes are interpreted as failure (status = FAILURE). # # NOTE: This is a short documentation, for more information visit -# http://nzbget.sourceforge.net/Post-processing_scripts. -DefScript= +# http://nzbget.net/Extension_scripts. +PostScript= + +# List of scan scripts to execute before a nzb-file is added to queue. +# +# The scripts in the list must be separated with commas or semicolons. Only +# filenames without path must be used. All scripts must be stored in directory +# pointed by option . +# +# The scripts are executed each time a new file is found in incoming +# directory (option ) or a file is received via RPC (web-interface, +# command "nzbget --append", etc.). +# +# Example: UnzipNzb.sh, ScanNotify.py. +# +# The scripts can unpack archives which were put in incoming directory, make +# filename cleanup, change nzb-name, category, priority and post-processing +# parameters of the nzb-file or do other things. +# +# INFO FOR DEVELOPERS: +# NOTE: This is a short documentation, for more information visit +# http://nzbget.net/Extension_scripts. +# +# NZBGet passes following arguments to the script as environment +# variables: +# NZBNP_DIRECTORY - path to directory, where file is located. It is a directory +# specified by the option or a subdirectory; +# NZBNP_FILENAME - name of file to be processed; +# NZBNP_NZBNAME - nzb-name (without path but with extension); +# NZBNP_CATEGORY - category of nzb-file; +# NZBNP_PRIORITY - priority of nzb-file; +# NZBNP_TOP - flag indicating that the file will be added to the top +# of queue: 0 or 1; +# NZBNP_PAUSED - flag indicating that the file will be added as +# paused: 0 or 1; +# NZBNP_DUPEKEY - duplicate key of nzb-file; +# NZBNP_DUPESCORE - duplicate score of nzb-file; +# NZBNP_DUPEMODE - duplicate mode of nzb-file: SCORE, ALL, FORCE. +# +# In addition to these arguments NZBGet passes all nzbget.conf-options +# as environment variables. These variables have prefix "NZBOP_" and +# are written in UPPER CASE. For Example option "ParRepair" is passed as +# environment variable "NZBOP_PARREPAIR". The dots in option names are +# replaced with underscores, for example "SERVER1_HOST". For options +# with predefined possible values (yes/no, etc.) the values are passed +# always in lower case. +# +# The script can change nzb-name, category, priority, +# post-processing parameters and top-/paused-flags of the nzb-file +# by printing special messages into standard output (which is processed +# by NZBGet). +# +# To change nzb-name use following syntax: +# echo "[NZB] NZBNAME=my download"; +# +# To change category: +# echo "[NZB] CATEGORY=my category"; +# +# To change priority: +# echo "[NZB] PRIORITY=signed_integer_value"; +# +# for example: to set priority higher than normal: +# echo "[NZB] PRIORITY=50"; +# +# another example: use a negative value for "lower than normal" priority: +# echo "[NZB] PRIORITY=-100"; +# +# Although priority can be any integer value, the web-interface operates +# with six predefined priorities: +# -100 - very low priority; +# -50 - low priority; +# 0 - normal priority (default); +# 50 - high priority; +# 100 - very high priority; +# 900 - force priority. +# +# Downloads with priorities equal to or greater than 900 are downloaded and +# post-processed even if the program is in paused state (force mode). +# +# To assign post-processing parameters: +# echo "[NZB] NZBPR_myvar=my value"; +# +# The prefix "NZBPR_" will be removed. In this example a post-processing +# parameter with name "myvar" and value "my value" will be associated +# with nzb-file. +# +# To change top-flag (nzb-file will be added to the top of queue): +# echo "[NZB] TOP=1"; +# +# To change paused-flag (nzb-file will be added in paused state): +# echo "[NZB] PAUSED=1"; +# +# To change duplicate key: +# echo "[NZB] DUPEKEY=tv show s01e02"; +# +# To change duplicate score: +# echo "[NZB] DUPESCORE=integer_value"; +# +# To change duplicate mode: +# echo "[NZB] DUPEMODE=(SCORE|ALL|FORCE)"; +# +# The script can delete processed file, rename it or move somewhere. +# After the calling of the script the file will be either added to queue +# (if it was an nzb-file) or renamed by adding the extension ".processed". +# +# NOTE: Files with extensions ".processed", ".queued" and ".error" are skipped +# during the directory scanning. +# +# NOTE: Files with extension ".nzb_processed" are not passed to +# scan-script before adding to queue. This feature allows scan-script +# to prevent the scanning of nzb-files extracted from archives, if +# they were already processed by the script. +# +# NOTE: Files added via RPC calls in particular from web-interface are +# saved into incoming nzb-directory and then processed by the script. +# +# NOTE: This is a short documentation, for more information visit +# http://nzbget.net/Extension_scripts. +ScanScript= + +# List of queue scripts to execute after a nzb-file is added to queue. +# +# The scripts in the list must be separated with commas or semicolons. Only +# filenames without path must be used. All scripts must be stored in directory +# pointed by option . +# +# The scripts are executed each time a new file is added to queue. +# +# Example: DeleteQueueSamples.sh, NzbAddedNotify.py. +# +# The script can modify the files in download queue (for example +# delete or pause all nfo, sfv, sample files) or do something else. +# +# INFO FOR DEVELOPERS: +# NOTE: This is a short documentation, for more information visit +# http://nzbget.net/Extension_scripts. +# +# NZBGet passes following arguments to the queue script as environment +# variables: +# NZBNA_NZBNAME - name of nzb-group. This name can be used in calls +# to nzbget edit-command using subswitch "-GN name"; +# NZBNA_FILENAME - filename of the nzb-file. If the file was added +# from nzb-directory this is the fullname with path. +# If the file was added via web-interface it contains +# only filename without path; +# NZBNA_EVENT - describes why the script was called: +# NZB_ADDED - after adding of nzb-file to queue; +# FILE_DOWNLOADED - after a file included in nzb is +# downloaded; +# NZB_DOWNLOADED - after all files in nzb are downloaded +# (before post-processing). +# In the future the list of supported events may be +# extended. To avoid conflicts with future NZBGet +# versions the script must exit if the parameter +# has a value unknown to the script. +# NZBNA_CATEGORY - category of nzb-file (if assigned); +# NZBNA_NZBID - id of the nzb-file. This ID can be used with +# calls to nzbget edit-command; +# NZBNA_PRIORITY - priority (default is 0). +# +# In addition to these arguments NZBGet passes all nzbget.conf-options +# to the script as environment variables. These variables have prefix +# "NZBOP_" and are written in UPPER CASE. For Example option "ParRepair" +# is passed as environment variable "NZBOP_PARREPAIR". The dots in option +# names are replaced with underscores, for example "SERVER1_HOST". For +# options with predefined possible values (yes/no, etc.) the values are +# passed always in lower case. +# +# The script can printing special messages into standard output (which +# is processed by NZBGet). +# +# To assign post-processing parameters: +# echo "[NZB] NZBPR_myvar=my value"; +# +# The prefix "NZBPR_" will be removed. In this example a post-processing +# parameter with name "myvar" and value "my value" will be associated +# with nzb-file. +# +# To inform NZBGet about bad download: +# echo "[NZB] MARK=BAD"; +# +# Examples of what the script can do: +# 1) pausing nzb-file using file-id: +# "$NZBOP_APPBIN" -c "$NZBOP_CONFIGFILE" -E G P $NZBNA_NZBID; +# 2) setting category using nzb-name: +# "$NZBOP_APPBIN" -c "$NZBOP_CONFIGFILE" -E GN K "my cat" "$NZBNA_NZBNAME"; +# 3) pausing files with extension "nzb": +# "$NZBOP_APPBIN" -c "$NZBOP_CONFIGFILE" -E FR P "$NZBNA_NZBNAME/.*\.nzb"; +# +# NOTE: This is a short documentation, for more information visit +# http://nzbget.net/Extension_scripts. +QueueScript= # Execution order for scripts. # @@ -1121,3 +1627,26 @@ ScriptOrder= # # NOTE: See also options and . ScriptPauseQueue=yes + +# Minimum interval between calls of queue-scripts (seconds). +# +# Queue-scripts are executed during download, after every file included in +# nzb-file is downloaded. If the files are small they may be downloaded +# very fast causing queue-scripts to be working all the time. Sometimes +# this may lead to a performance decrease on systems with slow CPUs. +# +# This option allows to reduce the number of calls of queue-scripts by +# skipping "file-downloaded"-events if the previous call of queue-scripts +# for the same download (nzb-file) were performed a short time ago +# (as defined by the option). +# +# Value "-1" disables executing of queue-scripts on +# "file-downloaded"-events. Scripts are still executed on events +# "nzb-added" and "nzb-downloaded". +# +# NOTE: This options affects only queue-scripts and only +# "file-downloaded"-events. Queue-scripts can be activated using +# option (for pure queue-scripts) or option +# (for dual-mode scripts which act as queue- and post-processing-scripts +# at the same time). +EventInterval=0 diff --git a/content/usr/local/share/nzbget/ppscripts/EMail.py b/content/usr/local/share/nzbget/ppscripts/EMail.py index 01a7836..aa43f59 100755 --- a/content/usr/local/share/nzbget/ppscripts/EMail.py +++ b/content/usr/local/share/nzbget/ppscripts/EMail.py @@ -2,7 +2,7 @@ # # E-Mail post-processing script for NZBGet # -# Copyright (C) 2013 Andrey Prygunkov +# Copyright (C) 2013-2014 Andrey Prygunkov # # 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 @@ -18,8 +18,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -# $Revision: 707 $ -# $Date: 2013-06-12 22:25:06 +0200 (Wed, 12 Jun 2013) $ +# $Revision: 1107 $ +# $Date: 2014-08-27 18:27:40 +0200 (Wed, 27 Aug 2014) $ # @@ -56,6 +56,9 @@ # SMTP server password, if required. #Password=mypass +# Append statistics to the message (yes, no). +#Statistics=yes + # Append list of files to the message (yes, no). # # Add the list of downloaded files (the content of destination directory). @@ -92,74 +95,47 @@ POSTPROCESS_ERROR=94 # Check if the script is called from nzbget 11.0 or later -if not 'NZBOP_SCRIPTDIR' in os.environ: +if not 'NZBPP_TOTALSTATUS' in os.environ: print('*** NZBGet post-processing script ***') - print('This script is supposed to be called from nzbget (11.0 or later).') + print('This script is supposed to be called from nzbget (13.0 or later).') sys.exit(POSTPROCESS_ERROR) print('[DETAIL] Script successfully started') sys.stdout.flush() -required_options = ('NZBPO_FROM', 'NZBPO_TO', 'NZBPO_SERVER', 'NZBPO_PORT', 'NZBPO_ENCRYPTION', - 'NZBPO_USERNAME', 'NZBPO_PASSWORD', 'NZBPO_FILELIST', 'NZBPO_BROKENLOG', 'NZBPO_POSTPROCESSLOG') +required_options = ('NZBPO_FROM', 'NZBPO_TO', 'NZBPO_SERVER', 'NZBPO_PORT', 'NZBPO_ENCRYPTION', 'NZBPO_USERNAME', 'NZBPO_PASSWORD') for optname in required_options: if (not optname in os.environ): print('[ERROR] Option %s is missing in configuration file. Please check script settings' % optname[6:]) sys.exit(POSTPROCESS_ERROR) + +status = os.environ['NZBPP_STATUS'] +total_status = os.environ['NZBPP_TOTALSTATUS'] + +# If any script fails the status of the item in the history is "WARNING/SCRIPT". +# This status however is not passed to pp-scripts in the env var "NZBPP_STATUS" +# because most scripts are independent of each other and should work even +# if a previous script has failed. But not in the case of E-Mail script, +# which should take the status of the previous scripts into account as well. +if total_status == 'SUCCESS' and os.environ['NZBPP_SCRIPTSTATUS'] == 'FAILURE': + total_status = 'WARNING' + status = 'WARNING/SCRIPT' -# Check par and unpack status for errors. -success=False -if os.environ['NZBPP_PARSTATUS'] == '1' or os.environ['NZBPP_UNPACKSTATUS'] == '1': - subject = 'Failure for "%s"' % (os.environ['NZBPP_NZBNAME']) - text = 'Download of "%s" has failed.' % (os.environ['NZBPP_NZBNAME']) -elif os.environ['NZBPP_PARSTATUS'] == '4': - subject = 'Damaged for "%s"' % (os.environ['NZBPP_NZBNAME']) - text = 'Download of "%s" requires par-repair.' % (os.environ['NZBPP_NZBNAME']) -else: +success = total_status == 'SUCCESS' +if success: subject = 'Success for "%s"' % (os.environ['NZBPP_NZBNAME']) text = 'Download of "%s" has successfully completed.' % (os.environ['NZBPP_NZBNAME']) - success=True - -# NZBPP_PARSTATUS - result of par-check: -# 0 = not checked: par-check is disabled or nzb-file does -# not contain any par-files; -# 1 = checked and failed to repair; -# 2 = checked and successfully repaired; -# 3 = checked and can be repaired but repair is disabled. -# 4 = par-check needed but skipped (option ParCheck=manual); -parStatus = { '0': 'skipped', '1': 'failed', '2': 'repaired', '3': 'repairable', '4': 'manual' } -text += '\nPar-Status: %s' % parStatus[os.environ['NZBPP_PARSTATUS']] - -# NZBPP_UNPACKSTATUS - result of unpack: -# 0 = unpack is disabled or was skipped due to nzb-file -# properties or due to errors during par-check; -# 1 = unpack failed; -# 2 = unpack successful. -unpackStatus = { '0': 'skipped', '1': 'failed', '2': 'success' } -text += '\nUnpack-Status: %s' % unpackStatus[os.environ['NZBPP_UNPACKSTATUS']] - -# add list of downloaded files -if os.environ['NZBPO_FILELIST'] == 'yes': - text += '\n\nFiles:' - for dirname, dirnames, filenames in os.walk(os.environ['NZBPP_DIRECTORY']): - for filename in filenames: - text += '\n' + os.path.join(dirname, filename)[len(os.environ['NZBPP_DIRECTORY']) + 1:] +else: + subject = 'Failure for "%s"' % (os.environ['NZBPP_NZBNAME']) + text = 'Download of "%s" has failed.' % (os.environ['NZBPP_NZBNAME']) -# add _brokenlog.txt (if exists) -if os.environ['NZBPO_BROKENLOG'] == 'yes': - brokenlog = '%s/_brokenlog.txt' % os.environ['NZBPP_DIRECTORY'] - if os.path.exists(brokenlog): - text += '\n\nBrokenlog:\n' + open(brokenlog, 'r').read().strip() +text += '\nStatus: %s' % status -# add post-processing log -if os.environ['NZBPO_POSTPROCESSLOG'] == 'Always' or \ - (os.environ['NZBPO_POSTPROCESSLOG'] == 'OnFailure' and not success): - # To get the post-processing log we connect to NZBGet via XML-RPC - # and call method "postqueue", which returns the list of post-processing job. - # The first item in the list is current job. This item has a field 'Log', - # containing an array of log-entries. - # For more info visit http://nzbget.sourceforge.net/RPC_API_reference - +if os.environ.get('NZBPO_STATISTICS') == 'yes' or \ + os.environ.get('NZBPO_POSTPROCESSLOG') == 'Always' or \ + (os.environ.get('NZBPO_POSTPROCESSLOG') == 'OnFailure' and not success): + # To get statistics or the post-processing log we connect to NZBGet via XML-RPC. + # For more info visit http://nzbget.net/RPC_API_reference # First we need to know connection info: host, port and password of NZBGet server. # NZBGet passes all configuration options to post-processing script as # environment variables. @@ -175,6 +151,73 @@ # Create remote server object server = ServerProxy(rpcUrl) + +if os.environ.get('NZBPO_STATISTICS') == 'yes': + # Find correct nzb in method listgroups + groups = server.listgroups(0) + nzbID = int(os.environ['NZBPP_NZBID']) + for nzbGroup in groups: + if nzbGroup['NZBID'] == nzbID: + break + + text += '\n\nStatistics:'; + + # add download size + DownloadedSize = float(nzbGroup['DownloadedSizeMB']) + unit = ' MB' + if DownloadedSize > 1024: + DownloadedSize = DownloadedSize / 1024 # GB + unit = ' GB' + text += '\nDownloaded size: %.2f' % (DownloadedSize) + unit + + # add average download speed + DownloadedSizeMB = float(nzbGroup['DownloadedSizeMB']) + DownloadTimeSec = float(nzbGroup['DownloadTimeSec']) + if DownloadTimeSec > 0: # check x/0 errors + avespeed = (DownloadedSizeMB/DownloadTimeSec) # MB/s + unit = ' MB/s' + if avespeed < 1: + avespeed = avespeed * 1024 # KB/s + unit = ' KB/s' + text += '\nAverage download speed: %.2f' % (avespeed) + unit + + def format_time_sec(sec): + Hour = sec/3600 + Min = (sec - (sec/3600)*3600)/60 + Sec = (sec - (sec/3600)*3600)%60 + return '%d:%02d:%02d' % (Hour,Min,Sec) + + # add times + text += '\nTotal time: ' + format_time_sec(int(nzbGroup['DownloadTimeSec']) + int(nzbGroup['PostTotalTimeSec'])) + text += '\nDownload time: ' + format_time_sec(int(nzbGroup['DownloadTimeSec'])) + text += '\nVerification time: ' + format_time_sec(int(nzbGroup['ParTimeSec']) - int(nzbGroup['RepairTimeSec'])) + text += '\nRepair time: ' + format_time_sec(int(nzbGroup['RepairTimeSec'])) + text += '\nUnpack time: ' + format_time_sec(int(nzbGroup['UnpackTimeSec'])) + +# add list of downloaded files +files = False +if os.environ.get('NZBPO_FILELIST') == 'yes': + text += '\n\nFiles:' + for dirname, dirnames, filenames in os.walk(os.environ['NZBPP_DIRECTORY']): + for filename in filenames: + text += '\n' + os.path.join(dirname, filename)[len(os.environ['NZBPP_DIRECTORY']) + 1:] + files = True + if not files: + text += '\n' + +# add _brokenlog.txt (if exists) +if os.environ.get('NZBPO_BROKENLOG') == 'yes': + brokenlog = '%s/_brokenlog.txt' % os.environ['NZBPP_DIRECTORY'] + if os.path.exists(brokenlog): + text += '\n\nBrokenlog:\n' + open(brokenlog, 'r').read().strip() + +# add post-processing log +if os.environ.get('NZBPO_POSTPROCESSLOG') == 'Always' or \ + (os.environ.get('NZBPO_POSTPROCESSLOG') == 'OnFailure' and not success): + # To get the post-processing log we call method "postqueue", which returns + # the list of post-processing job. + # The first item in the list is current job. This item has a field 'Log', + # containing an array of log-entries. # Call remote method 'postqueue'. The only parameter tells how many log-entries to return as maximum. postqueue = server.postqueue(10000) @@ -193,6 +236,8 @@ msg['Subject'] = subject msg['From'] = os.environ['NZBPO_FROM'] msg['To'] = os.environ['NZBPO_TO'] +msg['Date'] = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000") +msg['X-Application'] = 'NZBGet' # Send message print('[DETAIL] Sending E-Mail') diff --git a/content/usr/local/share/nzbget/ppscripts/Logger.py b/content/usr/local/share/nzbget/ppscripts/Logger.py index 5444826..663ba34 100755 --- a/content/usr/local/share/nzbget/ppscripts/Logger.py +++ b/content/usr/local/share/nzbget/ppscripts/Logger.py @@ -18,8 +18,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -# $Revision: 707 $ -# $Date: 2013-06-12 22:25:06 +0200 (Wed, 12 Jun 2013) $ +# $Revision: 978 $ +# $Date: 2014-04-04 23:45:48 +0200 (Fri, 04 Apr 2014) $ # @@ -64,7 +64,7 @@ # and call method "postqueue", which returns the list of post-processing job. # The first item in the list is current job. This item has a field 'Log', # containing an array of log-entries. -# For more info visit http://nzbget.sourceforge.net/RPC_API_reference +# For more info visit http://nzbget.net/RPC_API_reference # First we need to know connection info: host, port and password of NZBGet server. # NZBGet passes all configuration options to post-processing script as diff --git a/content/usr/local/share/nzbget/webui/config.js b/content/usr/local/share/nzbget/webui/config.js index 1f5a819..415c946 100644 --- a/content/usr/local/share/nzbget/webui/config.js +++ b/content/usr/local/share/nzbget/webui/config.js @@ -1,7 +1,7 @@ /* * This file is part of nzbget * - * Copyright (C) 2012-2013 Andrey Prygunkov + * Copyright (C) 2012-2014 Andrey Prygunkov * * 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 @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * $Revision: 716 $ - * $Date: 2013-06-26 22:52:10 +0200 (Wed, 26 Jun 2013) $ + * $Revision: 1097 $ + * $Date: 2014-08-19 21:56:09 +0200 (Tue, 19 Aug 2014) $ * */ @@ -65,7 +65,7 @@ var Options = (new function($) // loading config templates and build list of post-processing parameters _this.postParamConfig = []; - RPC.call('configtemplates', [], function(data) + RPC.call('configtemplates', [false], function(data) { initPostParamConfig(data); RPC.next(); @@ -116,7 +116,7 @@ var Options = (new function($) function serverValuesLoaded(data) { serverValues = data; - RPC.call('configtemplates', [], serverTemplateLoaded, loadServerTemplateError); + RPC.call('configtemplates', [true], serverTemplateLoaded, loadServerTemplateError); } function serverTemplateLoaded(data) @@ -152,6 +152,10 @@ var Options = (new function($) scriptConfig.name = scriptConfig.name.replace(/\\/, ' \\ ').replace(/\//, ' / '); scriptConfig.shortName = shortScriptName(scriptName); scriptConfig.shortName = scriptConfig.shortName.replace(/\\/, ' \\ ').replace(/\//, ' / '); + scriptConfig.post = serverTemplateData[i].PostScript; + scriptConfig.scan = serverTemplateData[i].ScanScript; + scriptConfig.queue = serverTemplateData[i].QueueScript; + scriptConfig.scheduler = serverTemplateData[i].SchedulerScript; mergeValues(scriptConfig.sections, serverValues); config.push(scriptConfig); } @@ -416,32 +420,35 @@ var Options = (new function($) for (var i=1; i < data.length; i++) { - var scriptName = data[i].Name; - var sectionId = (scriptName + ':').replace(/ |\/|[\.|$|\:|\*]/g, '_'); - var option = {}; - option.name = scriptName + ':'; - option.caption = shortScriptName(scriptName); - option.caption = option.caption.replace(/\\/, ' \\ ').replace(/\//, ' / '); - - option.defvalue = 'no'; - option.description = (data[i].Template.trim().split('\n')[0].substr(1, 1000).trim() || 'Post-processing script ' + scriptName + '.'); - option.value = null; - option.sectionId = sectionId; - option.select = ['yes', 'no']; - section.options.push(option); - - var templateData = data[i].Template; - var postConfig = readConfigTemplate(templateData, POSTPARAM_SECTIONS, undefined, scriptName + ':'); - for (var j=0; j < postConfig.sections.length; j++) - { - var sec = postConfig.sections[j]; - if (!sec.hidden) + if (data[i].PostScript) + { + var scriptName = data[i].Name; + var sectionId = (scriptName + ':').replace(/ |\/|[\.|$|\:|\*]/g, '_'); + var option = {}; + option.name = scriptName + ':'; + option.caption = shortScriptName(scriptName); + option.caption = option.caption.replace(/\\/, ' \\ ').replace(/\//, ' / '); + + option.defvalue = 'no'; + option.description = (data[i].Template.trim().split('\n')[0].substr(1, 1000).trim() || 'Post-processing script ' + scriptName + '.'); + option.value = null; + option.sectionId = sectionId; + option.select = ['yes', 'no']; + section.options.push(option); + + var templateData = data[i].Template; + var postConfig = readConfigTemplate(templateData, POSTPARAM_SECTIONS, undefined, scriptName + ':'); + for (var j=0; j < postConfig.sections.length; j++) { - for (var n=0; n < sec.options.length; n++) + var sec = postConfig.sections[j]; + if (!sec.hidden) { - var option = sec.options[n]; - option.sectionId = sectionId; - section.options.push(option); + for (var n=0; n < sec.options.length; n++) + { + var option = sec.options[n]; + option.sectionId = sectionId; + section.options.push(option); + } } } } @@ -465,16 +472,21 @@ var Config = (new function($) var $ConfigInfo; var $ConfigTitle; var $ConfigTable; + var $ViewButton; + var $LeaveConfigDialog; var $Body; // State - var config; + var config = null; var values; var filterText = ''; var lastSection; var reloadTime; var updateTabInfo; var restored = false; + var compactMode = false; + var configSaved = false; + var leaveTarget; this.init = function(options) { @@ -488,12 +500,15 @@ var Config = (new function($) $ConfigContent = $('#ConfigContent'); $ConfigInfo = $('#ConfigInfo'); $ConfigTitle = $('#ConfigTitle'); + $ViewButton = $('#Config_ViewButton'); + $LeaveConfigDialog = $('#LeaveConfigDialog'); Util.show('#ConfigBackupSafariNote', $.browser.safari); $('#ConfigTable_filter').val(''); + compactMode = UISettings.read('$Config_ViewCompact', 'no') == 'yes'; + setViewMode(); - $('#ConfigTabLink').on('show', show); - $('#ConfigTabLink').on('shown', shown); + $(window).bind('beforeunload', userLeavesPage); $ConfigNav.on('click', 'li > a', navClick); @@ -507,20 +522,12 @@ var Config = (new function($) }); } - this.cleanup = function() - { - Options.cleanup(); - config = null; - $ConfigNav.children().not('.config-static').remove(); - $ConfigData.children().not('.config-static').remove(); - } - this.config = function() { return config; } - function show() + this.show = function() { removeSaveBanner(); $('#ConfigSaved').hide(); @@ -528,9 +535,10 @@ var Config = (new function($) $('#ConfigLoadServerTemplateError').hide(); $('#ConfigLoadError').hide(); $ConfigContent.hide(); + configSaved = false; } - function shown() + this.shown = function() { Options.loadConfig({ complete: buildPage, @@ -539,6 +547,14 @@ var Config = (new function($) }); } + this.hide = function() + { + Options.cleanup(); + config = null; + $ConfigNav.children().not('.config-static').remove(); + $ConfigData.children().not('.config-static').remove(); + } + function loadConfigError(message, resultObj) { $('#ConfigLoadInfo').hide(); @@ -574,8 +590,9 @@ var Config = (new function($) for (var j=0; j < section.options.length; j++) { var option = section.options[j]; - if ((option.Name && option.Name.toLowerCase() === name) || - (option.name && option.name.toLowerCase() === name)) + if (!option.template && + ((option.Name && option.Name.toLowerCase() === name) || + (option.name && option.name.toLowerCase() === name))) { return option; } @@ -764,7 +781,7 @@ var Config = (new function($) } else if (option.name.toLowerCase().indexOf('username') > -1 || option.name.toLowerCase().indexOf('password') > -1 || - option.name.indexOf('IP') > -1) + (option.name.indexOf('IP') > -1 && option.name.toLowerCase() !== 'authorizedip')) { option.type = 'text'; html += ''; @@ -775,7 +792,7 @@ var Config = (new function($) html += '
'; html += ''; html += ''; - html += ''; + html += ''; html += '
'; } else @@ -788,14 +805,15 @@ var Config = (new function($) { var htmldescr = option.description; htmldescr = htmldescr.replace(/NOTE: do not forget to uncomment the next line.\n/, ''); - htmldescr = htmldescr.replace(/\/g, 'CLOSETAG'); - htmldescr = htmldescr.replace(/OPENTAG/g, ''); - htmldescr = htmldescr.replace(/CLOSETAG/g, ''); + + // replace option references + var exp = /\<([A-Z0-9]*)\>/ig; + htmldescr = htmldescr.replace(exp, '$1'); + htmldescr = htmldescr.replace(/&/g, '&'); // replace URLs - var exp = /(http:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; + exp = /(http:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; htmldescr = htmldescr.replace(exp, "$1"); // highlight first line @@ -812,6 +830,12 @@ var Config = (new function($) htmldescr += ''; } + if (htmldescr.indexOf('MORE INFO:') > -1) + { + htmldescr = htmldescr.replace(/MORE INFO:
/g, ''); + htmldescr += ''; + } + if (section.multi) { // replace strings like "TaskX.Command" and "Task1.Command" @@ -846,6 +870,11 @@ var Config = (new function($) html += '

'; html += ''; + if (setname.toLowerCase() === 'feed') + { + html += ' '; + } html += '
'; html += '
'; } @@ -899,6 +928,8 @@ var Config = (new function($) $ConfigNav.append(html); } } + + notifyChanges(); $ConfigNav.append('
  • '); $ConfigNav.append('
  • SEARCH RESULTS
  • '); @@ -967,13 +998,12 @@ var Config = (new function($) option.sectionId = firstVisibleSection.id; option.select = []; var description = conf.description; - option.description = description !== '' ? description : 'No description available.\n\nNOTE: The script doesn\'t have a description section. '+ - 'It\'s either not NZBGet script or a script created for an older NZBGet version and might not work properly.'; + option.description = description !== '' ? description : 'No description available.'; option.nocontent = true; firstVisibleSection.options.unshift(option); } - // register editors for options "DefScript" and "ScriptOrder" + // register editors for certain options var conf = config[0]; for (var j=0; j < conf.sections.length; j++) { @@ -986,14 +1016,54 @@ var Config = (new function($) { option.editor = { caption: 'Reorder', click: 'Config.editScriptOrder' }; } - if (optname.indexOf('defscript') > -1) + if (optname.indexOf('postscript') > -1) + { + option.editor = { caption: 'Choose', click: 'Config.editPostScript' }; + } + if (optname.indexOf('scanscript') > -1) + { + option.editor = { caption: 'Choose', click: 'Config.editScanScript' }; + } + if (optname.indexOf('queuescript') > -1) { - option.editor = { caption: 'Choose', click: 'Config.editDefScript' }; + option.editor = { caption: 'Choose', click: 'Config.editQueueScript' }; + } + if (optname.indexOf('task') > -1 && optname.indexOf('.param') > -1) + { + option.editor = { caption: 'Choose', click: 'Config.editSchedulerScript' }; + } + if (optname.indexOf('task') > -1 && optname.indexOf('.command') > -1) + { + option.onchange = Config.schedulerCommandChanged; + } + if (optname.indexOf('.filter') > -1) + { + option.editor = { caption: 'Change', click: 'Config.editFilter' }; } } } } + function notifyChanges() + { + for (var k=0; k < config.length; k++) + { + var sections = config[k].sections; + for (var i=0; i < sections.length; i++) + { + var section = sections[i]; + for (var j=0; j < section.options.length; j++) + { + var option = section.options[j]; + if (option.onchange && !option.template) + { + option.onchange(option); + } + } + } + } + } + function scrollOptionIntoView(optFormId) { var option = findOptionById(optFormId); @@ -1017,14 +1087,31 @@ var Config = (new function($) var state = $(control).val().toLowerCase(); $('.btn', $(control).parent()).removeClass('btn-primary'); $(control).addClass('btn-primary'); + + // not for page Postprocess in download details + if (config) + { + var optFormId = $(control).parent().attr('id'); + var option = findOptionById(optFormId); + if (option.onchange) + { + option.onchange(option); + } + } } function switchGetValue(control) { - var state = $('.btn-primary', $(control).parent()).val(); + var state = $('.btn-primary', control).val(); return state; } + function switchSetValue(control, value) + { + $('.btn', control).removeClass('btn-primary'); + $('.btn@[value=' + value + ']', control).addClass('btn-primary'); + } + /*** CHANGE/ADD/REMOVE OPTIONS *************************************************************/ function navClick(event) @@ -1040,6 +1127,7 @@ var Config = (new function($) $('li', $ConfigNav).removeClass('active'); link.closest('li').addClass('active'); $ConfigContent.removeClass('search'); + Util.show($ViewButton, sectionId !== 'Config-Info'); $ConfigInfo.hide(); @@ -1141,6 +1229,7 @@ var Config = (new function($) $('.config-settitle.' + section.id + '.multiid' + oldMultiId, $ConfigData).text(setname + newMultiId); $('.' + section.id + '.multiid' + oldMultiId + ' .config-multicaption', $ConfigData).text(setname + newMultiId + '.'); $('.' + section.id + '.multiid' + oldMultiId + ' .config-delete', $ConfigData).text('Delete ' + setname + newMultiId).attr('data-multiid', newMultiId); + $('.' + section.id + '.multiid' + oldMultiId + ' .config-feed', $ConfigData).attr('data-multiid', newMultiId); //update class $('.' + section.id + '.multiid' + oldMultiId, $ConfigData).removeClass('multiid' + oldMultiId).addClass('multiid' + newMultiId); @@ -1151,6 +1240,9 @@ var Config = (new function($) option.formId = option.formId.replace(new RegExp(option.multiid), newMultiId); $('#' + oldFormId).attr('id', option.formId); + // update editor id + $('#' + oldFormId + '_Editor').attr('id', option.formId + '_Editor'); + // update name option.name = option.name.replace(new RegExp(option.multiid), newMultiId); @@ -1181,6 +1273,7 @@ var Config = (new function($) multiid++; // create new multi set + var addedOptions = []; for (var j=0; j < section.options.length; j++) { var option = section.options[j]; @@ -1194,6 +1287,7 @@ var Config = (new function($) newoption.template = false; newoption.multiid = multiid; section.options.push(newoption); + addedOptions.push(newoption); } } @@ -1210,6 +1304,15 @@ var Config = (new function($) div.hide(); addButton.parent().before(div); + for (var j=0; j < addedOptions.length; j++) + { + var option = addedOptions[j]; + if (option.onchange) + { + option.onchange(option); + } + } + div.slideDown('normal', function() { var opts = div.children(); @@ -1219,17 +1322,93 @@ var Config = (new function($) }); } + this.viewMode = function() + { + compactMode = !compactMode; + UISettings.write('$Config_ViewCompact', compactMode ? 'yes' : 'no'); + setViewMode(); + } + + function setViewMode() + { + $('#Config_ViewCompact i').toggleClass('icon-ok', compactMode).toggleClass('icon-empty', !compactMode); + $ConfigContent.toggleClass('hide-help-block', compactMode); + } + /*** OPTION SPECIFIC EDITORS *************************************************/ + this.editScriptOrder = function(optFormId) { var option = findOptionById(optFormId); - ScriptListDialog.showModal(option, config); + ScriptListDialog.showModal(option, config, null); + } + + this.editPostScript = function(optFormId) + { + var option = findOptionById(optFormId); + ScriptListDialog.showModal(option, config, 'post'); + } + + this.editScanScript = function(optFormId) + { + var option = findOptionById(optFormId); + ScriptListDialog.showModal(option, config, 'scan'); } - this.editDefScript = function(optFormId) + this.editQueueScript = function(optFormId) { var option = findOptionById(optFormId); - ScriptListDialog.showModal(option, config); + ScriptListDialog.showModal(option, config, 'queue'); + } + + this.editSchedulerScript = function(optFormId) + { + var option = findOptionById(optFormId); + var command = getOptionValue(findOptionById(optFormId.replace(/Param/, 'Command'))); + if (command !== 'Script') + { + alert('This button is to choose scheduler scripts when option TaskX.Command is set to "Script".'); + return; + } + ScriptListDialog.showModal(option, config, 'scheduler'); + } + + this.schedulerCommandChanged = function(option) + { + var command = getOptionValue(option); + var btnId = option.formId.replace(/Command/, 'Param_Editor'); + Util.show('#' + btnId, command === 'Script'); + } + + /*** RSS FEEDS ********************************************************************/ + + this.editFilter = function(optFormId) + { + var option = findOptionById(optFormId); + FeedFilterDialog.showModal( + getOptionValue(findOptionByName('Feed' + option.multiid + '.Name')), + getOptionValue(findOptionByName('Feed' + option.multiid + '.URL')), + getOptionValue(findOptionByName('Feed' + option.multiid + '.Filter')), + getOptionValue(findOptionByName('Feed' + option.multiid + '.PauseNzb')), + getOptionValue(findOptionByName('Feed' + option.multiid + '.Category')), + getOptionValue(findOptionByName('Feed' + option.multiid + '.Priority')), + function(filter) + { + var control = $('#' + option.formId); + control.val(filter); + }); + } + + this.previewFeed = function(control, setname, sectionId) + { + var multiid = parseInt($(control).attr('data-multiid')); + FeedDialog.showModal(0, + getOptionValue(findOptionByName('Feed' + multiid + '.Name')), + getOptionValue(findOptionByName('Feed' + multiid + '.URL')), + getOptionValue(findOptionByName('Feed' + multiid + '.Filter')), + getOptionValue(findOptionByName('Feed' + multiid + '.PauseNzb')), + getOptionValue(findOptionByName('Feed' + multiid + '.Category')), + getOptionValue(findOptionByName('Feed' + multiid + '.Priority'))); } /*** SAVE ********************************************************************/ @@ -1248,6 +1427,20 @@ var Config = (new function($) } this.getOptionValue = getOptionValue; + function setOptionValue(option, value) + { + var control = $('#' + option.formId); + if (option.type === 'switch') + { + switchSetValue(control, value); + } + else + { + control.val(value); + } + } + this.setOptionValue = setOptionValue; + // Checks if there are obsolete or invalid options function invalidOptionsExist() { @@ -1266,7 +1459,7 @@ var Config = (new function($) return false; } - function prepareSaveRequest() + function prepareSaveRequest(onlyUserChanges) { var modified = false; var request = []; @@ -1291,7 +1484,14 @@ var Config = (new function($) } if (newValue != null) { - modified = modified || (oldValue != newValue) || (option.value === null); + if (onlyUserChanges) + { + modified = modified || (oldValue != newValue && oldValue !== null); + } + else + { + modified = modified || (oldValue != newValue) || (option.value === null); + } var opt = {Name: option.name, Value: newValue}; request.push(opt); } @@ -1302,12 +1502,14 @@ var Config = (new function($) } } - return modified || invalidOptionsExist() || restored ? request : []; + return modified || (!onlyUserChanges && invalidOptionsExist()) || restored ? request : []; } this.saveChanges = function() { - var serverSaveRequest = prepareSaveRequest(); + $LeaveConfigDialog.modal('hide'); + + var serverSaveRequest = prepareSaveRequest(false); if (serverSaveRequest.length === 0) { @@ -1318,7 +1520,6 @@ var Config = (new function($) showSaveBanner(); Util.show('#ConfigSaved_Reload, #ConfigReload', serverSaveRequest.length > 0); - Util.show('#ConfigClose, #ConfigSaved_Close', serverSaveRequest.length === 0); if (serverSaveRequest.length > 0) { @@ -1348,11 +1549,34 @@ var Config = (new function($) { Notification.show('#Notif_Config_Failed'); } + configSaved = true; } - this.close = function() + this.canLeaveTab = function(target) { - $('#DownloadsTabLink').tab('show'); + if (!config || prepareSaveRequest(true).length === 0 || configSaved) + { + return true; + } + + leaveTarget = target; + $LeaveConfigDialog.modal({backdrop: 'static'}); + return false; + } + + function userLeavesPage(e) + { + if (config && !configSaved && !UISettings.connectionError && prepareSaveRequest(true).length > 0) + { + return "Discard changes?"; + } + } + + this.discardChanges = function() + { + configSaved = true; + $LeaveConfigDialog.modal('hide'); + leaveTarget.click(); } this.scrollToOption = function(event, control) @@ -1495,16 +1719,9 @@ var Config = (new function($) /*** RELOAD ********************************************************************/ - this.reloadConfirm = function() - { - ConfirmDialog.showModal('ReloadConfirmDialog', Config.reload); - } - - this.reload = function() + function restart(callback) { Refresher.pause(); - - $('#ConfigReloadAction').text('Stopping all activities and reloading...'); $('#ConfigReloadInfoNotes').hide(); $('body').fadeOut(function() @@ -1515,10 +1732,21 @@ var Config = (new function($) $('body').show(); $('#ConfigReloadInfo').fadeIn(); reloadTime = new Date(); - RPC.call('reload', [], reloadCheckStatus); + callback(); }); } + this.reloadConfirm = function() + { + ConfirmDialog.showModal('ReloadConfirmDialog', Config.reload); + } + + this.reload = function() + { + $('#ConfigReloadAction').text('Stopping all activities and reloading...'); + restart(function() { RPC.call('reload', [], reloadCheckStatus); }); + } + function reloadCheckStatus() { RPC.call('status', [], function(status) @@ -1561,6 +1789,42 @@ var Config = (new function($) Options.reloadConfig(values, buildPage); restored = true; } + + /*** SHUTDOWN ********************************************************************/ + + this.shutdownConfirm = function() + { + ConfirmDialog.showModal('ShutdownConfirmDialog', Config.shutdown); + } + + this.shutdown = function() + { + $('#ConfigReloadTitle').text('Shutdown NZBGet'); + $('#ConfigReloadAction').text('Stopping all activities...'); + restart(function() { RPC.call('shutdown', [], shutdownCheckStatus); }); + } + + function shutdownCheckStatus() + { + RPC.call('version', [], function(version) + { + // the program still runs, waiting 0.5 sec. and retrying + setTimeout(shutdownCheckStatus, 500); + }, + function() + { + // the program has been stopped + $('#ConfigReloadTransmit').hide(); + $('#ConfigReloadAction').text('The program has been stopped.'); + }); + } + + /*** UPDATE ********************************************************************/ + + this.checkUpdates = function() + { + UpdateDialog.showModal(); + } }(jQuery)); @@ -1575,7 +1839,9 @@ var ScriptListDialog = (new function($) var $ScriptTable; var option; var config; + var kind; var scriptList; + var allScripts; var orderChanged; var orderMode; @@ -1607,10 +1873,11 @@ var ScriptListDialog = (new function($) }); } - this.showModal = function(_option, _config) + this.showModal = function(_option, _config, _kind) { option = _option; config = _config; + kind = _kind; orderChanged = false; orderMode = option.name === 'ScriptOrder'; @@ -1631,7 +1898,7 @@ var ScriptListDialog = (new function($) Util.show('#ScriptListDialog_OrderInfo', orderMode, 'inline-block'); buildScriptList(); - var selectedList = parseCommaList(Config.getOptionValue(option)); + var selectedList = Util.parseCommaList(Config.getOptionValue(option)); updateTable(selectedList); $ScriptListDialog.modal({backdrop: 'static'}); @@ -1663,33 +1930,25 @@ var ScriptListDialog = (new function($) $ScriptTable.fasttable('update', data); } - function parseCommaList(commaList) - { - var valueList = commaList.split(/[,;]+/); - for (var i=0; i < valueList.length; i++) - { - valueList[i] = valueList[i].trim(); - if (valueList[i] === '') - { - valueList.splice(i, 1); - i--; - } - } - return valueList; - } - function buildScriptList() { - var orderList = parseCommaList(Config.getOptionValue(Config.findOptionByName('ScriptOrder'))); + var orderList = Util.parseCommaList(Config.getOptionValue(Config.findOptionByName('ScriptOrder'))); var availableScripts = []; + var availableAllScripts = []; for (var i=1; i < config.length; i++) { - availableScripts.push(config[i].scriptName); + availableAllScripts.push(config[i].scriptName); + if (!kind || config[i][kind]) + { + availableScripts.push(config[i].scriptName); + } } availableScripts.sort(); + availableAllScripts.sort(); scriptList = []; + allScripts = []; // first add all scripts from orderList for (var i=0; i < orderList.length; i++) @@ -1699,18 +1958,32 @@ var ScriptListDialog = (new function($) { scriptList.push(scriptName); } + if (availableAllScripts.indexOf(scriptName) > -1) + { + allScripts.push(scriptName); + } } - // second add all other scripts from script list + // add all other scripts of this kind from script list for (var i=0; i < availableScripts.length; i++) { var scriptName = availableScripts[i]; - if (scriptList.indexOf(scriptName) == -1) + if (scriptList.indexOf(scriptName) === -1) { scriptList.push(scriptName); } } + // add all other scripts of other kinds from script list + for (var i=0; i < availableAllScripts.length; i++) + { + var scriptName = availableAllScripts[i]; + if (allScripts.indexOf(scriptName) === -1) + { + allScripts.push(scriptName); + } + } + return scriptList; } @@ -1740,7 +2013,26 @@ var ScriptListDialog = (new function($) { var scriptOrderOption = Config.findOptionByName('ScriptOrder'); var control = $('#' + scriptOrderOption.formId); - control.val(scriptList.join(', ')); + + // preserving order of scripts of other kinds which were not visible in the dialog + var orderList = []; + for (var i=0; i < allScripts.length; i++) + { + var scriptName = allScripts[i]; + if (orderList.indexOf(scriptName) === -1) + { + if (scriptList.indexOf(scriptName) > -1) + { + orderList = orderList.concat(scriptList); + } + else + { + orderList.push(scriptName); + } + } + } + + control.val(orderList.join(', ')); } $ScriptListDialog.modal('hide'); @@ -1996,7 +2288,7 @@ var ConfigBackupRestore = (new function($) removeValue(option.name); addValue(option.name); } - else if (!option.template && option.multiid === 1) + else if (option.template) { // delete all multi-options for (var j=1; ; j++) @@ -2018,7 +2310,7 @@ var ConfigBackupRestore = (new function($) } } } - + for (var k=0; k < config.length; k++) { var conf = config[k]; @@ -2134,3 +2426,332 @@ var RestoreSettingsDialog = (new function($) } }(jQuery)); + + +/*** UPDATE DIALOG *******************************************************/ + +var UpdateDialog = (new function($) +{ + 'use strict' + + // Controls + var $UpdateDialog; + var $UpdateProgressDialog; + var $UpdateProgressDialog_Log; + + // State + var VersionInfo; + var PackageInfo; + var UpdateInfo; + var lastUpTimeSec; + var installing = false; + + this.init = function() + { + $UpdateDialog = $('#UpdateDialog'); + $('#UpdateDialog_InstallStable,#UpdateDialog_InstallTesting,#UpdateDialog_InstallDevel').click(install); + $UpdateProgressDialog = $('#UpdateProgressDialog'); + $UpdateProgressDialog_Log = $('#UpdateProgressDialog_Log'); + + $UpdateDialog.on('hidden', resumeRefresher); + $UpdateProgressDialog.on('hidden', resumeRefresher); + } + + function resumeRefresher() + { + if (!installing) + { + Refresher.resume(); + } + } + + this.showModal = function() + { + $('#UpdateDialog_Install').hide(); + $('#UpdateDialog_CheckProgress').show(); + $('#UpdateDialog_CheckFailed').hide(); + $('#UpdateDialog_Versions').hide(); + $('#UpdateDialog_UpdateAvail').hide(); + $('#UpdateDialog_UpdateNotAvail').hide(); + $('#UpdateDialog_UpdateNoInfo').hide(); + $('#UpdateDialog_InstalledInfo').show(); + + $('#UpdateDialog_VerInstalled').text(Options.option('Version')); + + PackageInfo = {}; + VersionInfo = {}; + UpdateInfo = {}; + + installing = false; + Refresher.pause(); + + $UpdateDialog.modal({backdrop: 'static'}); + + RPC.call('readurl', ['http://nzbget.net/info/nzbget-version.php?nocache=' + new Date().getTime(), 'version info'], loadedUpstreamInfo, error); + } + + function error(e) + { + $('#UpdateDialog_CheckProgress').hide(); + $('#UpdateDialog_CheckFailed').show(); + } + + function parseJsonP(jsonp) + { + var p = jsonp.indexOf('{'); + var obj = JSON.parse(jsonp.substr(p, 10000)); + return obj; + } + + function loadedUpstreamInfo(data) + { + VersionInfo = parseJsonP(data); + if (VersionInfo['devel-version']) + { + loadPackageInfo(); + } + else + { + loadSvnVerData(); + } + } + + function loadSvnVerData() + { + // fetching devel version number from svn viewer + RPC.call('readurl', ['http://svn.code.sf.net/p/nzbget/code/trunk/', 'svn revision info'], + function(svnRevData) + { + RPC.call('readurl', ['http://svn.code.sf.net/p/nzbget/code/trunk/configure.ac', 'svn branch info'], + function(svnBranchData) + { + var rev = svnRevData.match(/.*Revision (\d+).*/); + if (rev.length > 1) + { + var ver = svnBranchData.match(/.*AM_INIT_AUTOMAKE\(nzbget, (.*)\).*/); + if (ver.length > 1) + { + VersionInfo['devel-version'] = ver[1] + '-r' + rev[1]; + } + } + + loadPackageInfo(); + }, error); + }, error); + } + + function loadPackageInfo() + { + $.get('package-info.json', loadedPackageInfo, 'html').fail(loadedAll); + } + + function loadedPackageInfo(data) + { + PackageInfo = parseJsonP(data); + if (PackageInfo['update-info-link']) + { + RPC.call('readurl', [PackageInfo['update-info-link'], 'update info'], loadedUpdateInfo, loadedAll); + } + else if (PackageInfo['update-info-script']) + { + RPC.call('checkupdates', [], loadedUpdateInfo, loadedAll); + } + else + { + loadedAll(); + } + } + + function loadedUpdateInfo(data) + { + UpdateInfo = parseJsonP(data); + loadedAll(); + } + + function formatTesting(str) + { + return str.replace('-testing-', '-'); + } + + function revision(version) + { + var rev = version.match(/.*r(\d+)/); + return rev && rev.length > 1 ? parseInt(rev[1]) : 0; + } + + function vernumber(version) + { + var ver = version.match(/([\d.]+).*/); + return ver && ver.length > 1 ? parseFloat(ver[1]) : 0; + } + + function loadedAll() + { + var installedVersion = Options.option('Version'); + + $('#UpdateDialog_CheckProgress').hide(); + $('#UpdateDialog_Versions').show(); + $('#UpdateDialog_InstalledInfo').show(); + + $('#UpdateDialog_CurStable').text(VersionInfo['stable-version'] ? VersionInfo['stable-version'] : 'no data'); + $('#UpdateDialog_CurTesting').text(VersionInfo['testing-version'] ? formatTesting(VersionInfo['testing-version']) : 'no data'); + $('#UpdateDialog_CurDevel').text(VersionInfo['devel-version'] ? formatTesting(VersionInfo['devel-version']) : 'no data'); + + $('#UpdateDialog_CurNotesStable').attr('href', VersionInfo['stable-release-notes']); + $('#UpdateDialog_CurNotesTesting').attr('href', VersionInfo['testing-release-notes']); + $('#UpdateDialog_CurNotesDevel').attr('href', VersionInfo['devel-release-notes']); + Util.show('#UpdateDialog_CurNotesStable', VersionInfo['stable-release-notes']); + Util.show('#UpdateDialog_CurNotesTesting', VersionInfo['testing-release-notes']); + Util.show('#UpdateDialog_CurNotesDevel', VersionInfo['devel-release-notes']); + + $('#UpdateDialog_AvailStable').text(UpdateInfo['stable-version'] ? UpdateInfo['stable-version'] : 'not available'); + $('#UpdateDialog_AvailTesting').text(UpdateInfo['testing-version'] ? formatTesting(UpdateInfo['testing-version']) : 'not available'); + $('#UpdateDialog_AvailDevel').text(UpdateInfo['devel-version'] ? formatTesting(UpdateInfo['devel-version']) : 'not available'); + + $('#UpdateDialog_AvailNotesStable').attr('href', UpdateInfo['stable-package-info']); + $('#UpdateDialog_AvailNotesTesting').attr('href', UpdateInfo['testing-package-info']); + $('#UpdateDialog_AvailNotesDevel').attr('href', UpdateInfo['devel-package-info']); + Util.show('#UpdateDialog_AvailNotesStableBlock', UpdateInfo['stable-package-info']); + Util.show('#UpdateDialog_AvailNotesTestingBlock', UpdateInfo['testing-package-info']); + Util.show('#UpdateDialog_AvailNotesDevelBlock', UpdateInfo['devel-package-info']); + + var installedRev = revision(installedVersion); + var installedVer = vernumber(installedVersion); + var installedStable = installedRev === 0 && installedVersion.indexOf('testing') === -1; + + var canInstallStable = UpdateInfo['stable-version'] && + ((installedStable && installedVer < vernumber(UpdateInfo['stable-version'])) || + (!installedStable && installedVer <= vernumber(UpdateInfo['stable-version']))); + var canInstallTesting = UpdateInfo['testing-version'] && + ((installedStable && installedVer < vernumber(UpdateInfo['testing-version'])) || + (!installedStable && (installedRev === 0 || installedRev < revision(UpdateInfo['testing-version'])))); + var canInstallDevel = UpdateInfo['devel-version'] && + ((installedStable && installedVer < vernumber(UpdateInfo['devel-version'])) || + (!installedStable && (installedRev === 0 || installedRev < revision(UpdateInfo['devel-version'])))); + Util.show('#UpdateDialog_InstallStable', canInstallStable); + Util.show('#UpdateDialog_InstallTesting', canInstallTesting); + Util.show('#UpdateDialog_InstallDevel', canInstallDevel); + + var hasUpdateSource = PackageInfo['update-info-link'] || PackageInfo['update-info-script']; + var hasUpdateInfo = UpdateInfo['stable-version'] || UpdateInfo['testing-version'] || UpdateInfo['devel-version']; + var canUpdate = canInstallStable || canInstallTesting || canInstallDevel; + Util.show('#UpdateDialog_UpdateAvail', canUpdate); + Util.show('#UpdateDialog_UpdateNotAvail', hasUpdateInfo && !canUpdate); + Util.show('#UpdateDialog_UpdateNoInfo', !hasUpdateSource); + Util.show('#UpdateDialog_CheckFailed', hasUpdateSource && !hasUpdateInfo); + $('#UpdateDialog_AvailRow').toggleClass('hide', !hasUpdateInfo); + } + + function install(e) + { + e.preventDefault(); + var kind = $(this).attr('data-kind'); + var script = PackageInfo['install-script']; + var info = PackageInfo['install-' + kind + '-info']; + + if (!script) + { + alert('Something is wrong with a package configuration file "package-info.json".'); + return; + } + + RPC.call('status', [], function(status) + { + lastUpTimeSec = status.UpTimeSec; + RPC.call('startupdate', [kind], updateStarted); + }); + } + + function updateStarted(started) + { + if (!started) + { + Notification.show('#Notif_StartUpdate_Failed'); + return; + } + + installing = true; + $UpdateDialog.fadeOut(250, function() + { + $UpdateProgressDialog_Log.text(''); + $UpdateProgressDialog.fadeIn(250, function() + { + $UpdateDialog.modal('hide'); + $UpdateProgressDialog.modal({backdrop: 'static'}); + updateLog(); + }); + }); + } + + function updateLog() + { + RPC.call('logupdate', [0, 100], function(data) + { + updateLogTable(data); + setTimeout(updateLog, 500); + }, + function() + { + // rpc-failure: the program has been terminated. Waiting for new instance. + setLogContentAndScroll($UpdateProgressDialog_Log.html() + '\n' + 'NZBGet has been terminated. Waiting for restart...'); + setTimeout(checkStatus, 500); + }, + 1000); + } + + function setLogContentAndScroll(html) + { + var scroll = $UpdateProgressDialog_Log.prop('scrollHeight') - $UpdateProgressDialog_Log.prop('scrollTop') === $UpdateProgressDialog_Log.prop('clientHeight'); + $UpdateProgressDialog_Log.html(html); + if (scroll) + { + $UpdateProgressDialog_Log.scrollTop($UpdateProgressDialog_Log.prop('scrollHeight')); + } + } + + function updateLogTable(messages) + { + var html = ''; + for (var i=0; i < messages.length; i++) + { + var message = messages[i]; + var text = Util.textToHtml(message.Text); + if (message.Kind === 'ERROR') + { + text = '' + text + ''; + } + html = html + text + '\n'; + } + setLogContentAndScroll(html); + } + + function checkStatus() + { + RPC.call('status', [], function(status) + { + // OK, checking if it is a restarted instance + if (status.UpTimeSec >= lastUpTimeSec) + { + // the old instance is not restarted yet + // waiting 0.5 sec. and retrying + setTimeout(checkStatus, 500); + } + else + { + // restarted successfully, refresh page + setLogContentAndScroll($UpdateProgressDialog_Log.html() + '\n' + 'Successfully started. Refreshing the page...'); + setTimeout(function() + { + document.location.reload(true); + }, 1000); + } + }, + function() + { + // Failure, waiting 0.5 sec. and retrying + setTimeout(checkStatus, 500); + }, + 1000); + } + +}(jQuery)); diff --git a/content/usr/local/share/nzbget/webui/downloads.js b/content/usr/local/share/nzbget/webui/downloads.js index 1db00c5..6a1db78 100644 --- a/content/usr/local/share/nzbget/webui/downloads.js +++ b/content/usr/local/share/nzbget/webui/downloads.js @@ -1,7 +1,7 @@ /* * This file is part of nzbget * - * Copyright (C) 2012-2013 Andrey Prygunkov + * Copyright (C) 2012-2014 Andrey Prygunkov * * 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 @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * $Revision: 671 $ - * $Date: 2013-05-08 23:50:47 +0200 (Wed, 08 May 2013) $ + * $Revision: 1112 $ + * $Date: 2014-08-28 22:51:29 +0200 (Thu, 28 Aug 2014) $ * */ @@ -40,12 +40,33 @@ var Downloads = (new function($) var $DownloadsTabBadgeEmpty; var $DownloadQueueEmpty; var $DownloadsRecordsPerPage; + var $DownloadsTable_Name; // State var notification = null; var updateTabInfo; var groups; var urls; + var nameColumnWidth = null; + var minLevel = null; + + var statusData = { + 'QUEUED': { Text: 'QUEUED', PostProcess: false }, + 'FETCHING': { Text: 'FETCHING', PostProcess: false }, + 'DOWNLOADING': { Text: 'DOWNLOADING', PostProcess: false }, + 'PP_QUEUED': { Text: 'PP-QUEUED', PostProcess: true }, + 'PAUSED': { Text: 'PAUSED', PostProcess: false }, + 'LOADING_PARS': { Text: 'CHECKING', PostProcess: true }, + 'VERIFYING_SOURCES': { Text: 'CHECKING', PostProcess: true }, + 'REPAIRING': { Text: 'REPAIRING', PostProcess: true }, + 'VERIFYING_REPAIRED': { Text: 'VERIFYING', PostProcess: true }, + 'RENAMING': { Text: 'RENAMING', PostProcess: true }, + 'MOVING': { Text: 'MOVING', PostProcess: true }, + 'UNPACKING': { Text: 'UNPACKING', PostProcess: true }, + 'EXECUTING_SCRIPT': { Text: 'PROCESSING', PostProcess: true }, + 'PP_FINISHED': { Text: 'FINISHED', PostProcess: false } + }; + this.statusData = statusData; this.init = function(options) { @@ -56,6 +77,7 @@ var Downloads = (new function($) $DownloadsTabBadgeEmpty = $('#DownloadsTabBadgeEmpty'); $DownloadQueueEmpty = $('#DownloadQueueEmpty'); $DownloadsRecordsPerPage = $('#DownloadsRecordsPerPage'); + $DownloadsTable_Name = $('#DownloadsTable_Name'); var recordsPerPage = UISettings.read('$DownloadsRecordsPerPage', 10); $DownloadsRecordsPerPage.val(recordsPerPage); @@ -93,88 +115,27 @@ var Downloads = (new function($) this.update = function() { - RPC.call('listgroups', [], groups_loaded); + if (!groups) + { + $('#DownloadsTable_Category').css('width', DownloadsUI.calcCategoryColumnWidth()); + } + + RPC.call('listgroups', [100], groups_loaded); } function groups_loaded(_groups) { groups = _groups; - RPC.call('postqueue', [100], posts_loaded); - } - - function posts_loaded(posts) - { - mergequeues(posts); prepare(); - RPC.call('urlqueue', [], urls_loaded); - } - - function urls_loaded(_urls) - { - urls = _urls; RPC.next(); } - function mergequeues(posts) - { - var lastPPItemIndex = -1; - for (var i=0, il=posts.length; i < il; i++) - { - var post = posts[i]; - var found = false; - for (var j=0, jl=groups.length; j < jl; j++) - { - var group = groups[j]; - if (group.NZBID === post.NZBID) - { - found = true; - if (!group.post) - { - group.post = post; - } - lastPPItemIndex = j; - break; - } - } - - if (!found) - { - // create a virtual group-item - var group = {post: post}; - group.NZBID = post.NZBID; - group.NZBName = post.NZBName; - group.MaxPriority = 0; - group.Category = ''; - group.LastID = 0; - group.MinPostTime = 0; - group.FileSizeMB = 0; - group.FileSizeLo = 0; - group.RemainingSizeMB = 0; - group.RemainingSizeLo = 0; - group.PausedSizeMB = 0; - group.PausedSizeLo = 0; - group.FileCount = 0; - group.RemainingFileCount = 0; - group.RemainingParCount = 0; - - // insert it after the last pp-item - if (lastPPItemIndex > -1) - { - groups.splice(lastPPItemIndex + 1, 0, group); - } - else - { - groups.unshift(group); - } - } - } - } - function prepare() { for (var j=0, jl=groups.length; j < jl; j++) { - detectStatus(groups[j]); + var group = groups[j]; + group.postprocess = statusData[group.Status].PostProcess; } } @@ -187,6 +148,11 @@ var Downloads = (new function($) Util.show($DownloadQueueEmpty, groups.length === 0); } + this.resize = function() + { + calcProgressLabels(); + } + /*** TABLE *************************************************************************/ function redraw_table() @@ -198,18 +164,20 @@ var Downloads = (new function($) var group = groups[i]; var nametext = group.NZBName; + var statustext = DownloadsUI.buildStatusText(group); var priority = DownloadsUI.buildPriorityText(group.MaxPriority); var estimated = DownloadsUI.buildEstimated(group); var age = Util.formatAge(group.MinPostTime + UISettings.timeZoneCorrection*60*60); var size = Util.formatSizeMB(group.FileSizeMB, group.FileSizeLo); var remaining = Util.formatSizeMB(group.RemainingSizeMB-group.PausedSizeMB, group.RemainingSizeLo-group.PausedSizeLo); - + var dupe = DownloadsUI.buildDupeText(group.DupeKey, group.DupeScore, group.DupeMode); + var item = { id: group.NZBID, group: group, data: { age: age, estimated: estimated, size: size, remaining: remaining }, - search: group.status + ' ' + nametext + ' ' + priority + ' ' + group.Category + ' ' + age + ' ' + size + ' ' + remaining + ' ' + estimated + search: statustext + ' ' + nametext + ' ' + priority + ' ' + dupe + ' ' + group.Category + ' ' + age + ' ' + size + ' ' + remaining + ' ' + estimated }; data.push(item); @@ -224,21 +192,53 @@ var Downloads = (new function($) var status = DownloadsUI.buildStatus(group); var priority = DownloadsUI.buildPriority(group.MaxPriority); - var progresslabel = DownloadsUI.buildProgressLabel(group); + var progresslabel = DownloadsUI.buildProgressLabel(group, nameColumnWidth); var progress = DownloadsUI.buildProgress(group, item.data.size, item.data.remaining, item.data.estimated); + var dupe = DownloadsUI.buildDupe(group.DupeKey, group.DupeScore, group.DupeMode); + + var age = new Date().getTime() / 1000 - (group.MinPostTime + UISettings.timeZoneCorrection*60*60); + var propagation = ''; + if (group.ActiveDownloads == 0 && age < parseInt(Options.option('PropagationDelay')) * 60) + { + propagation = 'delayed '; + } - var name = '' + Util.textToHtml(Util.formatNZBName(group.NZBName)) + ''; + var name = '' + Util.textToHtml(Util.formatNZBName(group.NZBName)) + ''; + + var url = ''; + if (group.Kind === 'URL') + { + url = 'URL '; + } + + var health = ''; + if (group.Health < 1000 && (!group.postprocess || + (group.Status === 'PP_QUEUED' && group.PostTotalTimeSec === 0))) + { + health = ' health: ' + Math.floor(group.Health / 10) + '% '; + } + + var backup = ''; + var backupPercent = calcBackupPercent(group); + if (backupPercent > 0) + { + backup = ' backup: ' + + (backupPercent < 10 ? Util.round1(backupPercent) : Util.round0(backupPercent)) + '% '; + } + var category = Util.textToHtml(group.Category); if (!UISettings.miniTheme) { - var info = name + ' ' + priority + progresslabel; + var info = name + ' ' + url + priority + dupe + health + backup + propagation + progresslabel; item.fields = ['
    ', status, info, category, item.data.age, progress, item.data.estimated]; } else { - var info = '
    ' + name + '' + - ' ' + (group.status === 'queued' ? '' : status) + ' ' + priority; + var info = '
    ' + name + '' + url + + ' ' + (group.Status === 'QUEUED' ? '' : status) + ' ' + priority + dupe + health + backup + propagation; if (category) { info += ' ' + category + ''; @@ -259,40 +259,45 @@ var Downloads = (new function($) cell.className = 'text-right'; } } - - function detectStatus(group) + + function calcBackupPercent(group) { - group.paused = (group.PausedSizeLo != 0) && (group.RemainingSizeLo == group.PausedSizeLo); - group.postprocess = group.post !== undefined; - if (group.postprocess) + var downloadedArticles = group.SuccessArticles + group.FailedArticles; + if (downloadedArticles === 0) { - switch (group.post.Stage) - { - case 'QUEUED': group.status = 'pp-queued'; break; - case 'LOADING_PARS': group.status = 'checking'; break; - case 'VERIFYING_SOURCES': group.status = 'checking'; break; - case 'REPAIRING': group.status = 'repairing'; break; - case 'VERIFYING_REPAIRED': group.status = 'verifying'; break; - case 'RENAMING': group.status = 'renaming'; break; - case 'MOVING': group.status = 'moving'; break; - case 'UNPACKING': group.status = 'unpacking'; break; - case 'EXECUTING_SCRIPT': group.status = 'processing'; break; - case 'FINISHED': group.status = 'finished'; break; - default: group.status = 'error: ' + group.post.Stage; break; - } + return 0; } - else if (group.ActiveDownloads > 0) + + if (minLevel === null) { - group.status = 'downloading'; + for (var i=0; i < Status.status.NewsServers.length; i++) + { + var server = Status.status.NewsServers[i]; + var level = parseInt(Options.option('Server' + server.ID + '.Level')); + if (minLevel === null || minLevel > level) + { + minLevel = level; + } + } } - else if (group.paused) + + var backupArticles = 0; + for (var j=0; j < group.ServerStats.length; j++) { - group.status = 'paused'; + var stat = group.ServerStats[j]; + var level = parseInt(Options.option('Server' + stat.ServerID + '.Level')); + if (level > minLevel && stat.SuccessArticles > 0) + { + backupArticles += stat.SuccessArticles; + } } - else + + var backupPercent = 0; + if (backupArticles > 0) { - group.status = 'queued'; + backupPercent = backupArticles * 100.0 / downloadedArticles; } + return backupPercent; } this.recordsPerPageChange = function() @@ -307,13 +312,32 @@ var Downloads = (new function($) updateTabInfo($DownloadsTabBadge, stat); } + function calcProgressLabels() + { + var progressLabels = $('.label-inline', $DownloadsTable); + + if (UISettings.miniTheme) + { + nameColumnWidth = null; + progressLabels.css('max-width', ''); + return; + } + + progressLabels.hide(); + nameColumnWidth = Math.max($DownloadsTable_Name.width(), 50) - 4*2; // 4 - padding of span + progressLabels.css('max-width', nameColumnWidth); + progressLabels.show(); + } + /*** EDIT ******************************************************/ - function itemClick() + function itemClick(e) { - var nzbid = $(this).attr('nzbid'); + e.preventDefault(); + var nzbid = $(this).attr('data-nzbid'); + var area = $(this).attr('data-area'); $(this).blur(); - DownloadsEditDialog.showModal(nzbid, groups); + DownloadsEditDialog.showModal(nzbid, groups, area); } function editCompleted() @@ -328,7 +352,7 @@ var Downloads = (new function($) /*** CHECKMARKS ******************************************************/ - function checkBuildEditIDList(UseLastID) + function checkBuildEditIDList(allowPostProcess, allowUrl) { var checkedRows = $DownloadsTable.fasttable('checkedRows'); @@ -339,13 +363,18 @@ var Downloads = (new function($) var group = groups[i]; if (checkedRows.indexOf(group.NZBID) > -1) { - if (group.postprocess) + if (group.postprocess && !allowPostProcess) { Notification.show('#Notif_Downloads_CheckPostProcess'); return null; } + if (group.Kind === 'URL' && !allowUrl) + { + Notification.show('#Notif_Downloads_CheckURL'); + return null; + } - checkedEditIDs.push(UseLastID ? group.LastID : group.NZBID); + checkedEditIDs.push(group.NZBID); } } @@ -362,7 +391,7 @@ var Downloads = (new function($) this.editClick = function() { - var checkedEditIDs = checkBuildEditIDList(false); + var checkedEditIDs = checkBuildEditIDList(false, true); if (!checkedEditIDs) { return; @@ -380,7 +409,7 @@ var Downloads = (new function($) this.mergeClick = function() { - var checkedEditIDs = checkBuildEditIDList(false); + var checkedEditIDs = checkBuildEditIDList(false, false); if (!checkedEditIDs) { return; @@ -397,7 +426,7 @@ var Downloads = (new function($) this.pauseClick = function() { - var checkedEditIDs = checkBuildEditIDList(true); + var checkedEditIDs = checkBuildEditIDList(false, false); if (!checkedEditIDs) { return; @@ -408,7 +437,7 @@ var Downloads = (new function($) this.resumeClick = function() { - var checkedEditIDs = checkBuildEditIDList(true); + var checkedEditIDs = checkBuildEditIDList(false, false); if (!checkedEditIDs) { return; @@ -432,6 +461,8 @@ var Downloads = (new function($) var checkedRows = $DownloadsTable.fasttable('checkedRows'); var downloadIDs = []; var postprocessIDs = []; + var hasNzb = false; + var hasUrl = false; for (var i = 0; i < groups.length; i++) { var group = groups[i]; @@ -439,12 +470,11 @@ var Downloads = (new function($) { if (group.postprocess) { - postprocessIDs.push(group.post.ID); - } - if (group.LastID > 0) - { - downloadIDs.push(group.LastID); + postprocessIDs.push(group.NZBID); } + downloadIDs.push(group.NZBID); + hasNzb = hasNzb || group.Kind === 'NZB'; + hasUrl = hasUrl || group.Kind === 'URL'; } } @@ -468,11 +498,11 @@ var Downloads = (new function($) } }; - var deleteGroups = function() + var deleteGroups = function(command) { if (downloadIDs.length > 0) { - RPC.call('editqueue', ['GroupDelete', 0, '', downloadIDs], deletePosts); + RPC.call('editqueue', [command, 0, '', downloadIDs], deletePosts); } else { @@ -480,15 +510,12 @@ var Downloads = (new function($) } }; - Util.show('#DownloadsDeleteConfirmDialog_Cleanup', Options.option('DeleteCleanupDisk') === 'yes'); - Util.show('#DownloadsDeleteConfirmDialog_Remain', Options.option('DeleteCleanupDisk') != 'yes'); - - ConfirmDialog.showModal('DownloadsDeleteConfirmDialog', deleteGroups); + DownloadsUI.deleteConfirm(deleteGroups, true, hasNzb, hasUrl); } this.moveClick = function(action) { - var checkedEditIDs = checkBuildEditIDList(true); + var checkedEditIDs = checkBuildEditIDList(true, true); if (!checkedEditIDs) { return; @@ -528,14 +555,19 @@ var DownloadsUI = (new function($) { 'use strict'; + // State + var categoryColumnWidth = null; + var dupeCheck = null; + this.fillPriorityCombo = function(combo) { combo.empty(); - combo.append(''); - combo.append(''); - combo.append(''); - combo.append(''); + combo.append(''); combo.append(''); + combo.append(''); + combo.append(''); + combo.append(''); + combo.append(''); } this.fillCategoryCombo = function(combo) @@ -549,36 +581,51 @@ var DownloadsUI = (new function($) } } + this.buildStatusText = function(group) + { + var statusText = Downloads.statusData[group.Status].Text; + if (statusText === undefined) + { + statusText = 'Internal error(' + group.Status + ')'; + } + return statusText; + } + this.buildStatus = function(group) { - if (group.postprocess && group.status !== 'pp-queued') + var statusText = Downloads.statusData[group.Status].Text; + var badgeClass = ''; + + if (group.postprocess && group.Status !== 'PP_QUEUED') { - if (Status.status.PostPaused) - { - return '' + group.status + ''; - } - else - { - return '' + group.status + ''; - } + badgeClass = Status.status.PostPaused && group.MinPriority < 900 ? 'label-warning' : 'label-success'; } - switch (group.status) + else if (group.Status === 'DOWNLOADING' || group.Status === 'FETCHING') { - case 'pp-queued': return 'pp-queued'; - case 'downloading': return 'downloading'; - case 'paused': return 'paused'; - case 'queued': return 'queued'; - default: return 'internal error(' + group.status + ')'; + badgeClass = 'label-success'; } + else if (group.Status === 'PAUSED') + { + badgeClass = 'label-warning'; + } + else if (statusText === undefined) + { + statusText = 'INTERNAL_ERROR (' + group.Status + ')'; + badgeClass = 'label-important'; + } + + return '' + statusText + ''; } this.buildProgress = function(group, totalsize, remaining, estimated) { - if (group.status === 'downloading' || (group.postprocess && !Status.status.PostPaused)) + if (group.Status === 'DOWNLOADING' || + (group.postprocess && !(Status.status.PostPaused && group.MinPriority < 900))) { var kind = 'progress-success'; } - else if (group.status === 'paused' || (group.postprocess && Status.status.PostPaused)) + else if (group.Status === 'PAUSED' || + (group.postprocess && !(Status.status.PostPaused && group.MinPriority < 900))) { var kind = 'progress-warning'; } @@ -596,7 +643,13 @@ var DownloadsUI = (new function($) { totalsize = ''; remaining = ''; - percent = Math.round(group.post.StageProgress / 10); + percent = Math.round(group.PostStageProgress / 10); + } + + if (group.Kind === 'URL') + { + totalsize = ''; + remaining = ''; } if (!UISettings.miniTheme) @@ -630,12 +683,12 @@ var DownloadsUI = (new function($) { if (group.postprocess) { - if (group.post.StageProgress > 0) + if (group.PostStageProgress > 0) { - return Util.formatTimeLeft(group.post.StageTimeSec / group.post.StageProgress * (1000 - group.post.StageProgress)); + return Util.formatTimeLeft(group.PostStageTimeSec / group.PostStageProgress * (1000 - group.PostStageProgress)); } } - else if (!group.paused && Status.status.DownloadRate > 0) + else if (group.Status !== 'PAUSED' && Status.status.DownloadRate > 0) { return Util.formatTimeLeft((group.RemainingSizeMB-group.PausedSizeMB)*1024/(Status.status.DownloadRate/1024)); } @@ -643,12 +696,12 @@ var DownloadsUI = (new function($) return ''; } - this.buildProgressLabel = function(group) + this.buildProgressLabel = function(group, maxWidth) { var text = ''; - if (group.postprocess && !Status.status.PostPaused) + if (group.postprocess && !(Status.status.PostPaused && group.MinPriority < 900)) { - switch (group.post.Stage) + switch (group.Status) { case "REPAIRING": break; @@ -657,24 +710,25 @@ var DownloadsUI = (new function($) case "VERIFYING_REPAIRED": case "UNPACKING": case "RENAMING": - text = group.post.ProgressLabel; + text = group.PostInfoText; break; case "EXECUTING_SCRIPT": - if (group.post.Log && group.post.Log.length > 0) + if (group.Log && group.Log.length > 0) { - text = group.post.Log[group.post.Log.length-1].Text; + text = group.Log[group.Log.length-1].Text; // remove "for " from label text text = text.replace(' for ' + group.NZBName, ' '); } else { - text = group.post.ProgressLabel; + text = group.PostInfoText; } break; } } - return text !== '' ? ' ' + text + '' : ''; + return text !== '' ? ' ' + text + '' : ''; } this.buildPriorityText = function(priority) @@ -682,6 +736,7 @@ var DownloadsUI = (new function($) switch (priority) { case 0: return ''; + case 900: return 'force priority'; case 100: return 'very high priority'; case 50: return 'high priority'; case -50: return 'low priority'; @@ -695,6 +750,7 @@ var DownloadsUI = (new function($) switch (priority) { case 0: return ''; + case 900: return ' force priority'; case 100: return ' very high priority'; case 50: return ' high priority'; case -50: return ' low priority'; @@ -709,4 +765,129 @@ var DownloadsUI = (new function($) return ' priority: ' + priority + ''; } } + + function formatDupeText(dupeKey, dupeScore, dupeMode) + { + dupeKey = dupeKey.replace('rageid=', ''); + dupeKey = dupeKey.replace('imdb=', ''); + dupeKey = dupeKey.replace('series=', ''); + dupeKey = dupeKey.replace('nzb=', '#'); + dupeKey = dupeKey.replace('=', ' '); + dupeKey = dupeKey === '' ? 'title' : dupeKey; + return dupeKey; + } + + this.buildDupeText = function(dupeKey, dupeScore, dupeMode) + { + if (dupeCheck == null) + { + dupeCheck = Options.option('DupeCheck') === 'yes'; + } + + if (dupeCheck && dupeKey != '' && UISettings.dupeBadges) + { + return formatDupeText(dupeKey, dupeScore, dupeMode); + } + else + { + return ''; + } + } + + this.buildDupe = function(dupeKey, dupeScore, dupeMode) + { + if (dupeCheck == null) + { + dupeCheck = Options.option('DupeCheck') === 'yes'; + } + + if (dupeCheck && dupeKey != '' && UISettings.dupeBadges) + { + return ' ' + formatDupeText(dupeKey, dupeScore, dupeMode) + ' '; + } + else + { + return ''; + } + } + + this.resetCategoryColumnWidth = function() + { + categoryColumnWidth = null; + } + + this.calcCategoryColumnWidth = function() + { + if (categoryColumnWidth === null) + { + var widthHelper = $('
    ').css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden'}).appendTo($('body')); + + // default (min) width + categoryColumnWidth = 60; + + for (var i = 1; ; i++) + { + var opt = Options.option('Category' + i + '.Name'); + if (!opt) + { + break; + } + widthHelper.text(opt); + var catWidth = widthHelper.width(); + categoryColumnWidth = Math.max(categoryColumnWidth, catWidth); + } + + widthHelper.remove(); + + categoryColumnWidth += 'px'; + } + + return categoryColumnWidth; + } + + this.deleteConfirm = function(actionCallback, multi, hasNzb, hasUrl) + { + var dupeCheck = Options.option('DupeCheck') === 'yes'; + var cleanupDisk = Options.option('DeleteCleanupDisk') === 'yes'; + var history = Options.option('KeepHistory') !== '0'; + var dialog = null; + + function init(_dialog) + { + dialog = _dialog; + + if (!multi) + { + var html = $('#ConfirmDialog_Text').html(); + html = html.replace(/downloads/g, 'download'); + $('#ConfirmDialog_Text').html(html); + } + + $('#DownloadsDeleteConfirmDialog_Delete', dialog).prop('checked', true); + $('#DownloadsDeleteConfirmDialog_Delete', dialog).prop('checked', true); + $('#DownloadsDeleteConfirmDialog_DeleteDupe', dialog).prop('checked', false); + $('#DownloadsDeleteConfirmDialog_DeleteFinal', dialog).prop('checked', false); + Util.show($('#DownloadsDeleteConfirmDialog_Options', dialog), history); + Util.show($('#DownloadsDeleteConfirmDialog_Simple', dialog), !history); + Util.show($('#DownloadsDeleteConfirmDialog_DeleteDupe,#DownloadsDeleteConfirmDialog_DeleteDupeLabel', dialog), dupeCheck && hasNzb); + Util.show($('#DownloadsDeleteConfirmDialog_Remain', dialog), !cleanupDisk && hasNzb); + Util.show($('#DownloadsDeleteConfirmDialog_Cleanup', dialog), cleanupDisk && hasNzb); + Util.show('#ConfirmDialog_Help', history && dupeCheck && hasNzb); + }; + + function action() + { + var deleteNormal = $('#DownloadsDeleteConfirmDialog_Delete', dialog).is(':checked'); + var deleteDupe = $('#DownloadsDeleteConfirmDialog_DeleteDupe', dialog).is(':checked'); + var deleteFinal = $('#DownloadsDeleteConfirmDialog_DeleteFinal', dialog).is(':checked'); + var command = deleteNormal ? 'GroupDelete' : (deleteDupe ? 'GroupDupeDelete' : 'GroupFinalDelete'); + actionCallback(command); + } + + ConfirmDialog.showModal('DownloadsDeleteConfirmDialog', action, init); + } }(jQuery)); diff --git a/content/usr/local/share/nzbget/webui/edit.js b/content/usr/local/share/nzbget/webui/edit.js index 310b49d..ec6dab4 100644 --- a/content/usr/local/share/nzbget/webui/edit.js +++ b/content/usr/local/share/nzbget/webui/edit.js @@ -1,7 +1,7 @@ /* * This file is part of nzbget * - * Copyright (C) 2012-2013 Andrey Prygunkov + * Copyright (C) 2012-2014 Andrey Prygunkov * * 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 @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * $Revision: 683 $ - * $Date: 2013-05-16 22:54:13 +0200 (Thu, 16 May 2013) $ + * $Revision: 1108 $ + * $Date: 2014-08-28 21:15:42 +0200 (Thu, 28 Aug 2014) $ * */ @@ -30,9 +30,9 @@ * 4) Download split dialog; * 5) History edit dialog. */ - + /*** DOWNLOAD EDIT DIALOG ************************************************************/ - + var DownloadsEditDialog = (new function($) { 'use strict'; @@ -42,7 +42,8 @@ var DownloadsEditDialog = (new function($) var $DownloadsLogTable; var $DownloadsFileTable; var $DownloadsEdit_ParamData; - + var $ServStatsTable; + // State var curGroup; var notification = null; @@ -51,6 +52,9 @@ var DownloadsEditDialog = (new function($) var lastFullscreen; var logFilled; var files; + var refreshTimer = 0; + var showing; + var oldCategory; this.init = function() { @@ -62,8 +66,9 @@ var DownloadsEditDialog = (new function($) $('#DownloadsEdit_Resume').click(itemResume); $('#DownloadsEdit_Delete').click(itemDelete); $('#DownloadsEdit_CancelPP').click(itemCancelPP); - $('#DownloadsEdit_Param, #DownloadsEdit_Log, #DownloadsEdit_File').click(tabClick); + $('#DownloadsEdit_Param, #DownloadsEdit_Log, #DownloadsEdit_File, #DownloadsEdit_Dupe').click(tabClick); $('#DownloadsEdit_Back').click(backClick); + $('#DownloadsEdit_Category').change(categoryChange); $DownloadsLogTable = $('#DownloadsEdit_LogTable'); $DownloadsLogTable.fasttable( @@ -76,7 +81,7 @@ var DownloadsEditDialog = (new function($) hasHeader: true, renderCellCallback: logTableRenderCellCallback }); - + $DownloadsFileTable = $('#DownloadsEdit_FileTable'); $DownloadsFileTable.fasttable( { @@ -89,24 +94,36 @@ var DownloadsEditDialog = (new function($) renderCellCallback: fileTableRenderCellCallback }); + $ServStatsTable = $('#DownloadsEdit_ServStatsTable'); + $ServStatsTable.fasttable( + { + filterInput: '#DownloadsEdit_ServStatsTable_filter', + pagerContainer: '#DownloadsEdit_ServStatsTable_pager', + pageSize: 100, + maxPages: 3, + hasHeader: true, + renderCellCallback: EditUI.servStatsTableRenderCellCallback + }); + $DownloadsFileTable.on('click', 'tbody div.check', function(event) { $DownloadsFileTable.fasttable('itemCheckClick', this.parentNode.parentNode, event); }); $DownloadsFileTable.on('click', 'thead div.check', function() { $DownloadsFileTable.fasttable('titleCheckClick') }); $DownloadsFileTable.on('mousedown', Util.disableShiftMouseDown); - + $DownloadsEditDialog.on('hidden', function() { // cleanup $DownloadsLogTable.fasttable('update', []); $DownloadsFileTable.fasttable('update', []); $DownloadsEdit_ParamData.empty(); + clearTimeout(refreshTimer); // resume updates Refresher.resume(); }); TabDialog.extend($DownloadsEditDialog); - + if (UISettings.setFocus) { $DownloadsEditDialog.on('shown', function() @@ -119,7 +136,7 @@ var DownloadsEditDialog = (new function($) } } - this.showModal = function(nzbid, allGroups) + this.showModal = function(nzbid, allGroups, area) { var group = null; @@ -146,25 +163,43 @@ var DownloadsEditDialog = (new function($) var age = Util.formatAge(group.MinPostTime + UISettings.timeZoneCorrection*60*60); var size = Util.formatSizeMB(group.FileSizeMB, group.FileSizeLo); var remaining = Util.formatSizeMB(group.RemainingSizeMB-group.PausedSizeMB, group.RemainingSizeLo-group.PausedSizeLo); - var unpausedSize = Util.formatSizeMB(group.PausedSizeMB, group.PausedSizeLo); - var estimated = group.paused ? '' : (Status.status.DownloadRate > 0 ? Util.formatTimeHMS((group.RemainingSizeMB-group.PausedSizeMB)*1024/(Status.status.DownloadRate/1024)) : ''); + var pausedSize = Util.formatSizeMB(group.PausedSizeMB, group.PausedSizeLo); + var estimated = group.Status === 'PAUSED' ? '' : (Status.status.DownloadRate > 0 ? Util.formatTimeHMS((group.RemainingSizeMB-group.PausedSizeMB)*1024/(Status.status.DownloadRate/1024)) : ''); + var completion = group.SuccessArticles + group.FailedArticles > 0 ? Util.round0(group.SuccessArticles * 100.0 / (group.SuccessArticles + group.FailedArticles)) + '%' : '--'; + if (group.FailedArticles > 0 && completion === '100%') + { + completion = '99.9%'; + } var table = ''; - table += 'Age' + age + ''; + //table += 'Age' + age + ''; table += 'Total' + size + ''; - table += 'Paused' + unpausedSize + ''; + table += 'Paused' + pausedSize + ''; table += 'Unpaused' + remaining + ''; + //table += 'Size (total/remaining/paused)4.10 / 4.10 / 0.00 GB'; //table += 'Active downloads' + group.ActiveDownloads + ''; - table += 'Estimated time' + estimated + ''; - table += 'Files (total/remaining/pars)' + group.FileCount + ' / ' + + //table += 'Estimated time' + estimated + ''; + table += 'Health (critical/current)' + + Math.floor(group.CriticalHealth / 10) + '% / ' + Math.floor(group.Health / 10) + '%'; + table += 'Files (total/remaining/pars)' + group.FileCount + ' / ' + group.RemainingFileCount + ' / ' + group.RemainingParCount + ''; + table += '' + + (group.ServerStats.length > 0 ? '
    ' : '') + + 'Articles (total/completion)' + + (group.ServerStats.length > 0 ? ' ' : '') + + '' + group.TotalArticles + ' / ' + completion + ''; $('#DownloadsEdit_Statistics').html(table); - $('#DownloadsEdit_Title').text(Util.formatNZBName(group.NZBName)); - $('DownloadsEdit_Title').html($('#DownloadsEdit_Title').html() + ' ' + status); + $('#DownloadsEdit_ServStats').click(tabClick); + EditUI.fillServStats($ServStatsTable, group); + $ServStatsTable.fasttable('setCurPage', 1); + + $('#DownloadsEdit_Title').html(Util.formatNZBName(group.NZBName) + + (group.Kind === 'URL' ? ' URL' : '')); $('#DownloadsEdit_NZBName').attr('value', group.NZBName); $('#DownloadsEdit_NZBName').attr('readonly', group.postprocess); + $('#DownloadsEdit_URL').attr('value', group.URL); // Priority var v = $('#DownloadsEdit_Priority'); @@ -177,7 +212,7 @@ var DownloadsEditDialog = (new function($) v.attr('disabled', 'disabled'); // Category - var v = $('#DownloadsEdit_Category'); + v = $('#DownloadsEdit_Category'); DownloadsUI.fillCategoryCombo(v); v.val(group.Category); if (v.val() != group.Category) @@ -185,19 +220,30 @@ var DownloadsEditDialog = (new function($) v.append($('').text(group.Category)); } + // duplicate settings + $('#DownloadsEdit_DupeKey').val(group.DupeKey); + $('#DownloadsEdit_DupeScore').val(group.DupeScore); + $('#DownloadsEdit_DupeMode').val(group.DupeMode); + $DownloadsLogTable.fasttable('update', []); $DownloadsFileTable.fasttable('update', []); var postParamConfig = ParamTab.createPostParamConfig(); - + Util.show('#DownloadsEdit_NZBNameReadonly', group.postprocess); - Util.show('#DownloadsEdit_CancelPPGroup', group.postprocess); - Util.show('#DownloadsEdit_DeleteGroup', !group.postprocess); - Util.show('#DownloadsEdit_PauseGroup', !group.postprocess); - Util.show('#DownloadsEdit_ResumeGroup', false); + Util.show('#DownloadsEdit_CancelPP', group.postprocess); + Util.show('#DownloadsEdit_Delete', !group.postprocess); + Util.show('#DownloadsEdit_Pause', group.Kind === 'NZB' && !group.postprocess); + Util.show('#DownloadsEdit_Resume', false); Util.show('#DownloadsEdit_Save', !group.postprocess); - var postParam = postParamConfig[0].options.length > 0; - var postLog = group.postprocess && group.post.Log.length > 0; + Util.show('#DownloadsEdit_StatisticsGroup', group.Kind === 'NZB'); + Util.show('#DownloadsEdit_File', group.Kind === 'NZB'); + Util.show('#DownloadsEdit_URLGroup', group.Kind === 'URL'); + $('#DownloadsEdit_CategoryGroup').toggleClass('control-group-last', group.Kind === 'URL'); + var dupeCheck = Options.option('DupeCheck') === 'yes'; + Util.show('#DownloadsEdit_Dupe', dupeCheck); + var postParam = postParamConfig[0].options.length > 0 && group.Kind === 'NZB'; + var postLog = group.postprocess && group.Log.length > 0; Util.show('#DownloadsEdit_Param', postParam); Util.show('#DownloadsEdit_Log', postLog); @@ -207,6 +253,7 @@ var DownloadsEditDialog = (new function($) $('#DownloadsEdit_Priority').attr('disabled', 'disabled'); $('#DownloadsEdit_Category').attr('disabled', 'disabled'); $('#DownloadsEdit_Close').addClass('btn-primary'); + $('#DownloadsEdit_Close').text('Close'); } else { @@ -214,11 +261,13 @@ var DownloadsEditDialog = (new function($) $('#DownloadsEdit_Priority').removeAttr('disabled'); $('#DownloadsEdit_Category').removeAttr('disabled'); $('#DownloadsEdit_Close').removeClass('btn-primary'); + $('#DownloadsEdit_Close').text('Cancel'); - if (group.RemainingSizeHi == group.PausedSizeHi && group.RemainingSizeLo == group.PausedSizeLo) + if (group.RemainingSizeHi == group.PausedSizeHi && group.RemainingSizeLo == group.PausedSizeLo && + group.Kind === 'NZB') { - $('#DownloadsEdit_ResumeGroup').show(); - $('#DownloadsEdit_PauseGroup').hide(); + $('#DownloadsEdit_Resume').show(); + $('#DownloadsEdit_Pause').hide(); } } @@ -227,16 +276,20 @@ var DownloadsEditDialog = (new function($) postParams = ParamTab.buildPostParamTab($DownloadsEdit_ParamData, postParamConfig, curGroup.Parameters); } + EditUI.buildDNZBLinks(curGroup.Parameters, 'DownloadsEdit_DNZB'); + enableAllButtons(); $('#DownloadsEdit_GeneralTab').show(); $('#DownloadsEdit_ParamTab').hide(); + $('#DownloadsEdit_ServStatsTab').hide(); $('#DownloadsEdit_LogTab').hide(); $('#DownloadsEdit_FileTab').hide(); + $('#DownloadsEdit_DupeTab').hide(); $('#DownloadsEdit_Back').hide(); $('#DownloadsEdit_BackSpace').show(); $DownloadsEditDialog.restoreTab(); - + $('#DownloadsEdit_FileTable_filter').val(''); $('#DownloadsEdit_LogTable_filter').val(''); $('#DownloadsEdit_LogTable_pagerBlock').hide(); @@ -244,6 +297,14 @@ var DownloadsEditDialog = (new function($) files = null; logFilled = false; notification = null; + oldCategory = curGroup.Category; + + if (area === 'backup') + { + showing = true; + $('#DownloadsEdit_ServStats').trigger('click'); + } + showing = false; $DownloadsEditDialog.modal({backdrop: 'static'}); } @@ -258,20 +319,20 @@ var DownloadsEditDialog = (new function($) notification = null; } } - + function tabClick(e) { e.preventDefault(); - $('#DownloadsEdit_Back').fadeIn(500); + $('#DownloadsEdit_Back').fadeIn(showing ? 0 : 500); $('#DownloadsEdit_BackSpace').hide(); var tab = '#' + $(this).attr('data-tab'); lastPage = $(tab); lastFullscreen = ($(this).attr('data-fullscreen') === 'true') && !UISettings.miniTheme; - + $('#DownloadsEdit_FileBlock').removeClass('modal-inner-scroll'); $('#DownloadsEdit_FileBlock').css('top', ''); - + if (UISettings.miniTheme && files === null) { $('#DownloadsEdit_FileBlock').css('min-height', $DownloadsEditDialog.height()); @@ -281,9 +342,9 @@ var DownloadsEditDialog = (new function($) { $('#DownloadsEdit_LogBlock').css('min-height', $DownloadsEditDialog.height()); } - - $DownloadsEditDialog.switchTab($('#DownloadsEdit_GeneralTab'), lastPage, - e.shiftKey || !UISettings.slideAnimation ? 0 : 500, + + $DownloadsEditDialog.switchTab($('#DownloadsEdit_GeneralTab'), lastPage, + e.shiftKey || !UISettings.slideAnimation || showing ? 0 : 500, {fullscreen: lastFullscreen, mini: UISettings.miniTheme, complete: function() { if (!UISettings.miniTheme) @@ -298,8 +359,8 @@ var DownloadsEditDialog = (new function($) } }}); - if (tab === '#DownloadsEdit_LogTab' && !logFilled && curGroup.post && - curGroup.post.Log && curGroup.post.Log.length > 0) + if (tab === '#DownloadsEdit_LogTab' && !logFilled && curGroup.postprocess && + curGroup.Log && curGroup.Log.length > 0) { fillLog(); } @@ -308,6 +369,11 @@ var DownloadsEditDialog = (new function($) { fillFiles(); } + + if (tab === '#DownloadsEdit_ServStatsTab') + { + scheduleRefresh(); + } } function backClick(e) @@ -320,10 +386,12 @@ var DownloadsEditDialog = (new function($) $('#DownloadsEdit_FileBlock').removeClass('modal-inner-scroll'); $('#DownloadsEdit_FileBlock').css('top', ''); - - $DownloadsEditDialog.switchTab(lastPage, $('#DownloadsEdit_GeneralTab'), + + $DownloadsEditDialog.switchTab(lastPage, $('#DownloadsEdit_GeneralTab'), e.shiftKey || !UISettings.slideAnimation ? 0 : 500, {fullscreen: lastFullscreen, mini: UISettings.miniTheme, back: true}); + + clearTimeout(refreshTimer); } function disableAllButtons() @@ -341,8 +409,9 @@ var DownloadsEditDialog = (new function($) $('#DownloadsEdit_Transmit').hide(); } - function saveChanges() + function saveChanges(e) { + e.preventDefault(); disableAllButtons(); notification = null; saveName(); @@ -352,7 +421,7 @@ var DownloadsEditDialog = (new function($) { var name = $('#DownloadsEdit_NZBName').val(); name !== curGroup.NZBName && !curGroup.postprocess ? - RPC.call('editqueue', ['GroupSetName', 0, name, [curGroup.LastID]], function() + RPC.call('editqueue', ['GroupSetName', 0, name, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; savePriority(); @@ -363,8 +432,8 @@ var DownloadsEditDialog = (new function($) function savePriority() { var priority = parseInt($('#DownloadsEdit_Priority').val()); - priority !== curGroup.MaxPriority && curGroup.LastID > 0 ? - RPC.call('editqueue', ['GroupSetPriority', 0, ''+priority, [curGroup.LastID]], function() + priority !== curGroup.MaxPriority ? + RPC.call('editqueue', ['GroupSetPriority', 0, ''+priority, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; saveCategory(); @@ -375,27 +444,29 @@ var DownloadsEditDialog = (new function($) function saveCategory() { var category = $('#DownloadsEdit_Category').val(); - category !== curGroup.Category && curGroup.LastID > 0 ? - RPC.call('editqueue', ['GroupSetCategory', 0, category, [curGroup.LastID]], function() + category !== curGroup.Category ? + RPC.call('editqueue', ['GroupSetCategory', 0, category, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; - saveParam(); + saveDupeKey(); }) - : saveParam(); + : saveDupeKey(); } - function itemPause() + function itemPause(e) { + e.preventDefault(); disableAllButtons(); notification = '#Notif_Downloads_Paused'; - RPC.call('editqueue', ['GroupPause', 0, '', [curGroup.LastID]], completed); + RPC.call('editqueue', ['GroupPause', 0, '', [curGroup.NZBID]], completed); } - function itemResume() + function itemResume(e) { + e.preventDefault(); disableAllButtons(); notification = '#Notif_Downloads_Resumed'; - RPC.call('editqueue', ['GroupResume', 0, '', [curGroup.LastID]], function() + RPC.call('editqueue', ['GroupResume', 0, '', [curGroup.NZBID]], function() { if (Options.option('ParCheck') === 'force') { @@ -403,42 +474,49 @@ var DownloadsEditDialog = (new function($) } else { - RPC.call('editqueue', ['GroupPauseExtraPars', 0, '', [curGroup.LastID]], completed); + RPC.call('editqueue', ['GroupPauseExtraPars', 0, '', [curGroup.NZBID]], completed); } }); } - function itemDelete() + function itemDelete(e) + { + e.preventDefault(); + DownloadsUI.deleteConfirm(doItemDelete, false, curGroup.Kind === 'NZB', curGroup.Kind === 'URL'); + } + + function doItemDelete(command) { disableAllButtons(); notification = '#Notif_Downloads_Deleted'; - RPC.call('editqueue', ['GroupDelete', 0, '', [curGroup.LastID]], completed); + RPC.call('editqueue', [command, 0, '', [curGroup.NZBID]], completed); } - function itemCancelPP() + function itemCancelPP(e) { + e.preventDefault(); disableAllButtons(); notification = '#Notif_Downloads_PostCanceled'; - - var postDelete = function() - { - RPC.call('editqueue', ['PostDelete', 0, '', [curGroup.post.ID]], completed); - }; - - if (curGroup.LastID > 0) - { - RPC.call('editqueue', ['GroupDelete', 0, '', [curGroup.LastID]], postDelete); - } - else - { - postDelete(); - } + RPC.call('editqueue', ['PostDelete', 0, '', [curGroup.NZBID]], completed); } + function categoryChange() + { + var category = $('#DownloadsEdit_Category').val(); + ParamTab.reassignParams(postParams, oldCategory, category); + oldCategory = category; + } + /*** TAB: POST-PROCESSING PARAMETERS **************************************************/ function saveParam() { + if (curGroup.Kind === 'URL') + { + completed(); + return; + } + var paramList = ParamTab.prepareParamRequest(postParams); saveNextParam(paramList); } @@ -447,7 +525,7 @@ var DownloadsEditDialog = (new function($) { if (paramList.length > 0) { - RPC.call('editqueue', ['GroupSetParameter', 0, paramList[0], [curGroup.LastID]], function() + RPC.call('editqueue', ['GroupSetParameter', 0, paramList[0], [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; paramList.shift(); @@ -460,6 +538,44 @@ var DownloadsEditDialog = (new function($) } } + /*** TAB: DUPLICATE SETTINGS **************************************************/ + + function saveDupeKey() + { + var value = $('#DownloadsEdit_DupeKey').val(); + value !== curGroup.DupeKey ? + RPC.call('editqueue', ['GroupSetDupeKey', 0, value, [curGroup.NZBID]], function() + { + notification = '#Notif_Downloads_Saved'; + saveDupeScore(); + }) + :saveDupeScore(); + } + + function saveDupeScore() + { + var value = $('#DownloadsEdit_DupeScore').val(); + value != curGroup.DupeScore ? + RPC.call('editqueue', ['GroupSetDupeScore', 0, value, [curGroup.NZBID]], function() + { + notification = '#Notif_Downloads_Saved'; + saveDupeMode(); + }) + :saveDupeMode(); + } + + function saveDupeMode() + { + var value = $('#DownloadsEdit_DupeMode').val(); + value !== curGroup.DupeMode ? + RPC.call('editqueue', ['GroupSetDupeMode', 0, value, [curGroup.NZBID]], function() + { + notification = '#Notif_Downloads_Saved'; + saveParam(); + }) + :saveParam(); + } + /*** TAB: LOG *************************************************************************/ function fillLog() @@ -467,9 +583,9 @@ var DownloadsEditDialog = (new function($) logFilled = true; var data = []; - for (var i=0; i < curGroup.post.Log.length; i++) + for (var i=0; i < curGroup.Log.length; i++) { - var message = curGroup.post.Log[i]; + var message = curGroup.Log[i]; var kind; switch (message.Kind) @@ -494,7 +610,7 @@ var DownloadsEditDialog = (new function($) var info = kind + ' ' + time + ' ' + text; fields = [info]; } - + var item = { id: message, @@ -542,13 +658,13 @@ var DownloadsEditDialog = (new function($) { file.status = file.Paused ? (file.ActiveDownloads > 0 ? 'pausing' : 'paused') : (file.ActiveDownloads > 0 ? 'downloading' : 'queued'); } - + var age = Util.formatAge(file.PostTime + UISettings.timeZoneCorrection*60*60); var size = Util.formatSizeMB(0, file.FileSizeLo); if (file.FileSizeLo !== file.RemainingSizeLo) { size = '(' + Util.round0(file.RemainingSizeLo / file.FileSizeLo * 100) + '%) ' + size; - } + } var status; switch (file.status) @@ -560,34 +676,28 @@ var DownloadsEditDialog = (new function($) case 'deleted': status = 'deleted'; break; default: status = 'internal error(' + file.status + ')'; } - - var priority = ''; - if (file.Priority != curGroup.MaxPriority) - { - priority = DownloadsUI.buildPriority(file.Priority); - } var name = Util.textToHtml(file.Filename); var fields; if (!UISettings.miniTheme) { - var info = name + ' ' + priority; + var info = name; fields = ['
    ', status, info, age, size]; } else { var info = '
    ' + name + '' + - ' ' + (file.status === 'queued' ? '' : status) + ' ' + priority; + ' ' + (file.status === 'queued' ? '' : status); fields = [info]; } - + var item = { id: file.ID, file: file, fields: fields, - search: file.status + ' ' + file.Filename + ' ' + priority + ' ' + age + ' ' + size + search: file.status + ' ' + file.Filename + ' ' + age + ' ' + size }; data.push(item); @@ -615,7 +725,7 @@ var DownloadsEditDialog = (new function($) var checkedRows = $DownloadsFileTable.fasttable('checkedRows'); if (checkedRows.length == 0) { - Notification.show('#Notif_Select'); + Notification.show('#Notif_Edit_Select'); return; } @@ -627,7 +737,7 @@ var DownloadsEditDialog = (new function($) var editIDList = []; var splitError = false; - + for (var i = 0; i < files.length; i++) { var n = i; @@ -637,11 +747,11 @@ var DownloadsEditDialog = (new function($) n = files.length-1-i; } var file = files[n]; - + if (checkedRows.indexOf(file.ID) > -1) { editIDList.push(file.ID); - + switch (action) { case 'pause': @@ -703,7 +813,7 @@ var DownloadsEditDialog = (new function($) } } } - + if (action === 'split') { if (splitError) @@ -715,7 +825,7 @@ var DownloadsEditDialog = (new function($) DownloadsSplitDialog.showModal(curGroup, editIDList); } } - + filesLoaded(files); } @@ -726,7 +836,7 @@ var DownloadsEditDialog = (new function($) saveFileOrder(); return; } - + var action = actions.shift(); var command = commands.shift(); @@ -775,7 +885,7 @@ var DownloadsEditDialog = (new function($) IDs.push(file.ID); hasMovedFiles |= file.editMoved; } - + if (hasMovedFiles) { RPC.call('editqueue', ['FileReorder', 0, '', IDs], function() @@ -789,6 +899,124 @@ var DownloadsEditDialog = (new function($) completed(); } } + + /*** TAB: PER-SERVER STATUSTICS *****************************************************************/ + + function scheduleRefresh() + { + refreshTimer = setTimeout(updateServStats, UISettings.refreshInterval * 1000); + } + + function updateServStats() + { + RPC.call('listgroups', [], groups_loaded); + } + + function groups_loaded(groups) + { + for (var i=0, il=groups.length; i < il; i++) + { + var group = groups[i]; + if (group.NZBID === curGroup.NZBID) + { + curGroup.ServerStats = group.ServerStats; + EditUI.fillServStats($ServStatsTable, group); + scheduleRefresh(); + break; + } + } + } +}(jQuery)); + + +/*** COMMON FUNCTIONS FOR EDIT DIALOGS ************************************************************/ + +var EditUI = (new function($) +{ + 'use strict' + + this.buildDNZBLinks = function(parameters, prefix) + { + $('.' + prefix).hide(); + var hasItems = false; + + for (var i=0; i < parameters.length; i++) + { + var param = parameters[i]; + if (param.Name.substr(0, 6) === '*DNZB:') + { + var linkName = param.Name.substr(6, 100); + var $paramLink = $('#' + prefix + '_' + linkName); + if($paramLink.length > 0) + { + $paramLink.attr('href', param.Value); + $paramLink.show(); + hasItems = true; + } + } + } + + Util.show('#' + prefix + '_Section', hasItems); + } + + /*** TAB: SERVER STATISTICS **************************************************/ + + this.fillServStats = function(table, editItem) + { + var data = []; + for (var i=0; i < Status.status.NewsServers.length; i++) + { + var server = Status.status.NewsServers[i]; + var name = Options.option('Server' + server.ID + '.Name'); + if (name === null || name === '') + { + var host = Options.option('Server' + server.ID + '.Host'); + var port = Options.option('Server' + server.ID + '.Port'); + name = (host === null ? '' : host) + ':' + (port === null ? '119' : port); + } + + var articles = '--'; + var artquota = '--'; + var success = '--'; + var failures = '--'; + for (var j=0; j < editItem.ServerStats.length; j++) + { + var stat = editItem.ServerStats[j]; + if (stat.ServerID === server.ID && stat.SuccessArticles + stat.FailedArticles > 0) + { + articles = stat.SuccessArticles + stat.FailedArticles; + artquota = Util.round0(articles * 100.0 / (editItem.SuccessArticles + editItem.FailedArticles)) + '%'; + success = Util.round0(stat.SuccessArticles * 100.0 / articles) + '%'; + failures = Util.round0(stat.FailedArticles * 100.0 / articles) + '%'; + if (stat.FailedArticles > 0 && failures === '0%') + { + success = '99.9%'; + failures = '0.1%'; + } + break; + } + } + + var fields = [server.ID + '. ' + name, articles, artquota, success, failures]; + var item = + { + id: server.ID, + fields: fields, + search: '' + }; + data.push(item); + } + table.fasttable('update', data); + } + + this.servStatsTableRenderCellCallback = function (cell, index, item) + { + if (index > 0) + { + cell.className = 'text-right'; + } + } + }(jQuery)); @@ -829,26 +1057,21 @@ var ParamTab = (new function($) defineBuiltinParams(postParamConfig); return postParamConfig; } - + function defineBuiltinParams(postParamConfig) { - if (Options.option('Unpack') !== 'yes') - { - return; - } - if (postParamConfig.length == 0) { postParamConfig.push({category: 'P', postparam: true, options: []}); } - + if (!Options.findOption(postParamConfig[0].options, '*Unpack:')) { postParamConfig[0].options.unshift({name: '*Unpack:Password', value: '', defvalue: '', select: [], caption: 'Password', sectionId: '_Unpack_', description: 'Unpack-password for encrypted archives.'}); postParamConfig[0].options.unshift({name: '*Unpack:', value: '', defvalue: 'yes', select: ['yes', 'no'], caption: 'Unpack', sectionId: '_Unpack_', description: 'Unpack rar and 7-zip archives.'}); } } - + this.prepareParamRequest = function(postParams) { var request = []; @@ -862,7 +1085,7 @@ var ParamTab = (new function($) { var oldValue = option.value; var newValue = Config.getOptionValue(option); - if (oldValue != newValue && !(oldValue === '' && newValue === option.defvalue)) + if (oldValue != newValue && !((oldValue === null || oldValue === '') && newValue === option.defvalue)) { var opt = option.name + '=' + newValue; request.push(opt); @@ -872,6 +1095,65 @@ var ParamTab = (new function($) } return request; } + + function buildCategoryScriptList(category) + { + var scriptList = []; + + for (var i=0; i < Options.categories.length; i++) + { + if (category === Options.categories[i]) + { + scriptList = Util.parseCommaList(Options.option('Category' + (i + 1) + '.PostScript')); + if (scriptList.length === 0) + { + scriptList = Util.parseCommaList(Options.option('PostScript')); + } + if (Options.option('Category' + (i + 1) + '.Unpack') === 'yes') + { + scriptList.push('*Unpack'); + } + return scriptList; + } + } + + // empty category or category not found + scriptList = Util.parseCommaList(Options.option('PostScript')); + if (Options.option('Unpack') === 'yes') + { + scriptList.push('*Unpack'); + } + return scriptList; + } + + this.reassignParams = function(postParams, oldCategory, newCategory) + { + var oldScriptList = buildCategoryScriptList(oldCategory); + var newScriptList = buildCategoryScriptList(newCategory); + + for (var i=0; i < postParams.length; i++) + { + var section = postParams[i]; + for (var j=0; j < section.options.length; j++) + { + var option = section.options[j]; + if (!option.template && !section.hidden && option.name.substr(option.name.length - 1, 1) === ':') + { + console.log(option.name); + var scriptName = option.name.substr(0, option.name.length-1); + if (oldScriptList.indexOf(scriptName) > -1 && newScriptList.indexOf(scriptName) === -1) + { + Config.setOptionValue(option, 'no'); + } + else if (oldScriptList.indexOf(scriptName) === -1 && newScriptList.indexOf(scriptName) > -1) + { + Config.setOptionValue(option, 'yes'); + } + } + } + } + } + }(jQuery)); @@ -883,17 +1165,17 @@ var DownloadsMultiDialog = (new function($) // Controls var $DownloadsMultiDialog; - + // State var multiIDList; var notification = null; var oldPriority; var oldCategory; - + this.init = function() { $DownloadsMultiDialog = $('#DownloadsMultiDialog'); - + $('#DownloadsMulti_Save').click(saveChanges); $DownloadsMultiDialog.on('hidden', function () @@ -924,7 +1206,7 @@ var DownloadsMultiDialog = (new function($) if (nzbIdList.indexOf(gr.NZBID) > -1) { groups.push(gr); - multiIDList.push(gr.LastID); + multiIDList.push(gr.NZBID); } } if (groups.length == 0) @@ -955,7 +1237,7 @@ var DownloadsMultiDialog = (new function($) FileCount += group.FileCount; RemainingFileCount += group.RemainingFileCount; RemainingParCount += group.RemainingParCount; - paused = paused && group.paused; + paused = paused && group.Status === 'PAUSED'; PriorityDiff = PriorityDiff || (Priority !== group.MaxPriority); CategoryDiff = CategoryDiff || (Category !== group.Category); } @@ -1011,7 +1293,7 @@ var DownloadsMultiDialog = (new function($) $('#DownloadsMulti_GeneralTabLink').tab('show'); notification = null; - + $DownloadsMultiDialog.modal({backdrop: 'static'}); } @@ -1030,8 +1312,9 @@ var DownloadsMultiDialog = (new function($) }, 500); } - function saveChanges() + function saveChanges(e) { + e.preventDefault(); disableAllButtons(); savePriority(); } @@ -1052,14 +1335,14 @@ var DownloadsMultiDialog = (new function($) { var category = $('#DownloadsMulti_Category').val(); (category !== oldCategory && category !== '') ? - RPC.call('editqueue', ['GroupSetCategory', 0, category, multiIDList], function() + RPC.call('editqueue', ['GroupApplyCategory', 0, category, multiIDList], function() { notification = '#Notif_Downloads_Saved'; completed(); }) : completed(); } - + function completed() { $DownloadsMultiDialog.modal('hide'); @@ -1080,14 +1363,14 @@ var DownloadsMergeDialog = (new function($) // Controls var $DownloadsMergeDialog; - + // State var mergeEditIDList; this.init = function() { $DownloadsMergeDialog = $('#DownloadsMergeDialog'); - + $('#DownloadsMerge_Merge').click(merge); $DownloadsMergeDialog.on('hidden', function () @@ -1115,7 +1398,7 @@ var DownloadsMergeDialog = (new function($) var group = allGroups[i]; if (nzbIdList.indexOf(group.NZBID) > -1) { - mergeEditIDList.push(group.LastID); + mergeEditIDList.push(group.NZBID); var html = '
    ' + Util.formatNZBName(group.NZBName) + '
    '; $('#DownloadsMerge_Files').append(html); @@ -1147,14 +1430,14 @@ var DownloadsSplitDialog = (new function($) // Controls var $DownloadsSplitDialog; - + // State var splitEditIDList; this.init = function() { $DownloadsSplitDialog = $('#DownloadsSplitDialog'); - + $('#DownloadsSplit_Split').click(split); $DownloadsSplitDialog.on('hidden', function () @@ -1205,6 +1488,7 @@ var HistoryEditDialog = (new function() // Controls var $HistoryEditDialog; var $HistoryEdit_ParamData; + var $ServStatsTable; // State var curHist; @@ -1212,7 +1496,7 @@ var HistoryEditDialog = (new function() var postParams = []; var lastPage; var lastFullscreen; - var saveParamCompleted; + var saveCompleted; this.init = function() { @@ -1221,18 +1505,32 @@ var HistoryEditDialog = (new function() $('#HistoryEdit_Save').click(saveChanges); $('#HistoryEdit_Delete').click(itemDelete); - $('#HistoryEdit_Return').click(itemReturn); + $('#HistoryEdit_Return, #HistoryEdit_ReturnURL').click(itemReturn); $('#HistoryEdit_Reprocess').click(itemReprocess); - $('#HistoryEdit_Param').click(tabClick); + $('#HistoryEdit_Redownload').click(itemRedownload); + $('#HistoryEdit_Param, #HistoryEdit_Dupe').click(tabClick); $('#HistoryEdit_Back').click(backClick); - + $('#HistoryEdit_MarkGood').click(itemGood); + $('#HistoryEdit_MarkBad').click(itemBad); + + $ServStatsTable = $('#HistoryEdit_ServStatsTable'); + $ServStatsTable.fasttable( + { + filterInput: '#HistoryEdit_ServStatsTable_filter', + pagerContainer: '#HistoryEdit_ServStatsTable_pager', + pageSize: 100, + maxPages: 3, + hasHeader: true, + renderCellCallback: EditUI.servStatsTableRenderCellCallback + }); + $HistoryEditDialog.on('hidden', function () { $HistoryEdit_ParamData.empty(); // resume updates Refresher.resume(); }); - + TabDialog.extend($HistoryEditDialog); } @@ -1243,66 +1541,206 @@ var HistoryEditDialog = (new function() curHist = hist; var status; - if (hist.Kind === 'URL') - { - status = HistoryUI.buildStatus(hist.status, ''); - } - else + if (hist.Kind === 'NZB') { - status = HistoryUI.buildStatus(hist.ParStatus, 'Par: ') + ' ' + - (Options.option('Unpack') == 'yes' || hist.UnpackStatus != 'NONE' ? HistoryUI.buildStatus(hist.UnpackStatus, 'Unpack: ') + ' ' : '') + - (hist.MoveStatus === "FAILURE" ? HistoryUI.buildStatus(hist.MoveStatus, 'Move: ') + ' ' : ""); + status = 'health: ' + Math.floor(hist.Health / 10) + '%'; + + if (hist.MarkStatus !== 'NONE') + { + status += ' ' + buildStatus(hist.MarkStatus, 'Mark: '); + } + + if (hist.DeleteStatus === 'NONE') + { + status += ' ' + buildStatus(hist.ParStatus, 'Par: ') + + ' ' + (Options.option('Unpack') == 'yes' || hist.UnpackStatus != 'NONE' ? buildStatus(hist.UnpackStatus, 'Unpack: ') : '') + + ' ' + (hist.MoveStatus === "FAILURE" ? buildStatus(hist.MoveStatus, 'Move: ') : ''); + } + else + { + status += ' ' + buildStatus('DELETED-' + hist.DeleteStatus, 'Delete: '); + } + for (var i=0; iURL'); + $('#HistoryEdit_Title').html($('#HistoryEdit_Title').html() + ' ' + '' + + (hist.Kind === 'DUP' ? 'hidden' : hist.Kind) + ''); } - $('#HistoryEdit_Status').html(status); - $('#HistoryEdit_Category').text(hist.Category !== '' ? hist.Category : ''); - $('#HistoryEdit_Path').text(hist.DestDir); + if (hist.Kind !== 'DUP') + { + $('#HistoryEdit_Category').text(hist.Category); + } - var size = Util.formatSizeMB(hist.FileSizeMB, hist.FileSizeLo); + if (hist.Kind === 'NZB') + { + $('#HistoryEdit_Path').text(hist.FinalDir !== '' ? hist.FinalDir : hist.DestDir); - var table = ''; - table += 'Total' + size + ''; - table += 'Files (total/parked)' + hist.FileCount + '/' + hist.RemainingFileCount + ''; - $('#HistoryEdit_Statistics').html(table); + var size = Util.formatSizeMB(hist.FileSizeMB, hist.FileSizeLo); + var completion = hist.SuccessArticles + hist.FailedArticles > 0 ? Util.round0(hist.SuccessArticles * 100.0 / (hist.SuccessArticles + hist.FailedArticles)) + '%' : '--'; + if (hist.FailedArticles > 0 && completion === '100%') + { + completion = '99.9%'; + } + var time = Util.formatTimeHMS(hist.DownloadTimeSec + hist.PostTotalTimeSec); + + var table = ''; + table += 'Total '+ + '' + + '' + size + ''; + table += 'Files (total/remaining)' + hist.FileCount + ' / ' + hist.RemainingFileCount + ''; + table += '' + + (hist.ServerStats.length > 0 ? '' : '') + + 'Articles (total/completion)' + + (hist.ServerStats.length > 0 ? ' ' : '') + + '' + hist.TotalArticles + ' / ' + completion + ''; + $('#HistoryEdit_Statistics').html(table); + + $('#HistoryEdit_ServStats').click(tabClick); + EditUI.fillServStats($ServStatsTable, hist); + $ServStatsTable.fasttable('setCurPage', 1); + + $('#HistoryEdit_TimeStats').click(tabClick); + fillTimeStats(); + } - Util.show($('#HistoryEdit_ReturnGroup'), hist.RemainingFileCount > 0 || hist.Kind === 'URL'); - Util.show($('#HistoryEdit_PathGroup, #HistoryEdit_StatisticsGroup, #HistoryEdit_ReprocessGroup'), hist.Kind === 'NZB'); + $('#HistoryEdit_DupeKey').val(hist.DupeKey); + $('#HistoryEdit_DupeScore').val(hist.DupeScore); + $('#HistoryEdit_DupeMode').val(hist.DupeMode); + $('#HistoryEdit_DupeBackup').prop('checked', hist.DeleteStatus === 'DUPE'); + $('#HistoryEdit_DupeBackup').prop('disabled', !(hist.DeleteStatus === 'DUPE' || hist.DeleteStatus === 'MANUAL')); + Util.show($('#HistoryEdit_DupeBackup').closest('.control-group'), hist.Kind === 'NZB'); + $('#HistoryEdit_DupeMode').closest('.control-group').toggleClass('last-group', hist.Kind !== 'NZB'); + + Util.show('#HistoryEdit_Return', hist.RemainingFileCount > 0); + Util.show('#HistoryEdit_ReturnURL', hist.Kind === 'URL'); + Util.show('#HistoryEdit_Redownload', hist.Kind === 'NZB'); + Util.show('#HistoryEdit_PathGroup, #HistoryEdit_StatisticsGroup, #HistoryEdit_Reprocess', hist.Kind === 'NZB'); + Util.show('#HistoryEdit_CategoryGroup', hist.Kind !== 'DUP'); + Util.show('#HistoryEdit_DupGroup', hist.Kind === 'DUP'); + var dupeCheck = Options.option('DupeCheck') === 'yes'; + Util.show('#HistoryEdit_MarkGood', dupeCheck && ((hist.Kind === 'NZB' && hist.MarkStatus !== 'GOOD') || (hist.Kind === 'DUP' && hist.DupStatus !== 'GOOD'))); + Util.show('#HistoryEdit_MarkBad', dupeCheck && hist.Kind !== 'URL'); + Util.show('#HistoryEdit_Dupe', dupeCheck); + $('#HistoryEdit_CategoryGroup').toggleClass('control-group-last', hist.Kind === 'URL'); + + Util.show('#HistoryEdit_URLGroup', hist.Kind === 'URL'); + $('#HistoryEdit_URL').attr('value', hist.URL); var postParamConfig = ParamTab.createPostParamConfig(); var postParam = hist.Kind === 'NZB' && postParamConfig[0].options.length > 0; Util.show('#HistoryEdit_Param', postParam); - Util.show('#HistoryEdit_Save', postParam); - $('#HistoryEdit_Close').toggleClass('btn-primary', !postParam); - + if (postParam) { postParams = ParamTab.buildPostParamTab($HistoryEdit_ParamData, postParamConfig, curHist.Parameters); } - + + EditUI.buildDNZBLinks(curHist.Parameters ? curHist.Parameters : [], 'HistoryEdit_DNZB'); + enableAllButtons(); - + $('#HistoryEdit_GeneralTab').show(); $('#HistoryEdit_ParamTab').hide(); + $('#HistoryEdit_ServStatsTab').hide(); + $('#HistoryEdit_TimeStatsTab').hide(); + $('#HistoryEdit_DupeTab').hide(); $('#HistoryEdit_Back').hide(); $('#HistoryEdit_BackSpace').show(); $HistoryEditDialog.restoreTab(); - + notification = null; - + $HistoryEditDialog.modal({backdrop: 'static'}); } + function buildStatus(status, prefix) + { + switch (status) + { + case 'SUCCESS': + case 'GOOD': + return '' + prefix + status + ''; + case 'FAILURE': + return '' + prefix + 'failure'; + case 'BAD': + return '' + prefix + status + ''; + case 'REPAIR_POSSIBLE': + return '' + prefix + 'repairable'; + case 'MANUAL': // PAR-MANUAL + case 'SPACE': + case 'PASSWORD': + return '' + prefix + status + ''; + case 'DELETED-DUPE': + return '' + prefix + 'dupe'; + case 'DELETED-MANUAL': + return '' + prefix + 'manual'; + case 'DELETED-HEALTH': + return '' + prefix + 'health'; + case 'DELETED-BAD': + return '' + prefix + 'bad'; + case 'SCAN_SKIPPED': + return '' + prefix + 'skipped'; + case 'NONE': + return '' + prefix + 'none'; + default: + return '' + prefix + status + ''; + } + } + + function fillTimeStats() + { + var hist = curHist; + var downloaded = Util.formatSizeMB(hist.DownloadedSizeMB, hist.DownloadedSizeLo); + var speed = hist.DownloadTimeSec > 0 ? Util.formatSpeed((hist.DownloadedSizeMB > 1024 ? hist.DownloadedSizeMB * 1024.0 * 1024.0 : hist.DownloadedSizeLo) / hist.DownloadTimeSec) : '--'; + var table = ''; + table += 'Downloaded size' + downloaded + ''; + table += 'Download speed' + speed + ''; + table += 'Total time' + Util.formatTimeHMS(hist.DownloadTimeSec + hist.PostTotalTimeSec) + ''; + table += 'Download time' + Util.formatTimeHMS(hist.DownloadTimeSec) + ''; + table += 'Verification time ' + Util.formatTimeHMS(hist.ParTimeSec - hist.RepairTimeSec) + ''; + table += 'Repair time' + Util.formatTimeHMS(hist.RepairTimeSec) + ''; + table += 'Unpack time' + Util.formatTimeHMS(hist.UnpackTimeSec) + ''; + + $('#HistoryEdit_TimeStatsTable tbody').html(table); + } + function tabClick(e) { e.preventDefault(); @@ -1312,9 +1750,9 @@ var HistoryEditDialog = (new function() var tab = '#' + $(this).attr('data-tab'); lastPage = $(tab); lastFullscreen = ($(this).attr('data-fullscreen') === 'true') && !UISettings.miniTheme; - - $HistoryEditDialog.switchTab($('#HistoryEdit_GeneralTab'), lastPage, - e.shiftKey || !UISettings.slideAnimation ? 0 : 500, + + $HistoryEditDialog.switchTab($('#HistoryEdit_GeneralTab'), lastPage, + e.shiftKey || !UISettings.slideAnimation ? 0 : 500, {fullscreen: lastFullscreen, mini: UISettings.miniTheme}); } @@ -1326,7 +1764,7 @@ var HistoryEditDialog = (new function() $('#HistoryEdit_BackSpace').show(); }); - $HistoryEditDialog.switchTab(lastPage, $('#HistoryEdit_GeneralTab'), + $HistoryEditDialog.switchTab(lastPage, $('#HistoryEdit_GeneralTab'), e.shiftKey || !UISettings.slideAnimation ? 0 : 500, {fullscreen: lastFullscreen, mini: UISettings.miniTheme, back: true}); } @@ -1346,28 +1784,60 @@ var HistoryEditDialog = (new function() $('#HistoryEdit_Transmit').hide(); } - function itemDelete() + function itemDelete(e) + { + e.preventDefault(); + HistoryUI.deleteConfirm(doItemDelete, curHist.Kind === 'NZB', curHist.Kind === 'DUP', + curHist.ParStatus === 'FAILURE' || curHist.UnpackStatus === 'FAILURE', false); + } + + function doItemDelete(command) { disableAllButtons(); notification = '#Notif_History_Deleted'; - RPC.call('editqueue', ['HistoryDelete', 0, '', [curHist.ID]], completed); + RPC.call('editqueue', [command, 0, '', [curHist.ID]], completed); } - function itemReturn() + function itemReturn(e) { + e.preventDefault(); disableAllButtons(); notification = '#Notif_History_Returned'; RPC.call('editqueue', ['HistoryReturn', 0, '', [curHist.ID]], completed); } - function itemReprocess() + function itemRedownload(e) + { + e.preventDefault(); + if (curHist.SuccessArticles > 0) + { + ConfirmDialog.showModal('HistoryEditRedownloadConfirmDialog', doItemRedownload); + } + else + { + doItemRedownload(); + } + } + + function doItemRedownload() { disableAllButtons(); - saveParam(function() - { - notification = '#Notif_History_Reproces'; - RPC.call('editqueue', ['HistoryProcess', 0, '', [curHist.ID]], completed); - }); + notification = '#Notif_History_Returned'; + RPC.call('editqueue', ['HistoryRedownload', 0, '', [curHist.ID]], completed); + } + + function itemReprocess(e) + { + e.preventDefault(); + disableAllButtons(); + saveCompleted = reprocess; + saveDupeKey(); + } + + function reprocess() + { + notification = '#Notif_History_Reproces'; + RPC.call('editqueue', ['HistoryProcess', 0, '', [curHist.ID]], completed); } function completed() @@ -1380,19 +1850,52 @@ var HistoryEditDialog = (new function() notification = null; } } - - function saveChanges() + + function saveChanges(e) { + e.preventDefault(); disableAllButtons(); notification = null; - saveParam(completed); + saveCompleted = completed; + saveDupeKey(); } - + + function itemGood(e) + { + e.preventDefault(); + ConfirmDialog.showModal('HistoryEditGoodConfirmDialog', doItemGood); + } + + function doItemGood() + { + disableAllButtons(); + notification = '#Notif_History_Marked'; + RPC.call('editqueue', ['HistoryMarkGood', 0, '', [curHist.ID]], completed); + } + + function itemBad(e) + { + e.preventDefault(); + ConfirmDialog.showModal('HistoryEditBadConfirmDialog', doItemBad); + } + + function doItemBad() + { + disableAllButtons(); + notification = '#Notif_History_Marked'; + RPC.call('editqueue', ['HistoryMarkBad', 0, '', [curHist.ID]], completed); + } + /*** TAB: POST-PROCESSING PARAMETERS **************************************************/ - function saveParam(_saveParamCompleted) + function saveParam() { - saveParamCompleted = _saveParamCompleted; + if (curHist.Kind !== 'NZB') + { + saveCompleted(); + return; + } + var paramList = ParamTab.prepareParamRequest(postParams); saveNextParam(paramList); } @@ -1410,8 +1913,59 @@ var HistoryEditDialog = (new function() } else { - saveParamCompleted(); + saveCompleted(); } } - + + /*** TAB: DUPLICATE SETTINGS **************************************************/ + + function saveDupeKey() + { + var value = $('#HistoryEdit_DupeKey').val(); + value !== curHist.DupeKey ? + RPC.call('editqueue', ['HistorySetDupeKey', 0, value, [curHist.ID]], function() + { + notification = '#Notif_History_Saved'; + saveDupeScore(); + }) + :saveDupeScore(); + } + + function saveDupeScore() + { + var value = $('#HistoryEdit_DupeScore').val(); + value != curHist.DupeScore ? + RPC.call('editqueue', ['HistorySetDupeScore', 0, value, [curHist.ID]], function() + { + notification = '#Notif_History_Saved'; + saveDupeMode(); + }) + :saveDupeMode(); + } + + function saveDupeMode() + { + var value = $('#HistoryEdit_DupeMode').val(); + value !== curHist.DupeMode ? + RPC.call('editqueue', ['HistorySetDupeMode', 0, value, [curHist.ID]], function() + { + notification = '#Notif_History_Saved'; + saveDupeBackup(); + }) + :saveDupeBackup(); + } + + function saveDupeBackup() + { + var canChange = curHist.DeleteStatus === 'DUPE' || curHist.DeleteStatus === 'MANUAL'; + var oldValue = curHist.DeleteStatus === 'DUPE'; + var value = $('#HistoryEdit_DupeBackup').is(':checked'); + canChange && value !== oldValue ? + RPC.call('editqueue', ['HistorySetDupeBackup', 0, value ? "YES" : "NO", [curHist.ID]], function() + { + notification = '#Notif_History_Saved'; + saveParam(); + }) + :saveParam(); + } }(jQuery)); diff --git a/content/usr/local/share/nzbget/webui/fasttable.js b/content/usr/local/share/nzbget/webui/fasttable.js index 8fa7939..ea38f2a 100644 --- a/content/usr/local/share/nzbget/webui/fasttable.js +++ b/content/usr/local/share/nzbget/webui/fasttable.js @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * $Revision: 698 $ - * $Date: 2013-06-02 21:20:31 +0200 (Sun, 02 Jun 2013) $ + * $Revision: 778 $ + * $Date: 2013-08-07 22:09:43 +0200 (Wed, 07 Aug 2013) $ * */ @@ -222,6 +222,11 @@ return $(this).data('fasttable').filteredContent; }, + availableContent : function() + { + return $(this).data('fasttable').availableContent; + }, + checkedRows : function() { return $(this).data('fasttable').checkedRows; @@ -277,23 +282,22 @@ var phrase = filterInput.length > 0 ? filterInput.val() : ''; var caseSensitive = data.config.filterCaseSensitive; var words = caseSensitive ? phrase.split(' ') : phrase.toLowerCase().split(' '); + var hasFilter = !(words.length === 1 && words[0] === ''); - if (words.length === 1 && words[0] === '') - { - data.filteredContent = data.content; - } - else + data.availableContent = []; + data.filteredContent = []; + for (var i = 0; i < data.content.length; i++) { - data.filteredContent = []; - for (var i = 0; i < data.content.length; i++) + var item = data.content[i]; + if (hasFilter && item.search === undefined && data.config.fillSearchCallback) { - var item = data.content[i]; - if (item.search === undefined && data.config.fillSearchCallback) - { - data.config.fillSearchCallback(item); - } - - if (has_words(item.search, words, caseSensitive)) + data.config.fillSearchCallback(item); + } + + if (!hasFilter || has_words(item.search, words, caseSensitive)) + { + data.availableContent.push(item); + if (!data.config.filterCallback || data.config.filterCallback(item)) { data.filteredContent.push(item); } @@ -546,7 +550,7 @@ var firstRecord = (data.curPage - 1) * data.pageSize + 1; var lastRecord = firstRecord + data.pageContent.length - 1; var infoText = 'Showing records ' + firstRecord + '-' + lastRecord + ' from ' + data.filteredContent.length; - if (data.filteredContent != data.content) + if (data.filteredContent.length != data.content.length) { infoText += ' filtered (total ' + data.content.length + ')'; } @@ -557,8 +561,8 @@ { data.config.updateInfoCallback({ total: data.content.length, - available: data.filteredContent.length, - filter: data.filteredContent != data.content, + available: data.availableContent.length, + filtered: data.filteredContent.length, firstRecord: firstRecord, lastRecord: lastRecord }); @@ -786,6 +790,7 @@ filterInputCallback: undefined, filterClearCallback: undefined, fillSearchCallback: undefined, + filterCallback: undefined, headerCheck: '#table-header-check' }; diff --git a/content/usr/local/share/nzbget/webui/feed.js b/content/usr/local/share/nzbget/webui/feed.js new file mode 100644 index 0000000..5a6c294 --- /dev/null +++ b/content/usr/local/share/nzbget/webui/feed.js @@ -0,0 +1,928 @@ +/* + * This file is part of nzbget + * + * Copyright (C) 2013 Andrey Prygunkov + * + * 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. + * + * $Revision: 1045 $ + * $Date: 2014-06-19 17:00:46 +0200 (Thu, 19 Jun 2014) $ + * + */ + +/* + * In this module: + * 1) Feeds menu; + * 2) Feed view/preview dialog; + * 3) Feed filter dialog. + */ + + +/*** FEEDS **********************************************/ + +var Feeds = (new function($) +{ + 'use strict'; + + this.init = function() + { + } + + this.redraw = function() + { + var menu = $('#RssMenu'); + var menuItemTemplate = $('.feed-menu-template', menu); + menuItemTemplate.removeClass('feed-menu-template').removeClass('hide').addClass('feed-menu'); + var insertPos = $('#RssMenu_Divider', menu); + + $('.feed-menu', menu).remove(); + for (var i=1; ;i++) + { + var url = Options.option('Feed' + i + '.URL'); + if (url === null) + { + break; + } + + if (url.trim() !== '') + { + var item = menuItemTemplate.clone(); + var name = Options.option('Feed' + i + '.Name'); + var a = $('a', item); + a.text(name !== '' ? name : 'Feed' + i); + a.attr('data-id', i); + a.click(viewFeed); + insertPos.before(item); + } + } + + Util.show('#RssMenuBlock', $('.feed-menu', menu).length > 0); + } + + function viewFeed() + { + var id = parseInt($(this).attr('data-id')); + FeedDialog.showModal(id); + } + + this.fetchAll = function() + { + RPC.call('fetchfeed', [0], function() + { + Notification.show('#Notif_Feeds_FetchAll'); + }); + } +}(jQuery)); + + +/*** FEEDS VIEW / PREVIEW DIALOG **********************************************/ + +var FeedDialog = (new function($) +{ + 'use strict'; + + // Controls + var $FeedDialog; + var $ItemTable; + + // State + var items = null; + var pageSize = 100; + var curFilter = 'ALL'; + var filenameMode = false; + var tableInitialized = false; + + this.init = function() + { + $FeedDialog = $('#FeedDialog'); + + $ItemTable = $('#FeedDialog_ItemTable'); + $ItemTable.fasttable( + { + filterInput: '#FeedDialog_ItemTable_filter', + pagerContainer: '#FeedDialog_ItemTable_pager', + filterCaseSensitive: false, + headerCheck: '#FeedDialog_ItemTable > thead > tr:first-child', + pageSize: pageSize, + hasHeader: true, + renderCellCallback: itemsTableRenderCellCallback + }); + + $ItemTable.on('click', 'tbody div.check', + function(event) { $ItemTable.fasttable('itemCheckClick', this.parentNode.parentNode, event); }); + $ItemTable.on('click', 'thead div.check', + function() { $ItemTable.fasttable('titleCheckClick') }); + $ItemTable.on('mousedown', Util.disableShiftMouseDown); + + $FeedDialog.on('hidden', function() + { + // cleanup + $ItemTable.fasttable('update', []); + // resume updates + Refresher.resume(); + }); + + TabDialog.extend($FeedDialog); + + if (UISettings.setFocus) + { + $FeedDialog.on('shown', function() + { + //$('#FeedDialog_Name').focus(); + }); + } + } + + this.showModal = function(id, name, url, filter, pauseNzb, category, priority) + { + Refresher.pause(); + + $ItemTable.fasttable('update', []); + + enableAllButtons(); + $FeedDialog.restoreTab(); + + $('#FeedDialog_ItemTable_filter').val(''); + $('#FeedDialog_ItemTable_pagerBlock').hide(); + + items = null; + + curFilter = 'ALL'; + filenameMode = false; + tableInitialized = false; + $('#FeedDialog_Toolbar .badge').text('?'); + updateFilterButtons(undefined, undefined, undefined, false); + tableInitialized = false; + + $FeedDialog.modal({backdrop: 'static'}); + $FeedDialog.maximize({mini: UISettings.miniTheme}); + + $('.loading-block', $FeedDialog).show(); + + if (id > 0) + { + var name = Options.option('Feed' + id + '.Name'); + $('#FeedDialog_Title').text(name !== '' ? name : 'Feed'); + RPC.call('viewfeed', [id, false], itemsLoaded, feedFailure); + } + else + { + $('#FeedDialog_Title').text(name !== '' ? name : 'Feed Preview'); + var feedPauseNzb = pauseNzb === 'yes'; + var feedCategory = category; + var feedPriority = parseInt(priority); + RPC.call('previewfeed', [name, url, filter, feedPauseNzb, feedCategory, feedPriority, false, 0, ''], itemsLoaded, feedFailure); + } + } + + function feedFailure(res) + { + $FeedDialog.modal('hide'); + AlertDialog.showModal('Error', res); + } + + function disableAllButtons() + { + $('#FeedDialog .modal-footer .btn').attr('disabled', 'disabled'); + setTimeout(function() + { + $('#FeedDialog_Transmit').show(); + }, 500); + } + + function enableAllButtons() + { + $('#FeedDialog .modal-footer .btn').removeAttr('disabled'); + $('#FeedDialog_Transmit').hide(); + } + + function itemsLoaded(itemsArr) + { + $('.loading-block', $FeedDialog).hide(); + items = itemsArr; + updateTable(); + $('.modal-inner-scroll', $FeedDialog).scrollTop(100).scrollTop(0); + } + + function updateTable() + { + var countNew = 0; + var countFetched = 0; + var countBacklog = 0; + var differentFilenames = false; + + var data = []; + + for (var i=0; i < items.length; i++) + { + var item = items[i]; + + var age = Util.formatAge(item.Time + UISettings.timeZoneCorrection*60*60); + var size = (item.SizeMB > 0 || item.SizeLo > 0 || item.SizeHi > 0) ? Util.formatSizeMB(item.SizeMB, item.SizeLo) : ''; + + var status; + switch (item.Status) + { + case 'UNKNOWN': status = 'UNKNOWN'; break; + case 'BACKLOG': status = 'BACKLOG'; countBacklog +=1; break; + case 'FETCHED': status = 'FETCHED'; countFetched +=1; break; + case 'NEW': status = 'NEW'; countNew +=1; break; + default: status = 'internal error(' + item.Status + ')'; + } + + if (!(curFilter === item.Status || curFilter === 'ALL')) + { + continue; + } + + differentFilenames = differentFilenames || (item.Filename !== item.Title); + + var itemName = filenameMode ? item.Filename : item.Title; + var name = Util.textToHtml(itemName); + name = name.replace(/\./g, '.').replace(/_/g, '_'); + + var fields; + + if (!UISettings.miniTheme) + { + fields = ['
    ', status, name, item.Category, age, size]; + } + else + { + var info = '
    ' + name + '' + ' ' + status; + fields = [info]; + } + + var item = + { + id: item.URL, + item: item, + fields: fields, + search: item.Status + ' ' + itemName + ' ' + item.Category + ' ' + age + ' ' + size + }; + + data.push(item); + } + + $ItemTable.fasttable('update', data); + $ItemTable.fasttable('setCurPage', 1); + + Util.show('#FeedDialog_ItemTable_pagerBlock', data.length > pageSize); + updateFilterButtons(countNew, countFetched, countBacklog, differentFilenames); + } + + function itemsTableRenderCellCallback(cell, index, item) + { + if (index > 3) + { + cell.className = 'text-right'; + } + } + + function updateFilterButtons(countNew, countFetched, countBacklog, differentFilenames) + { + if (countNew != undefined) + { + $('#FeedDialog_Badge_ALL,#FeedDialog_Badge_ALL2').text(countNew + countFetched + countBacklog); + $('#FeedDialog_Badge_NEW,#FeedDialog_Badge_NEW2').text(countNew); + $('#FeedDialog_Badge_FETCHED,#FeedDialog_Badge_FETCHED2').text(countFetched); + $('#FeedDialog_Badge_BACKLOG,#FeedDialog_Badge_BACKLOG2').text(countBacklog); + } + + $('#FeedDialog_Toolbar .btn').removeClass('btn-inverse'); + $('#FeedDialog_Badge_' + curFilter + ',#FeedDialog_Badge_' + curFilter + '2').closest('.btn').addClass('btn-inverse'); + $('#FeedDialog_Toolbar .badge').removeClass('badge-active'); + $('#FeedDialog_Badge_' + curFilter + ',#FeedDialog_Badge_' + curFilter + '2').addClass('badge-active'); + + if (differentFilenames != undefined && !tableInitialized) + { + Util.show('#FeedDialog .FeedDialog-names', differentFilenames); + tableInitialized = true; + } + + $('#FeedDialog_Titles,#FeedDialog_Titles2').toggleClass('btn-inverse', !filenameMode); + $('#FeedDialog_Filenames,#FeedDialog_Filenames2').toggleClass('btn-inverse', filenameMode); + $('#FeedDialog_ItemTable_Name').text(filenameMode ? 'Filename' : 'Title'); + } + + this.fetch = function() + { + var checkedRows = $ItemTable.fasttable('checkedRows'); + if (checkedRows.length == 0) + { + Notification.show('#Notif_FeedDialog_Select'); + return; + } + + disableAllButtons(); + + var fetchItems = []; + for (var i = 0; i < items.length; i++) + { + var item = items[i]; + if (checkedRows.indexOf(item.URL) > -1) + { + fetchItems.push(item); + } + } + + fetchNextItem(fetchItems); + } + + function fetchNextItem(fetchItems) + { + if (fetchItems.length > 0) + { + var name = fetchItems[0].Filename; + if (name.substr(name.length-4, 4).toLowerCase() !== '.nzb') + { + name += '.nzb'; + } + RPC.call('append', [name, fetchItems[0].URL, fetchItems[0].AddCategory, fetchItems[0].Priority, false, + false, fetchItems[0].DupeKey, fetchItems[0].DupeScore, fetchItems[0].DupeMode], + function() + { + fetchItems.shift(); + fetchNextItem(fetchItems); + }) + } + else + { + $FeedDialog.modal('hide'); + Notification.show('#Notif_FeedDialog_Fetched'); + } + } + + this.filter = function(type) + { + curFilter = type; + updateTable(); + } + + this.setFilenameMode = function(mode) + { + filenameMode = mode; + updateTable(); + } +}(jQuery)); + + +/*** FEED FILTER DIALOG **********************************************/ + +var FeedFilterDialog = (new function($) +{ + 'use strict'; + + // Controls + var $FeedFilterDialog; + var $ItemTable; + var $Splitter; + var $FilterInput; + var $FilterBlock; + var $FilterLines; + var $FilterNumbers; + var $PreviewBlock; + var $ModalBody; + var $LoadingBlock; + var $CHAutoRematch; + var $RematchIcon; + + // State + var items = null; + var pageSize = 100; + var curFilter = 'ALL'; + var filenameMode = false; + var tableInitialized = false; + var saveCallback; + var splitStartPos; + var feedName; + var feedUrl; + var feedFilter; + var feedPauseNzb; + var feedCategory; + var feedPriority; + var cacheTimeSec; + var cacheId; + var updating; + var updateTimerIntitialized = false; + var autoUpdate = false; + var splitRatio; + var firstUpdate; + var lineNo; + var showLines; + + this.init = function() + { + $FeedFilterDialog = $('#FeedFilterDialog'); + $Splitter = $('#FeedFilterDialog_Splitter'); + $Splitter.mousedown(splitterMouseDown); + $('#FeedFilterDialog_Save').click(save); + $FilterInput = $('#FeedFilterDialog_FilterInput'); + $FilterBlock = $('#FeedFilterDialog_FilterBlock'); + $FilterLines = $('#FeedFilterDialog_FilterLines'); + $FilterNumbers = $('#FeedFilterDialog_FilterNumbers'); + $PreviewBlock = $('#FeedFilterDialog_PreviewBlock'); + $ModalBody = $('.modal-body', $FeedFilterDialog); + $LoadingBlock = $('.loading-block', $FeedFilterDialog); + $CHAutoRematch = $('#FeedFilterDialog_CHAutoRematch'); + $RematchIcon = $('#FeedFilterDialog_RematchIcon'); + + autoUpdate = UISettings.read('$FeedFilterDialog_AutoRematch', '1') == '1'; + updateRematchState(); + initLines(); + + $ItemTable = $('#FeedFilterDialog_ItemTable'); + $ItemTable.fasttable( + { + filterInput: '', + pagerContainer: '#FeedFilterDialog_ItemTable_pager', + filterCaseSensitive: false, + headerCheck: '', + pageSize: pageSize, + hasHeader: true, + renderCellCallback: itemsTableRenderCellCallback + }); + + $ItemTable.on('mousedown', Util.disableShiftMouseDown); + + $FilterInput.keypress(filterKeyPress); + + $FeedFilterDialog.on('hidden', function() + { + // cleanup + $ItemTable.fasttable('update', []); + $(window).off('resize', windowResized); + // resume updates + Refresher.resume(); + }); + + TabDialog.extend($FeedFilterDialog); + + if (UISettings.setFocus) + { + $FeedFilterDialog.on('shown', function() + { + $FilterInput.focus(); + }); + } + } + + this.showModal = function(name, url, filter, pauseNzb, category, priority, _saveCallback) + { + saveCallback = _saveCallback; + + Refresher.pause(); + + $ItemTable.fasttable('update', []); + + $FeedFilterDialog.restoreTab(); + $(window).on('resize', windowResized); + splitterRestore(); + + $('#FeedFilterDialog_ItemTable_pagerBlock').hide(); + $FilterInput.val(filter.replace(/\s*%\s*/g, '\n')); + + items = null; + firstUpdate = true; + curFilter = 'ALL'; + filenameMode = false; + tableInitialized = false; + $('#FeedFilterDialog_Toolbar .badge').text('?'); + updateFilterButtons(undefined, undefined, undefined, false); + tableInitialized = false; + + $FeedFilterDialog.modal({backdrop: 'static'}); + $FeedFilterDialog.maximize({mini: UISettings.miniTheme}); + + updateLines(); + $LoadingBlock.show(); + + $('#FeedFilterDialog_Title').text(name !== '' ? name : 'Feed Preview'); + feedName = name; + feedUrl = url; + feedFilter = filter; + feedPauseNzb = pauseNzb === 'yes'; + feedCategory = category; + feedPriority = parseInt(priority); + cacheId = '' + Math.random()*10000000; + cacheTimeSec = 60*10; // 10 minutes + + if (url !== '') + { + RPC.call('previewfeed', [name, url, filter, feedPauseNzb, feedCategory, feedPriority, true, cacheTimeSec, cacheId], itemsLoaded, feedFailure); + } + else + { + $LoadingBlock.hide(); + } + } + + this.rematch = function() + { + updateFilter(); + } + + function updateFilter() + { + if (feedUrl == '') + { + return; + } + + tableInitialized = false; + updating = true; + + var filter = $FilterInput.val().replace(/\n/g, '%'); + RPC.call('previewfeed', [feedName, feedUrl, filter, feedPauseNzb, feedCategory, feedPriority, true, cacheTimeSec, cacheId], itemsLoaded, feedFailure); + + setTimeout(function() + { + if (updating) + { + $LoadingBlock.show(); + } + }, 500); + } + + function feedFailure(msg, result) + { + updating = false; + var filter = $FilterInput.val().replace(/\n/g, ' % '); + if (firstUpdate && filter === feedFilter) + { + $FeedFilterDialog.modal('hide'); + } + $LoadingBlock.hide(); + AlertDialog.showModal('Error', result ? result.error.message : msg); + } + + function itemsLoaded(itemsArr) + { + updating = false; + $LoadingBlock.hide(); + items = itemsArr; + updateTable(); + if (firstUpdate) + { + $('.modal-inner-scroll', $FeedFilterDialog).scrollTop(100).scrollTop(0); + } + firstUpdate = false; + + if (!updateTimerIntitialized) + { + setupUpdateTimer(); + updateTimerIntitialized = true; + } + } + + function updateTable() + { + var countAccepted = 0; + var countRejected = 0; + var countIgnored = 0; + var differentFilenames = false; + + var filter = $FilterInput.val().split('\n'); + + var data = []; + + for (var i=0; i < items.length; i++) + { + var item = items[i]; + + var age = Util.formatAge(item.Time + UISettings.timeZoneCorrection*60*60); + var size = (item.SizeMB > 0 || item.SizeLo > 0 || item.SizeHi > 0) ? Util.formatSizeMB(item.SizeMB, item.SizeLo) : ''; + + var status; + switch (item.Match) + { + case 'ACCEPTED': + var addInfo = [item.AddCategory !== feedCategory ? 'category: ' + item.AddCategory : null, + item.Priority !== feedPriority ? DownloadsUI.buildPriorityText(item.Priority) : null, + item.PauseNzb !== feedPauseNzb ? (item.PauseNzb ? 'paused' : 'unpaused') : null, + item.DupeScore != 0 ? 'dupe-score: ' + item.DupeScore : null, + item.DupeKey !== '' ? 'dupe-key: ' + item.DupeKey : null, + item.DupeMode !== 'SCORE' ? 'dupe-mode: ' + item.DupeMode.toLowerCase() : null]. + filter(function(e){return e}).join('; '); + status = 'ACCEPTED'; + countAccepted += 1; + break; + case 'REJECTED': status = 'REJECTED'; countRejected += 1; break; + case 'IGNORED': status = 'IGNORED'; countIgnored += 1; break; + default: status = 'internal error(' + item.Match + ')'; break; + } + + if (!(curFilter === item.Match || curFilter === 'ALL')) + { + continue; + } + + differentFilenames = differentFilenames || (item.Filename !== item.Title); + + var itemName = filenameMode ? item.Filename : item.Title; + var name = Util.textToHtml(itemName); + name = name.replace(/\./g, '.').replace(/_/g, '_'); + + var rule = ''; + if (item.Rule > 0) + { + rule = ' ' + item.Rule + ' '; + } + + var fields; + + if (!UISettings.miniTheme) + { + fields = [status, rule, name, item.Category, age, size]; + } + else + { + var info = '' + name + '' + ' ' + status; + fields = [info]; + } + + var dataItem = + { + id: item.URL, + item: item, + fields: fields, + search: item.Match + ' ' + itemName + ' ' + item.Category + ' ' + age + ' ' + size + }; + + data.push(dataItem); + } + + $ItemTable.fasttable('update', data); + + Util.show('#FeedFilterDialog_ItemTable_pagerBlock', data.length > pageSize); + updateFilterButtons(countAccepted, countRejected, countIgnored, differentFilenames); + } + + function itemsTableRenderCellCallback(cell, index, item) + { + if (index > 3) + { + cell.className = 'text-right'; + } + } + + function updateFilterButtons(countAccepted, countRejected, countIgnored, differentFilenames) + { + if (countAccepted != undefined) + { + $('#FeedFilterDialog_Badge_ALL,#FeedFilterDialog_Badge_ALL2').text(countAccepted + countRejected + countIgnored); + $('#FeedFilterDialog_Badge_ACCEPTED,#FeedFilterDialog_Badge_ACCEPTED2').text(countAccepted); + $('#FeedFilterDialog_Badge_REJECTED,#FeedFilterDialog_Badge_REJECTED2').text(countRejected); + $('#FeedFilterDialog_Badge_IGNORED,#FeedFilterDialog_Badge_IGNORED2').text(countIgnored); + } + + $('#FeedFilterDialog_Toolbar .FeedFilterDialog-filter .btn').removeClass('btn-inverse'); + $('#FeedFilterDialog_Badge_' + curFilter + ',#FeedFilterDialog_Badge_' + curFilter + '2').closest('.btn').addClass('btn-inverse'); + $('#FeedFilterDialog_Toolbar .badge').removeClass('badge-active'); + $('#FeedFilterDialog_Badge_' + curFilter + ',#FeedFilterDialog_Badge_' + curFilter + '2').addClass('badge-active'); + + if (differentFilenames != undefined && !tableInitialized) + { + Util.show('#FeedFilterDialog .FeedFilterDialog-names', differentFilenames); + tableInitialized = true; + } + + $('#FeedFilterDialog_Titles,#FeedFilterDialog_Titles2').toggleClass('btn-inverse', !filenameMode); + $('#FeedFilterDialog_Filenames,#FeedFilterDialog_Filenames2').toggleClass('btn-inverse', filenameMode); + $('#FeedFilterDialog_ItemTable_Name').text(filenameMode ? 'Filename' : 'Title'); + } + + this.filter = function(type) + { + curFilter = type; + updateTable(); + } + + this.setFilenameMode = function(mode) + { + filenameMode = mode; + updateTable(); + } + + function save(e) + { + e.preventDefault(); + + $FeedFilterDialog.modal('hide'); + var filter = $FilterInput.val().replace(/\n/g, ' % '); + saveCallback(filter); + } + + function setupUpdateTimer() + { + // Create a timer which gets reset upon every keyup event. + // Perform filter only when the timer's wait is reached (user finished typing or paused long enough to elapse the timer). + // Do not perform the filter if the query has not changed. + + var timer; + var lastFilter = $FilterInput.val(); + + $FilterInput.keyup(function() + { + var timerCallback = function() + { + var value = $FilterInput.val(); + if (value != lastFilter) + { + lastFilter = value; + if (autoUpdate) + { + updateFilter(); + } + } + }; + + // Reset the timer + clearTimeout(timer); + timer = setTimeout(timerCallback, 500); + + return false; + }); + } + + this.autoRematch = function() + { + autoUpdate = !autoUpdate; + UISettings.write('$FeedFilterDialog_AutoRematch', autoUpdate ? '1' : '0'); + updateRematchState(); + if (autoUpdate) + { + updateFilter(); + } + } + + function updateRematchState() + { + Util.show($CHAutoRematch, autoUpdate); + $RematchIcon.toggleClass('icon-process', !autoUpdate); + $RematchIcon.toggleClass('icon-process-auto', autoUpdate); + } + + function filterKeyPress(event) + { + if (event.which == 37) + { + event.preventDefault(); + alert('Percent character (%) cannot be part of a filter because it is used\nas line separator when saving filter into configuration file.'); + } + } + + /*** SPLITTER ***/ + + function splitterMouseDown(e) + { + e.stopPropagation(); + e.preventDefault(); + splitStartPos = e.pageX; + $(document).bind("mousemove", splitterMouseMove).bind("mouseup", splitterMouseUp); + $ModalBody.css('cursor', 'col-resize'); + $FilterInput.css('cursor', 'col-resize'); + } + + function splitterMouseMove(e) + { + var newPos = e.pageX; + var right = $PreviewBlock.position().left + $PreviewBlock.width(); + newPos = newPos < 150 ? 150 : newPos; + newPos = newPos > right - 150 ? right - 150 : newPos; + splitterMove(newPos - splitStartPos); + splitStartPos = newPos; + } + + function splitterMouseUp(e) + { + $ModalBody.css('cursor', ''); + $FilterInput.css('cursor', ''); + $(document).unbind("mousemove", splitterMouseMove).unbind("mouseup", splitterMouseUp); + splitterSave(); + } + + function splitterMove(delta) + { + $FilterBlock.css('width', parseInt($FilterBlock.css('width')) + delta); + $PreviewBlock.css('left', parseInt($PreviewBlock.css('left')) + delta); + $Splitter.css('left', parseInt($Splitter.css('left')) + delta); + } + + function splitterSave() + { + if (!UISettings.miniTheme) + { + splitRatio = parseInt($FilterBlock.css('width')) / $(window).width(); + UISettings.write('$FeedFilterDialog_SplitRatio', splitRatio); + } + } + + function splitterRestore() + { + if (!UISettings.miniTheme) + { + var oldSplitRatio = parseInt($FilterBlock.css('width')) / $(window).width(); + splitRatio = UISettings.read('$FeedFilterDialog_SplitRatio', oldSplitRatio); + windowResized(); + } + } + + function windowResized() + { + if (!UISettings.miniTheme) + { + var oldWidth = parseInt($FilterBlock.css('width')); + var winWidth = $(window).width(); + var newWidth = Math.round(winWidth * splitRatio); + var right = winWidth - 30; + newWidth = newWidth > right - 150 ? right - 150 : newWidth; + newWidth = newWidth < 150 ? 150 : newWidth; + splitterMove(newWidth - oldWidth); + } + } + + /*** LINE SELECTION ***/ + + this.selectRule = function(rule) + { + selectTextareaLine($FilterInput[0], rule); + } + + function selectTextareaLine(tarea, lineNum) + { + lineNum--; // array starts at 0 + var lines = tarea.value.split("\n"); + + // calculate start/end + var startPos = 0, endPos = tarea.value.length; + for (var x = 0; x < lines.length; x++) + { + if (x == lineNum) + { + break; + } + startPos += (lines[x].length+1); + } + + var endPos = lines[lineNum].length+startPos; + + if (typeof(tarea.selectionStart) != "undefined") + { + tarea.focus(); + tarea.selectionStart = startPos; + tarea.selectionEnd = endPos; + } + } + + /*** LINE NUMBERS ***/ + + // Idea and portions of code from LinedTextArea plugin by Alan Williamson + // http://files.aw20.net/jquery-linedtextarea/jquery-linedtextarea.html + + function initLines() + { + showLines = !UISettings.miniTheme; + if (showLines) + { + lineNo = 1; + $FilterInput.scroll(updateLines); + } + } + + function updateLines() + { + if (!UISettings.miniTheme && showLines) + { + var domTextArea = $FilterInput[0]; + var scrollTop = domTextArea.scrollTop; + var clientHeight = domTextArea.clientHeight; + $FilterNumbers.css('margin-top', (-1*scrollTop) + "px"); + lineNo = fillOutLines(scrollTop + clientHeight, lineNo); + } + } + + function fillOutLines(h, lineNo) + { + while ($FilterNumbers.height() - h <= 0) + { + $FilterNumbers.append("
    " + lineNo + "
    "); + lineNo++; + } + return lineNo; + } +}(jQuery)); diff --git a/content/usr/local/share/nzbget/webui/history.js b/content/usr/local/share/nzbget/webui/history.js index 8a8e244..af37746 100644 --- a/content/usr/local/share/nzbget/webui/history.js +++ b/content/usr/local/share/nzbget/webui/history.js @@ -1,7 +1,7 @@ /* * This file is part of nzbget * - * Copyright (C) 2012-2013 Andrey Prygunkov + * Copyright (C) 2012-2014 Andrey Prygunkov * * 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 @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * $Revision: 683 $ - * $Date: 2013-05-16 22:54:13 +0200 (Thu, 16 May 2013) $ + * $Revision: 1142 $ + * $Date: 2014-10-12 16:23:54 +0200 (Sun, 12 Oct 2014) $ * */ @@ -44,6 +44,9 @@ var History = (new function($) var history; var notification = null; var updateTabInfo; + var curFilter = 'ALL'; + var activeTab = false; + var showDup = false; this.init = function(options) { @@ -69,6 +72,7 @@ var History = (new function($) maxPages: UISettings.miniTheme ? 1 : 5, pageDots: !UISettings.miniTheme, fillFieldsCallback: fillFieldsCallback, + filterCallback: filterCallback, renderCellCallback: renderCellCallback, updateInfoCallback: updateInfo }); @@ -87,9 +91,26 @@ var History = (new function($) UISettings.miniTheme ? 1 : 5, !UISettings.miniTheme); } + this.show = function() + { + activeTab = true; + this.redraw(); + } + + this.hide = function() + { + activeTab = false; + } + this.update = function() { - RPC.call('history', [], loaded); + if (!history) + { + $('#HistoryTable_Category').css('width', DownloadsUI.calcCategoryColumnWidth()); + initFilterButtons(); + } + + RPC.call('history', [showDup], loaded); } function loaded(curHistory) @@ -103,50 +124,22 @@ var History = (new function($) { for (var j=0, jl=history.length; j < jl; j++) { - detectStatus(history[j]); - } - } - - function detectStatus(hist) - { - if (hist.Kind === 'NZB') - { - if (hist.ParStatus == 'FAILURE' || hist.UnpackStatus == 'FAILURE' || hist.MoveStatus == 'FAILURE' || hist.ScriptStatus == 'FAILURE') + var hist = history[j]; + if (hist.Status === 'DELETED/MANUAL') { - hist.status = 'failure'; + hist.FilterKind = 'DELETED'; } - else if (hist.ParStatus == 'MANUAL') + else if (hist.Status === 'DELETED/DUPE') { - hist.status = 'damaged'; + hist.FilterKind = 'DUPE'; } - else + else if (hist.Status.substring(0, 7) === 'SUCCESS') { - switch (hist.ScriptStatus) - { - case 'SUCCESS': hist.status = 'success'; break; - case 'UNKNOWN': hist.status = 'unknown'; break; - case 'NONE': - switch (hist.UnpackStatus) - { - case 'SUCCESS': hist.status = 'success'; break; - case 'NONE': - switch (hist.ParStatus) - { - case 'SUCCESS': hist.status = 'success'; break; - case 'REPAIR_POSSIBLE': hist.status = 'repairable'; break; - case 'NONE': hist.status = 'unknown'; break; - } - } - } + hist.FilterKind = 'SUCCESS'; } - } - else if (hist.Kind === 'URL') - { - switch (hist.UrlStatus) + else { - case 'SUCCESS': hist.status = 'success'; break; - case 'FAILURE': hist.status = 'failure'; break; - case 'UNKNOWN': hist.status = 'unknown'; break; + hist.FilterKind = 'FAILURE'; } } } @@ -160,23 +153,39 @@ var History = (new function($) var hist = history[i]; var kind = hist.Kind; - var statustext = hist.status === 'none' ? 'unknown' : hist.status; - var size = kind === 'NZB' ? Util.formatSizeMB(hist.FileSizeMB) : ''; + var statustext = HistoryUI.buildStatusText(hist); + var size = kind === 'URL' ? '' : Util.formatSizeMB(hist.FileSizeMB); + var time = Util.formatDateTime(hist.HistoryTime + UISettings.timeZoneCorrection*60*60); + var dupe = DownloadsUI.buildDupeText(hist.DupeKey, hist.DupeScore, hist.DupeMode); + var category = ''; var textname = hist.Name; - if (kind === 'URL') + var age = ''; + if (kind === 'NZB') { + age = Util.formatAge(hist.MinPostTime + UISettings.timeZoneCorrection*60*60); textname += ' URL'; } + else if (kind === 'URL') + { + textname += ' URL'; + } + else if (kind === 'DUP') + { + textname += ' hidden'; + } - var time = Util.formatDateTime(hist.HistoryTime + UISettings.timeZoneCorrection*60*60); + if (kind !== 'DUP') + { + category = hist.Category; + } var item = { id: hist.ID, hist: hist, - data: {time: time, size: size}, - search: statustext + ' ' + time + ' ' + textname + ' ' + hist.Category + ' ' + size + data: { time: time, age: age, size: size }, + search: statustext + ' ' + time + ' ' + textname + ' ' + dupe + ' ' + category + ' ' + age + ' ' + size }; data.push(item); @@ -192,23 +201,33 @@ var History = (new function($) { var hist = item.hist; - var status = HistoryUI.buildStatus(hist.status, ''); + var status = HistoryUI.buildStatus(hist); var name = '' + Util.textToHtml(Util.formatNZBName(hist.Name)) + ''; - var category = Util.textToHtml(hist.Category); + var dupe = DownloadsUI.buildDupe(hist.DupeKey, hist.DupeScore, hist.DupeMode); + var category = ''; + + if (hist.Kind !== 'DUP') + { + var category = Util.textToHtml(hist.Category); + } if (hist.Kind === 'URL') { name += ' URL'; } + else if (hist.Kind === 'DUP') + { + name += ' hidden'; + } if (!UISettings.miniTheme) { - item.fields = ['
    ', status, item.data.time, name, category, item.data.size]; + item.fields = ['
    ', status, item.data.time, name + dupe, category, item.data.age, item.data.size]; } else { - var info = '
    ' + name + '' + + var info = '
    ' + name + '' + dupe + ' ' + status + ' ' + item.data.time + ''; if (category) { @@ -228,7 +247,7 @@ var History = (new function($) { cell.className = 'text-center'; } - else if (index === 5) + else if (index === 5 || index === 6) { cell.className = 'text-right'; } @@ -244,56 +263,51 @@ var History = (new function($) function updateInfo(stat) { updateTabInfo($HistoryTabBadge, stat); + if (activeTab) + { + updateFilterButtons(); + } } this.deleteClick = function() { - if (history.length == 0) + var checkedRows = $HistoryTable.fasttable('checkedRows'); + if (checkedRows.length == 0) { + Notification.show('#Notif_History_Select'); return; } - var checkedRows = $HistoryTable.fasttable('checkedRows'); - if (checkedRows.length > 0) + var hasNzb = false; + var hasDup = false; + var hasFailed = false; + for (var i = 0; i < history.length; i++) { - ConfirmDialog.showModal('HistoryDeleteConfirmDialog', historyDelete); - } - else - { - ConfirmDialog.showModal('HistoryClearConfirmDialog', historyClear); + var hist = history[i]; + if (checkedRows.indexOf(hist.ID) > -1) + { + hasNzb |= hist.Kind === 'NZB'; + hasDup |= hist.Kind === 'DUP'; + hasFailed |= hist.ParStatus === 'FAILURE' || hist.UnpackStatus === 'FAILURE'; + } } + + HistoryUI.deleteConfirm(historyDelete, hasNzb, hasDup, hasFailed, true); } - function historyDelete() + function historyDelete(command) { Refresher.pause(); var IDs = $HistoryTable.fasttable('checkedRows'); - RPC.call('editqueue', ['HistoryDelete', 0, '', [IDs]], function() + RPC.call('editqueue', [command, 0, '', [IDs]], function() { notification = '#Notif_History_Deleted'; editCompleted(); }); } - function historyClear() - { - Refresher.pause(); - - var IDs = []; - for (var i=0; i' + prefix + 'success'; - case 'failure': + return detail === 'GOOD' ? 'GOOD' : 'SUCCESS'; case 'FAILURE': - return '' + prefix + 'failure'; - case 'unknown': - case 'UNKNOWN': - return '' + prefix + 'unknown'; - case 'repairable': - case 'REPAIR_POSSIBLE': - return '' + prefix + 'repairable'; - case 'manual': - case 'MANUAL': - case 'damaged': - return '' + prefix + status + ''; - case 'none': - case 'NONE': - return '' + prefix + 'none'; + return detail === 'BAD' ? 'BAD' : (status === 'FAILURE/INTERNAL_ERROR' ? 'INTERNAL_ERROR' : 'FAILURE'); + case 'WARNING': + return detail === 'SCRIPT' ? 'PP-FAILURE' : detail; + case 'DELETED': + return detail === 'MANUAL' ? 'DELETED' : detail; default: - return '' + prefix + status + ''; + return 'INTERNAL_ERROR (' + status + ')'; + } + } + + this.buildStatus = function(hist) + { + var total = hist.Status.substring(0, 7); + var statusText = HistoryUI.buildStatusText(hist); + var badgeClass = ''; + switch (total) + { + case 'SUCCESS': + badgeClass = 'label-success'; break; + case 'FAILURE': + badgeClass = 'label-important'; break; + case 'WARNING': + badgeClass = 'label-warning'; break; } + return '' + statusText + ''; } + this.deleteConfirm = function(actionCallback, hasNzb, hasDup, hasFailed, multi) + { + var dupeCheck = Options.option('DupeCheck') === 'yes'; + var cleanupDisk = Options.option('DeleteCleanupDisk') === 'yes'; + var dialog = null; + + function init(_dialog) + { + dialog = _dialog; + + if (!multi) + { + var html = $('#ConfirmDialog_Text').html(); + html = html.replace(/records/g, 'record'); + $('#ConfirmDialog_Text').html(html); + } + + $('#HistoryDeleteConfirmDialog_Hide', dialog).prop('checked', true); + Util.show($('#HistoryDeleteConfirmDialog_Options', dialog), hasNzb && dupeCheck); + Util.show($('#HistoryDeleteConfirmDialog_Simple', dialog), !(hasNzb && dupeCheck)); + Util.show($('#HistoryDeleteConfirmDialog_DeleteWillCleanup', dialog), hasNzb && hasFailed && cleanupDisk); + Util.show($('#HistoryDeleteConfirmDialog_DeleteCanCleanup', dialog), hasNzb && hasFailed && !cleanupDisk); + Util.show($('#HistoryDeleteConfirmDialog_DeleteNoCleanup', dialog), !(hasNzb && hasFailed)); + Util.show($('#HistoryDeleteConfirmDialog_DupAlert', dialog), !hasNzb && dupeCheck && hasDup); + Util.show('#ConfirmDialog_Help', hasNzb && dupeCheck); + }; + + function action() + { + var hide = $('#HistoryDeleteConfirmDialog_Hide', dialog).is(':checked'); + var command = hasNzb && hide ? 'HistoryDelete' : 'HistoryFinalDelete'; + actionCallback(command); + } + + ConfirmDialog.showModal('HistoryDeleteConfirmDialog', action, init); + } + }(jQuery)); diff --git a/content/usr/local/share/nzbget/webui/img/icons-2x.png b/content/usr/local/share/nzbget/webui/img/icons-2x.png index 71bda84..3a658b5 100644 Binary files a/content/usr/local/share/nzbget/webui/img/icons-2x.png and b/content/usr/local/share/nzbget/webui/img/icons-2x.png differ diff --git a/content/usr/local/share/nzbget/webui/img/icons.png b/content/usr/local/share/nzbget/webui/img/icons.png index 85b313e..5dc891e 100644 Binary files a/content/usr/local/share/nzbget/webui/img/icons.png and b/content/usr/local/share/nzbget/webui/img/icons.png differ diff --git a/content/usr/local/share/nzbget/webui/index.html b/content/usr/local/share/nzbget/webui/index.html index 2860222..1c762f5 100644 --- a/content/usr/local/share/nzbget/webui/index.html +++ b/content/usr/local/share/nzbget/webui/index.html @@ -2,7 +2,7 @@ @@ -38,6 +38,8 @@ + + @@ -50,6 +52,7 @@ + @@ -95,12 +98,10 @@ @@ -1315,7 +2091,7 @@

    Choose scripts

    -

    Select scripts for option DefScript

    +

    Select scripts for option PostScript

    @@ -1326,16 +2102,16 @@

    Choose scripts

    The script execution order is saved globally in the option ScriptOrder and affects all categories as well as the order of scripts in the edit download dialog.

    - +
    - + @@ -1361,20 +2137,20 @@

    Restore Settings

    - + -
    + + + + + + + + + + + + + + + + + + + + + +
    StableTestingDevelopment
    Official releases11.0
    + release notes
    12.0-r870
    + release notes
    12.0-r880
    + code log
    For your platform
    + package info
    + Install

    + package info
    + Install

    + package info
    + Install
    + +
    + +
    + + + + + + + + +
    @@ -1428,9 +2318,9 @@

    Help: New configuration option

    -

    Reloading NZBGet

    +

    Reloading NZBGet

    - Stopping all activities and reloading... + Stopping all activities and reloading...

    Should this take too long: @@ -1441,6 +2331,21 @@

    Reloading NZBGet

    + + +
    +
    Shutdown
    +
    +

    + Shutdown NZBGet? +

    +

    + The program will be stopped. You will no longer be able to access or start it via web-interface. Make sure you know how to start the program again. +

    +
    +
    Shutdown
    +
    +
    @@ -1511,6 +2416,10 @@

    Reloading NZBGet

    Post-processing-downloads cannot be edited
    +
    + URLs cannot be merged or paused +
    +
    Please select records first
    @@ -1539,6 +2448,26 @@

    Reloading NZBGet

    Saved
    +
    + Please select records first +
    + +
    + Marked +
    + +
    + Fetching new items +
    + +
    + No records selected +
    + +
    + Fetching items +
    +
    Nothing to save
    No changes have been made
    @@ -1560,9 +2489,21 @@

    Reload command has been sent

    Restoring settings... +
    + Could not start update script +
    +
    Debug
    +
    + Incorrect period +
    + +
    + Volume reset +
    + diff --git a/content/usr/local/share/nzbget/webui/index.js b/content/usr/local/share/nzbget/webui/index.js index f8622cc..339e4da 100644 --- a/content/usr/local/share/nzbget/webui/index.js +++ b/content/usr/local/share/nzbget/webui/index.js @@ -1,7 +1,7 @@ /* * This file is part of nzbget * - * Copyright (C) 2012 Andrey Prygunkov + * Copyright (C) 2012-2014 Andrey Prygunkov * * 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 @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * $Revision: 698 $ - * $Date: 2013-06-02 21:20:31 +0200 (Sun, 02 Jun 2013) $ + * $Revision: 973 $ + * $Date: 2014-04-01 23:06:31 +0200 (Tue, 01 Apr 2014) $ * */ @@ -50,11 +50,14 @@ var UISettings = (new function($) this.slideAnimation = true; // Automatically set focus to the first control in dialogs. - // Not good on touch devices, because may pop up the on-screen-keyboard. + // Not good on touch devices, because may pop up an on-screen-keyboard. this.setFocus = false; // Show popup notifications. - this.showNotifications = true; + this.notifications = true; + + // Show badges with duplicate info (downloads and history). + this.dupeBadges = false; // Time zone correction in hours. // You shouldn't require this unless you can't set the time zone on your computer/device properly. @@ -130,6 +133,8 @@ var Frontend = (new function($) var mobileSafari = false; var scrollbarWidth = 0; var switchingTheme = false; + var activeTab = 'Downloads'; + var lastTab = ''; this.init = function() { @@ -155,11 +160,17 @@ var Frontend = (new function($) Messages.init({ updateTabInfo: updateTabInfo }); History.init({ updateTabInfo: updateTabInfo }); Upload.init(); + Feeds.init(); + FeedDialog.init(); + FeedFilterDialog.init(); Config.init({ updateTabInfo: updateTabInfo }); ConfigBackupRestore.init(); ConfirmDialog.init(); + UpdateDialog.init(); + AlertDialog.init(); ScriptListDialog.init(); RestoreSettingsDialog.init(); + LimitDialog.init(); DownloadsEditDialog.init(); DownloadsMultiDialog.init(); @@ -233,6 +244,7 @@ var Frontend = (new function($) if (firstLoad) { + Feeds.redraw(); $('#FirstUpdateInfo').hide(); $('#Navbar').show(); $('#MainTabContent').show(); @@ -246,15 +258,38 @@ var Frontend = (new function($) { var tabname = $(e.target).attr('href'); tabname = tabname.substr(1, tabname.length - 4); + + if (activeTab === 'Config' && !Config.canLeaveTab(e.target)) + { + e.preventDefault(); + return; + } + + lastTab = activeTab; + activeTab = tabname; + $('#SearchBlock .search-query, #SearchBlock .search-clear').hide(); - $('#' + tabname + 'Table_filter, #' + tabname + 'Table_clearfilter').show(); + $('#' + activeTab + 'Table_filter, #' + activeTab + 'Table_clearfilter').show(); + + switch (activeTab) + { + case 'Config': Config.show(); break; + case 'Messages': Messages.show(); break; + case 'History': History.show(); break; + } } function afterTabShow(e) { - if ($(e.target).attr('href') !== '#ConfigTab') + switch (lastTab) { - Config.cleanup(); + case 'Config': Config.hide(); break; + case 'Messages': Messages.hide(); break; + case 'History': History.hide(); break; + } + switch (activeTab) + { + case 'Config': Config.shown(); break; } } @@ -302,17 +337,22 @@ var Frontend = (new function($) resizeNavbar(); - if (UISettings.miniTheme) + alignPopupMenu('#PlayMenu'); + alignPopupMenu('#RefreshMenu'); + alignPopupMenu('#RssMenu'); + alignPopupMenu('#StatDialog_MonthMenu', true); + + alignCenterDialogs(); + + if (initialized) { - centerPopupMenu('#PlayMenu', true); - centerPopupMenu('#RefreshMenu', true); + Downloads.resize(); } - - centerCenterDialogs(); } - function centerPopupMenu(menu, center) + function alignPopupMenu(menu, right) { + var center = UISettings.miniTheme; var $elem = $(menu); if (center) { @@ -336,10 +376,21 @@ var Frontend = (new function($) top: '', right: '' }); + var off = $elem.parent().offset(); + if (off.left + $elem.outerWidth() > $(window).width()) + { + var left = $(window).width() - $elem.outerWidth() - off.left; + $elem.css({ left: left }); + } + if (right) + { + $elem.addClass('pull-right'); + } } } - - function centerCenterDialogs() + this.alignPopupMenu = alignPopupMenu; + + function alignCenterDialogs() { $.each($('.modal-center'), function(index, element) { Util.centerDialog(element, true); @@ -392,15 +443,7 @@ var Frontend = (new function($) function updateTabInfo(control, stat) { - if (stat.filter) - { - control.removeClass('badge-info').addClass('badge-warning'); - } - else - { - control.removeClass('badge-warning').addClass('badge-info'); - } - + control.toggleClass('badge-info', stat.available == stat.total).toggleClass('badge-warning', stat.available != stat.total); control.html(stat.available); control.toggleClass('badge2', stat.total > 9); control.toggleClass('badge3', stat.total > 99); @@ -425,8 +468,10 @@ var Frontend = (new function($) $('#DownloadsTable').toggleClass('table-check', !UISettings.miniTheme || UISettings.showEditButtons); $('#HistoryTable').toggleClass('table-check', !UISettings.miniTheme || UISettings.showEditButtons); - centerPopupMenu('#PlayMenu', UISettings.miniTheme); - centerPopupMenu('#RefreshMenu', UISettings.miniTheme); + alignPopupMenu('#PlayMenu'); + alignPopupMenu('#RefreshMenu'); + alignPopupMenu('#RssMenu'); + alignPopupMenu('#StatDialog_MonthMenu', true); if (UISettings.miniTheme) { @@ -434,6 +479,7 @@ var Frontend = (new function($) $('#DownloadsRecordsPerPageBlock').appendTo($('#DownloadsRecordsPerPageBlockPhone')); $('#HistoryRecordsPerPageBlock').appendTo($('#HistoryRecordsPerPageBlockPhone')); $('#MessagesRecordsPerPageBlock').appendTo($('#MessagesRecordsPerPageBlockPhone')); + $('#StatDialog_MonthMenu').appendTo($('#StatDialog_MonthBlockPhone')); } else { @@ -441,6 +487,7 @@ var Frontend = (new function($) $('#DownloadsRecordsPerPageBlock').appendTo($('#DownloadsTableTopBlock')); $('#HistoryRecordsPerPageBlock').appendTo($('#HistoryTableTopBlock')); $('#MessagesRecordsPerPageBlock').appendTo($('#MessagesTableTopBlock')); + $('#StatDialog_MonthMenu').appendTo($('#StatDialog_MonthBlockTop')); } if (initialized) @@ -732,14 +779,30 @@ var ConfirmDialog = (new function($) $('#ConfirmDialog_OK').click(click); } - this.showModal = function(id, callback) + this.showModal = function(id, _actionCallback, initCallback) { $('#ConfirmDialog_Title').html($('#' + id + '_Title').html()); $('#ConfirmDialog_Text').html($('#' + id + '_Text').html()); $('#ConfirmDialog_OK').html($('#' + id + '_OK').html()); + var helpId = $('#' + id + '_Help').html(); + $('#ConfirmDialog_Help').attr('href', '#' + helpId); + Util.show('#ConfirmDialog_Help', helpId !== null); + + actionCallback = _actionCallback; + if (initCallback) + { + initCallback($ConfirmDialog); + } + Util.centerDialog($ConfirmDialog, true); - actionCallback = callback; - $ConfirmDialog.modal(); + $ConfirmDialog.modal({backdrop: 'static'}); + + // avoid showing multiple backdrops when the modal is shown from other modal + var backdrops = $('.modal-backdrop'); + if (backdrops.length > 1) + { + backdrops.last().remove(); + } } function hidden() @@ -754,12 +817,36 @@ var ConfirmDialog = (new function($) function click(event) { event.preventDefault(); // avoid scrolling - actionCallback(); + actionCallback($ConfirmDialog); $ConfirmDialog.modal('hide'); } }(jQuery)); +/*** ALERT DIALOG *****************************************************/ + +var AlertDialog = (new function($) +{ + 'use strict'; + + // Controls + var $AlertDialog; + + this.init = function() + { + $AlertDialog = $('#AlertDialog'); + } + + this.showModal = function(title, text) + { + $('#AlertDialog_Title').html(title); + $('#AlertDialog_Text').html(text); + Util.centerDialog($AlertDialog, true); + $AlertDialog.modal(); + } +}(jQuery)); + + /*** NOTIFICATIONS *********************************************************/ var Notification = (new function($) @@ -768,7 +855,7 @@ var Notification = (new function($) this.show = function(alert, completeFunc) { - if (UISettings.showNotifications || $(alert).hasClass('alert-error')) + if (UISettings.notifications || $(alert).hasClass('alert-error')) { $(alert).animate({'opacity':'toggle'}); var duration = $(alert).attr('data-duration'); diff --git a/content/usr/local/share/nzbget/webui/lib/elycharts.js b/content/usr/local/share/nzbget/webui/lib/elycharts.js new file mode 100644 index 0000000..bf159e9 --- /dev/null +++ b/content/usr/local/share/nzbget/webui/lib/elycharts.js @@ -0,0 +1,3987 @@ +/********* Source File: extra-resources/header.js*********/ +/*!********************************************************************* + * ELYCHARTS v2.1.6-SNAPSHOT (2014-02-19) $Id$ + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ +/********* Source File: src/elycharts_defaults.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { +if (!$.elycharts) + $.elycharts = {}; + +/*********************************************************************** + * DEFAULT OPTIONS + **********************************************************************/ + +$.elycharts.templates = { + + common : { + // Tipo di grafico + // type : 'line|pie|funnel|barline' + + // Permette di specificare una configurazione di default da utilizzare (definita in $.elycharts.templates.NOME) + // La configurazione completa � quindi data da tutti i valori della conf di default alla quale viene unita (con sovrascrittura) la conf corrente + // Il parametro � ricorsivo (la configurazione di default puo' a sua volta avere una configurazione di default) + // Se non specificato, la configurazione di default � quella con lo stesso nome del tipo di grafico + // template : 'NOME', + + /* DATI: + // I valori associati a ogni serie del grafico. Ogni serie � associata a una chiave dell'oggetto value, il cui + // valore � l'array di dati relativi + values : {}, + + // Label associate ai valori del grafico + // Solo in caso di label gestite da labelmanager (quindi per pie e funnel) e per label.html = true e' possibile inserire + // degli elementi DOM/JQUERY che verranno presi e posizionati correttament. + labels : [], + + // Anchor per la gestione mediante anchormanager. Possono essere stringhe e oggetti DOM/JQUERY che verranno riposizionati + anchors : {}, + + tooltips : {}, + + legend : [], + */ + + // Autoresize uses jQuery resize event to automatically resize the chart to the container + // autoresize makes sense only when width or height is not defined. + // autoresize: false, + + // Per impostare una dimensione diversa da quella del container settare width e height + //width : x, + //height : y + + // I margini del grafico rispetto al frame complessivo. Da notare che riguardano la posizione del grafico + // principale, e NON degli elementi aggiuntivi (legenda, label e titoli degli assi...). Quindi i margini devono + // essere impostati in genere proprio per lasciare lo spazio per questi elementi + // Sintassi: [top, right, bottom, left] + margins: [10, 10, 10, 10], + + // style : {}, + + // Per gestire al meglio l'interattivita' del grafico (tooltip, highlight, anchor...) viene inserito un secondo + // layer per le parti sensibili al mouse. Se si sa che il grafico non avra' alcuna interattivita' si puo' impostare + // questo valore a false per evitare di creare il layer (ottimizzando leggermente la pagina) + interactive : true, + + // Dati da applicare a tutte le serie del grafico + defaultSeries : { + // Impostare a false per disabilitare la visualizzazione della serie + visible : true, + + // Impostare color qui permette di impostare velocemente plotProps.stroke+fill, tooltip.frameProps.stroke, dotProps.stroke e fillProps.fill (se non specificati) + //color: 'blue', + + //plotProps : { }, + + // Impostazioni dei tooltip + tooltip : { + active : true, + // Se width ed height vengono impostati a 0 o ad "auto" (equivalenti) non vengono fissate dimensioni, quindi il contenuto si autodimensiona in funzione del tooltip + // Impostare a 0|auto � incompatibile con il frame SVG, quindi viene automaticamente disabilitato (come se frameProps = false) + width: 100, height: 50, + roundedCorners: 5, + padding: [6, 6] /* y, x */, + offset: [20, 0] /* y, x */, + // Se frameProps = false non disegna la cornice del tooltip (ad es. per permettere di definire la propria cornice HTML) + frameProps : { fill: "white", "stroke-width": 2 }, + contentStyle : { "font-family": "Arial", "font-size": "12px", "line-height": "16px", color: "black" } + }, + + // Highlight feature + highlight : { + // Cambia le dimensioni dell'elemento quando deve essere evidenziato + //scale : [x, y], + // Opzioni di animazione effetto "scale" + scaleSpeed : 100, scaleEasing : '', + // Cambia gli attributi dell'elemento quando evidenziato + //newProps : { opacity : 1 }, + // Inserisce un layer con gli attributi specificati sopra quello da evidenziare + //overlayProps : {"fill" : "white", "fill-opacity" : .3, "stroke-width" : 0} + // Muove l'area evidenziata. E' possibile specificare un valore X o un array [X, Y] + //move : 10, + // Opzioni di animazione effetto "move" + moveSpeed : 100, moveEasing : '', + // Opzioni di animazione da usare per riportare l'oggetto alle situazione iniziale + restoreSpeed : 0, restoreEasing : '' + }, + + anchor : { + // Aggiunge alle anchor esterne la classe selezionata quando il mouse passa sull'area + //addClass : "", + // Evidenzia la serie al passaggio del mouse + //highlight : "", + // Se impostato a true usa gli eventi mouseenter/mouseleave invece di mouseover/mouseout per l'highlight + //useMouseEnter : false, + }, + + // Opzioni per la generazione animata dei grafici + startAnimation : { + //active : true, + type : 'simple', + speed : 600, + delay : 0, + propsFrom : {}, // applicate a tutte le props di plot + propsTo : {}, // applicate a tutte le props di plot + easing : '' // easing raphael: >, <, <>, backIn, backOut, bounce, elastic + + // Opzionale per alcune animazioni, permette di specificare un sotto-tipo + // subType : 0|1|2 + }, + + // Opzioni per le transizioni dei grafici durante un cambiamento di configurazione + /* stepAnimation : { + speed : 600, + delay : 0, + easing : '' // easing raphael: >, <, <>, backIn, backOut, bounce, elastic + },*/ + + label : { + // Disegna o meno la label interna al grafico + active : false, + // Imposta un offset [X,Y] per la label (le coordinate sono relative al sistema di assi dello specifico settore disegnato. + // Ad es. per il piechart la X � la distanza dal centro, la Y lo spostamento ortogonale + //offset : [x, y], + html : false, + // Proprieta' della label (per HTML = false) + props : { fill: 'black', stroke: "none", "font-family": 'Arial', "font-size": "16px" }, + // Stile CSS della label (per HTML = true) + style : { cursor : 'default' } + // Posizionamento della label rispetto al punto centrale (+offset) identificato + //frameAnchor : ['start|middle|end', 'top|middle|bottom'] + } + + /*legend : { + dotType : 'rect', + dotWidth : 10, dotHeight : 10, dotR : 4, + dotProps : { }, + textProps : { font: '12px Arial', fill: "#000" } + }*/ + }, + + series : { + // Serie specifica usata quando ci sono "dati vuoti" (ad esempio quando un piechart e' a 0) + empty : { + //plotProps : { fill : "#D0D0D0" }, + label : { active : false }, + tooltip : { active : false } + } + /*root : { + values : [] + }*/ + }, + + features : { + tooltip : { + // Imposta una posizione fissa per tutti i tooltip + //fixedPos : [ x, y] + // Velocita' del fade + fadeDelay : 100, + // Velocita' dello spostamento del tip da un'area all'altra + moveDelay : 300 + // E' possibile specificare una funzione che filtra le coordinate del tooltip prima di mostrarlo, permettendo di modificarle + // Nota: le coordinate del mouse sono in mouseAreaData.event.pageX/pageY, e nel caso va ritornato [mouseAreaData.event.pageX, mouseAreaData.event.pageY, true] per indicare che il sistema e' relativo alla pagina) + //positionHandler : function(env, tooltipConf, mouseAreaData, suggestedX, suggestedY) { return [suggestedX, suggestedY] } + }, + mousearea : { + // 'single' le aree sensibili sono relative a ogni valore di ogni serie, se 'index' il mouse attiva tutte le serie per un indice + type : 'single', + // In caso di type = 'index', indica se le aree si basano sulle barre ('bar') o sui punti di una linea ('line'). Specificare 'auto' per scegliere automaticamente + indexCenter : 'auto', + // Quanto tempo puo' passare nel passaggio da un'area all'altra per considerarlo uno spostamento di puntatore + areaMoveDelay : 500, + // Se diversi chart specificano lo stesso syncTag quando si attiva l'area di uno si disattivano quelle degli altri + syncTag: false, + // Callback for mouse actions. Parameters passed: (env, serie, index, mouseAreaData) + onMouseEnter : false, + onMouseExit : false, + onMouseChanged : false, + onMouseOver : false, + onMouseOut : false + }, + highlight : { + // Evidenzia tutto l'indice con una barra ("bar"), una linea ("line") o una linea centrata sulle barre ("barline"). Se "auto" decide in autonomia tra bar e line + //indexHighlight : 'barline', + indexHighlightProps : { opacity : 1 /*fill : 'yellow', opacity : .3, scale : ".5 1"*/ } + }, + animation : { + // Valore di default per la generazione animata degli elementi del grafico (anche per le non-serie: label, grid...) + startAnimation : { + //active : true, + //propsFrom : {}, // applicate a tutte le props di plot + //propsTo : {}, // applicate a tutte le props di plot + speed : 600, + delay : 0, + easing : '' // easing raphael: >, <, <>, backIn, backOut, bounce, elastic + }, + // Valore di default per la transizione animata degli elementi del grafico (anche per le non-serie: label, grid...) + stepAnimation : { + speed : 600, + delay : 0, + easing : '' // easing raphael: >, <, <>, backIn, backOut, bounce, elastic + } + }, + frameAnimation : { + active : false, + cssFrom : { opacity : 0}, + cssTo : { opacity: 1 }, + speed : 'slow', + easing : 'linear' // easing jQuery: 'linear' o 'swing' + }, + // used to be true + pixelWorkAround : { + active : Raphael.svg + }, + label : {}, + shadows : { + active : false, + offset : [2, 2], // Per attivare l'ombra, [y, x] + props : {"stroke-width": 0, "stroke-opacity": 0, "fill": "black", "fill-opacity": .3} + }, + // BALLOONS: Applicabile solo al funnel (per ora) + balloons : { + active : false, + // Width: se non specificato e' automatico + //width : 200, + // Height: se non specificato e' automatico + //height : 50, + // Lo stile CSS da applicare a ogni balloon + style : { }, + // Padding + padding : [ 5, 5 ], + // La distanza dal bordo sinistro + left : 10, + // Percorso della linea: [ [ x, y iniziali (rispetto al punto di inizio standard)], ... [x, y intermedi (rispetto al punto di inizio standard)] ..., [x, y finale (rispetto all'angolo del balloon pi� vicino al punto di inizio)] ] + line : [ [ 0, 0 ], [0, 0] ], + // Propriet� della linea + lineProps : { } + }, + legend : { + horizontal : false, + x : 'auto', // X | auto, (auto solo per horizontal = true) + y : 10, + width : 'auto', // X | auto, (auto solo per horizontal = true) + height : 20, + itemWidth : "fixed", // fixed | auto, solo per horizontal = true + margins : [0, 0, 0, 0], + dotMargins : [10, 5], // sx, dx + borderProps : { fill : "white", stroke : "black", "stroke-width" : 1 }, + dotType : 'rect', + dotWidth : 10, dotHeight : 10, + // radius for the dots (used to be 4 but there also was a bug preventing radius support, so moved to 0) + dotR : 0, + dotProps : { type : "rect", width : 10, height : 10 }, + textProps : { font: '12px Arial', fill: "#000" } + }, + debug : { + active : false + } + }, + + enableInternalCaching : true, + + nop : 0 + }, + + line : { + template : 'common', + + // absolute margin left to both sides of each column / column group. + barMargins : 0, + // overlap between additional columns over the previous one (ignored for the first serie) + barOverlapPerc : 0, + + // disable this if you want to use null values and want the lines/area to be broken over null values + avgOverNulls: true, + + // Axis + defaultAxis : { + // [non per asse x] Normalizza il valore massimo dell'asse in modo che tutte le label abbiamo al massimo N cifre significative + // (Es: se il max e' 135 e normalize = 2 verra' impostato il max a 140, ma se il numero di label in y e' 3 verr� impostato 150) + normalize: 2, + // Permette di impostare i valori minimi e massimi di asse (invece di autorilevarli) + min: 0, //max: x, + // Imposta un testo da usare come prefisso e suffisso delle label + //prefix : "", suffix : "", + // Visualizza o meno le label dell'asse + labels: false, + // Distanza tra le label e l'asse relativo + labelsDistance: 8, + // [solo asse x] Rotazione (in gradi) delle label. Se specificato ignora i valori di labelsAnchor e labelsProps['text-anchor'] + labelsRotate: 0, + // Proprieta' grafiche delle label + labelsProps : {font: '10px Arial', fill: "#000"}, + // Compatta il numero mostrato nella label usando i suffissi specificati per migliaia, milioni... + //labelsCompactUnits : ['k', 'M'], + // Permette di specificare una funzione esterna che si occupa di formattare (o in generale trasformare) la label + //labelsFormatHandler : function (label) { return label }, + // Salta le prime N label + //labelsSkip : 0, + // Force alignment for the label. Auto will automatically center it for x axis (also considering labelsRotate), "end" for l axis, "start" for the right axis. + //labelsAnchor : "auto" + // [solo asse x] Force an alternative position for the X axis labels. Auto will automatically choose the right position depending on "labelsCenter", the type of charts (bars vs lines), and labelsRotate. + //labelsPos : "auto", + // Automatically hide labels that would overlap previous labels. + //labelsHideCovered : true, + // Inserisce un margine alla label (a sinistra se in asse x, in alto se in altri assi) + //labelsMargin: 10, + // [solo asse x] If labelsHideCovered = true, make sure each label have at least this space before the next one. + //labelsMarginRight: 0, + // Distanza del titolo dall'asse + titleDistance : 25, titleDistanceIE : .75, + // Proprieta' grafiche del titolo + titleProps : {font: '12px Arial', fill: "#000", "font-weight": "bold"} + }, + axis : { + x : { titleDistanceIE : 1.2 } + }, + + defaultSeries : { + // Tipo di serie, puo' essere 'line' o 'bar' + type : 'line', + // L'asse di riferimento della serie. Gli assi "l" ed "r" sono i 2 assi visibili destro e sinistro. + // E' possibile inserire anche un asse arbitrario (che non sar� visibile) + axis : 'l', + // Specificare cumulative = true se i valori inseriti per la serie sono cumulativi + cumulative : false, + // In caso di type="line" indica l'arrotondamento della linea + rounded : 1, + // Mette il punto di intersezione al centro dell'intervallo invece che al limite (per allineamento con bars). Se 'auto' decide autonomamente + lineCenter : 'auto', + // Permette di impilare le serie (i valori di uno iniziano dove finiscono quelli del precedente) con un altra (purche' dello stesso tipo) + // Specificare "true" per impilare con la serie visibile precedente, oppure il nome della serie sulla quale impilare + // stacked : false, + + plotProps : {"stroke-width": 1, "stroke-linejoin": "round"}, + + barWidthPerc: 100, + //DELETED: barProps : {"width-perc" : 100, "stroke-width": 1, "fill-opacity" : .3}, + + // Attiva o disattiva il riempimento + fill : false, + fillProps : {stroke: "none", "stroke-width" : 0, "stroke-opacity": 0, opacity: .3}, + + dot : false, + dotProps : {size: 4, stroke: "#000", zindex: 5}, + dotShowOnNull : false, + + mouseareaShowOnNull : false, + + startAnimation : { + plotPropsFrom : false, + // DELETED linePropsFrom : false, + fillPropsFrom : false, + dotPropsFrom : false, + //DELETED barPropsFrom : false, + shadowPropsFrom : false + } + + }, + + features : { + grid : { + // N. di divisioni sull'asse X. Se "auto" si basa sulla label da visualizzare. Se "0" imposta draw[vertical] = false + // Da notare che se "auto" allora la prima e l'ultima linea (bordi) le fa vedere sempre (se ci sono le label). Se invece e' un numero si comporta come ny: fa vedere i bordi solo se forzato con forceBorder + nx : "auto", + // N. di divisione sull'asse Y. Se "0" imposta draw[horizontal] = false + ny : 4, + // Disegna o meno la griglia. Si puo' specificare un array [horizontal, vertical] + draw : false, + // Forza la visualizzazione dei bordi/assi. Se true disegna comunque i bordi (anche se draw = false o se non ci sono label), + // altrimenti si basa sulle regole standard di draw e presenza label (per asse x) + // Puo' essere un booleano singolo o un array di bordi [up, dx, down, sx] + forceBorder : false, + // Proprieta' di visualizzazione griglia + props : {stroke: '#e0e0e0', "stroke-width": 1}, + // Dimensioni extra delle rette [up, dx, down, sx] + extra : [0, 0, 0, 0], + // Indica se le label (e le rispettive linee del grid) vanno centrate sulle barre (true), quindi tra 2 linee, o sui punti della serie (false), quindi su una sola linea + // Se specificato "auto" decide in autonomia + labelsCenter : "auto", + + // Display a rectangular region with properties specied for every even/odd vertical/horizontal grid division + evenVProps : false, + oddVProps : false, + evenHProps : false, + oddHProps : false, + + ticks : { + // Attiva le barrette sugli assi [x, l, r] + active : [false, false, false], + // Dimensioni da prima dell'asse a dopo l'asse + size : [10, 10], + // Proprieta' di visualizzazione griglia + props : {stroke: '#e0e0e0', "stroke-width": 1} + } + } + }, + + nop : 0 + }, + + pie : { + template : 'common', + + // Coordinate del centro, se non specificate vengono autodeterminate + //cx : 0, cy : 0, + // Raggio della torta, se non specificato viene autodeterminato + //r : 0 + // Radius in percentage of the available space + //rPerc : 80 + // Angolo dal quale iniziare a disegnare le fette, in gradi + startAngle : 0, + // Disegna la torta con le fette in senso orario (invece dell'orientamento standard per gradi, in senso antiorario) + clockwise : false, + // Soglia (rapporto sul totale) entro la quale una fetta non viene visualizzata + valueThresold : 0.006, + + // @since elycharts 2.1.5 (previously there was no margins support so when we implemented it we had to add a 0 margin + // here to not start adding the common margin to every pie user + margins : [0, 0, 0, 0], + + defaultSeries : { + // r: .5, raggio usato solo per questo spicchio, se <=1 e' in rapporto al raggio generale + // inside: X, inserisce questo spicchio dentro un altro (funziona solo inside: precedente, e non gestisce + spicchi dentro l'altro) + } + }, + + funnel : { + template : 'common', + + rh: 0, // height of ellipsis (for top and bottom cuts) + method: 'width', // width/cutarea + topSector: 0, // height factor of top cylinder + topSectorProps : { fill: "#d0d0d0" }, + bottomSector: .1, // height factor of bottom cylinder + bottomSectorProps : { fill: "#d0d0d0" }, + edgeProps : { fill: "#c0c0c0", "stroke-width": 1, opacity: 1 }, + + nop : 0 + }, + + barline : { + template : 'common', + + // Imposta il valore massimo per la scala (altrimenti prende il valore + alto) + // max : X + + // Impostare direction = rtl per creare un grafico che va da destra a sinistra + direction : 'ltr' + } +} + +})(jQuery); +/********* Source File: src/elycharts_core.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { +if (!$.elycharts) + $.elycharts = {}; + +$.elycharts.lastId = 0; + +/*********************************************************************** + * INITIALIZATION / MAIN CALL + **********************************************************************/ + +$.fn.chart = function($options) { + if (!this.length) + return this; + + var $env = this.data('elycharts_env'); + + if (typeof $options == "string") { + if ($options.toLowerCase() == "config") + return $env ? $env.opt : false; + if ($options.toLowerCase() == "clear") { + if ($env) { + if ($.elycharts.featuresmanager) $.elycharts.featuresmanager.clear($env); + $env.paper.clear(); + $env.cache = false; + if ($env.autoresize) $(window).unbind('resize', $env.autoresize); + this.html(""); + this.data('elycharts_env', false); + } + } + return this; + } + + if (!$env) { + // First call, initialization + + if ($options) + $options = _extendAndNormalizeOptions($options); + + if (!$options || !$options.type || !$.elycharts.templates[$options.type]) { + alert('ElyCharts ERROR: chart type is not specified'); + return false; + } + $env = _initEnv(this, $options); + + this.data('elycharts_env', $env); + } else { + if (!$options) $options = {}; + $options = _normalizeOptions($options, $env.opt); + + // Already initialized + $env.oldopt = common._clone($env.opt); + $env.opt = $.extend(true, $env.opt, $options); + $env.newopt = $options; + $env.oldwidth = $env.width; + $env.oldheight = $env.height; + + } + + $env.cache = $options['enableInternalCaching'] ? {} : false; + + _processGenericConfig($env, $options); + + if ($env.opt.autoresize) { + if (!$env.autoresize) { + var that = this; + $env.autoresize = _debounce(function() { + that.chart(); + }); + $(window).bind('resize', $env.autoresize); + } + } else { + if ($env.autoresize) { + $(window).unbind('resize', $env.autoresize); + $env.autoresize = false; + } + } + + + var pieces = $.elycharts[$env.opt.type].draw($env); + if ($env.pieces) { + pieces = _updatePieces($env, $env.pieces, pieces); + } + common._show($env, pieces); + $env.pieces = pieces; + + return this; +} + +function _updatePieces(env, pieces1, pieces2, section, serie, internal) { + // Se pieces2 == null deve essere nascosto tutto pieces1 + var newpieces = [], newpiece; + var j = 0; + for (var i = 0; i < pieces1.length; i ++) { + + // Se il piece attuale c'e' solo in pieces2 lo riporto nei nuovi, impostando come gia' mostrato + // A meno che internal = true (siamo in un multipath, nel caso se una cosa non c'e' va considerata da togliere) + if (pieces2 && (j >= pieces2.length || !common.samePiecePath(pieces1[i], pieces2[j]))) { + if (!internal) { + pieces1[i].show = false; + newpieces.push(pieces1[i]); + } else { + newpiece = { path : false, attr : false }; + newpiece.show = true; + newpiece.animation = { + element : pieces1[i].element ? pieces1[i].element : false, + speed : 0, + easing : '', + delay : 0 + } + newpieces.push(newpiece); + } + } + // Bisogna gestire la transizione dal vecchio piece al nuovo + else { + newpiece = pieces2 ? pieces2[j] : { path : false, attr : false }; + newpiece.show = true; + if (typeof pieces1[i].paths == 'undefined') { + newpiece.animation = { + element : pieces1[i].element ? pieces1[i].element : false, + speed : 0, + easing : '', + delay : 0 + } + } else { + // Multiple path piece + newpiece.paths = _updatePieces(env, pieces1[i].paths, pieces2[j].paths, pieces1[i].section, pieces1[i].serie, true); + } + newpieces.push(newpiece); + j++; + } + } + // If there are pieces left in pieces2 i must add them unchanged + if (pieces2) + for (; j < pieces2.length; j++) + newpieces.push(pieces2[j]); + + return newpieces; +}; + + +// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/ +function _debounce(func, threshold, execAsap) { + var timeout; + return function debounced () { + var obj = this, args = arguments; + function delayed () { + if (!execAsap) func.apply(obj, args); + timeout = null; + }; + + if (timeout) clearTimeout(timeout); + else if (execAsap) func.apply(obj, args); + + timeout = setTimeout(delayed, threshold || 100); + }; +} + +/** + * Must be called only in first call to .chart, to initialize elycharts environment. + */ +function _initEnv($container, $options) { + var $env = { + id : $.elycharts.lastId ++, + paper : common._RaphaelInstance($container.get()[0], 0, 0), + container : $container, + plots : [], + opt : $options + }; + + // Rendering a transparent pixel up-left. Thay way SVG area is well-covered (else the position starts at first real object, and that mess-ups everything) + $env.paper.rect(0,0,1,1).attr({opacity: 0}); + + $.elycharts[$options.type].init($env); + + return $env; +} + +function _processGenericConfig($env, $options) { + if ($options.style) + $env.container.css($options.style); + $env.width = $options.width ? $options.width : $env.container.width(); + $env.height = $options.height ? $options.height : $env.container.height(); + $env.paper.setSize($env.width, $env.height); +} + +/** + * Must be called in first call to .chart, to build the full config structure and normalize it. + */ +function _extendAndNormalizeOptions($options) { + var k; + // Compatibility with old $.elysia_charts.default_options and $.elysia_charts.templates + if ($.elysia_charts) { + if ($.elysia_charts.default_options) + for (k in $.elysia_charts.default_options) + $.elycharts.templates[k] = $.elysia_charts.default_options[k]; + if ($.elysia_charts.templates) + for (k in $.elysia_charts.templates) + $.elycharts.templates[k] = $.elysia_charts.templates[k]; + } + + // TODO Optimize extend cycle + while ($options.template) { + var d = $options.template; + delete $options.template; + $options = $.extend(true, {}, $.elycharts.templates[d], $options); + } + if (!$options.template && $options.type) { + $options.template = $options.type; + while ($options.template) { + d = $options.template; + delete $options.template; + $options = $.extend(true, {}, $.elycharts.templates[d], $options); + } + } + + return _normalizeOptions($options, $options); +} + +/** + * Normalize options passed (primarly for backward compatibility) + */ +function _normalizeOptions($options, $fullopt) { + if ($options.type == 'pie' || $options.type == 'funnel') { + if ($options.values && $.isArray($options.values) && !$.isArray($options.values[0])) + $options.values = { root : $options.values }; + if ($options.tooltips && $.isArray($options.tooltips) && !$.isArray($options.tooltips[0])) + $options.tooltips = { root : $options.tooltips }; + if ($options.anchors && $.isArray($options.anchors) && !$.isArray($options.anchors[0])) + $options.anchors = { root : $options.anchors }; + if ($options.balloons && $.isArray($options.balloons) && !$.isArray($options.balloons[0])) + $options.balloons = { root : $options.balloons }; + if ($options.legend && $.isArray($options.legend) && !$.isArray($options.legend[0])) + $options.legend = { root : $options.legend }; + } + + if ($options.defaultSeries) { + var plotType = $options.defaultSeries.type ? $options.defaultSeries.type : ($fullopt.defaultSeries.type ? $fullopt.defaultSeries.type : $fullopt.type); + _normalizeOptionsSerie($options.defaultSeries, $fullopt.type, plotType, $fullopt); + } + + if ($options.series) + for (var serie in $options.series) { + var seriePlotType = $options.series[serie].type ? $options.series[serie].type : ($fullopt.series[serie].type ? $fullopt.series[serie].type : (plotType ? plotType : $fullopt.type)); + _normalizeOptionsSerie($options.series[serie], $fullopt.type, seriePlotType, $fullopt); + } + + if ($options.type == 'line') { + if (!$options.features) + $options.features = {}; + if (!$options.features.grid) + $options.features.grid = {}; + + if (typeof $options.gridNX != 'undefined') { + $options.features.grid.nx = $options.gridNX; + delete $options.gridNX; + } + if (typeof $options.gridNY != 'undefined') { + $options.features.grid.ny = $options.gridNY; + delete $options.gridNY; + } + if (typeof $options.gridProps != 'undefined') { + $options.features.grid.props = $options.gridProps; + delete $options.gridProps; + } + if (typeof $options.gridExtra != 'undefined') { + $options.features.grid.extra = $options.gridExtra; + delete $options.gridExtra; + } + if (typeof $options.gridForceBorder != 'undefined') { + $options.features.grid.forceBorder = $options.gridForceBorder; + delete $options.gridForceBorder; + } + + if ($options.defaultAxis && $options.defaultAxis.normalize && ($options.defaultAxis.normalize == 'auto' || $options.defaultAxis.normalize == 'autony')) + $options.defaultAxis.normalize = 2; + + if ($options.axis) + for (var axis in $options.axis) + if ($options.axis[axis] && $options.axis[axis].normalize && ($options.axis[axis].normalize == 'auto' || $options.axis[axis].normalize == 'autony')) + $options.axis[axis].normalize = 2; + } + + return $options; +} + + +/** +* Manage "color" attribute, the stackedWith legacy and values "color" properties. +* @param $section Section part of external conf passed +* @param $type Chart type +* @param $plotType for line chart can be "line" or "bar", for other types is equal to chart type. +*/ +function _normalizeOptionsSerie($section, $type, $plotType, $fullopt) { + if ($section.stackedWith) { + $section.stacked = $section.stackedWith; + delete $section.stackedWith; + } +} + +/*********************************************************************** + * COMMON + **********************************************************************/ + +$.elycharts.common = { + _RaphaelInstance : function(c, w, h) { + var r = Raphael(c, w, h); + + r.customAttributes.slice = function (cx, cy, r, rint, aa1, aa2) { + // Method body is for clockwise angles, but parameters passed are ccw + a1 = 360 - aa2; a2 = 360 - aa1; + //a1 = aa1; a2 = aa2; + var flag = (a2 - a1) > 180; + a1 = (a1 % 360) * Math.PI / 180; + a2 = (a2 % 360) * Math.PI / 180; + // a1 == a2 (but they where different before) means that there is a complete round (eg: 0-360). This should be shown + if (a1 == a2 && aa1 != aa2) + a2 += 359.99 * Math.PI / 180; + + return { path : rint ? [ + ["M", cx + r * Math.cos(a1), cy + r * Math.sin(a1)], + ["A", r, r, 0, +flag, 1, cx + r * Math.cos(a2), cy + r * Math.sin(a2)], + ["L", cx + rint * Math.cos(a2), cy + rint * Math.sin(a2)], + //["L", cx + rint * Math.cos(a1), cy + rint * Math.sin(a1)], + ["A", rint, rint, 0, +flag, 0, cx + rint * Math.cos(a1), cy + rint * Math.sin(a1)], + ["z"] + ] : [ + ["M", cx, cy], + ["l", r * Math.cos(a1), r * Math.sin(a1)], + ["A", r, r, 0, +flag, 1, cx + r * Math.cos(a2), cy + r * Math.sin(a2)], + ["z"] + ] }; + }; + + return r; + }, + + _clone : function(obj){ + if(obj == null || typeof(obj) != 'object') + return obj; + if (obj.constructor == Array) + return [].concat(obj); + var temp = new obj.constructor(); // changed (twice) + for(var key in obj) + temp[key] = this._clone(obj[key]); + return temp; + }, + + compactUnits : function(val, units) { + for (var i = units.length - 1; i >= 0; i--) { + var v = val / Math.pow(1000, i + 1); + //console.warn(i, units[i], v, v * 10 % 10); + if (v >= 1 && v * 10 % 10 == 0) + return v + units[i]; + } + return val; + }, + + getElementOriginalAttrs : function(element) { + var attr = $(element.node).data('original-attr'); + if (!attr) { + attr = element.attr(); + $(element.node).data('original-attr', attr); + } + return attr; + }, + + findInPieces : function(pieces, section, serie, index, subsection) { + for (var i = 0; i < pieces.length; i++) { + if ( + (typeof section == undefined || section == -1 || section == false || pieces[i].section == section) && + (typeof serie == undefined || serie == -1 || serie == false || pieces[i].serie == serie) && + (typeof index == undefined || index == -1 || index == false || pieces[i].index == index) && + (typeof subsection == undefined || subsection == -1 || subsection == false || pieces[i].subSection == subsection) + ) + return pieces[i]; + } + return false; + }, + + samePiecePath : function(piece1, piece2) { + return (((typeof piece1.section == undefined || piece1.section == -1 || piece1.section == false) && (typeof piece2.section == undefined || piece2.section == -1 || piece2.section == false)) || piece1.section == piece2.section) && + (((typeof piece1.serie == undefined || piece1.serie == -1 || piece1.serie == false) && (typeof piece2.serie == undefined || piece2.serie == -1 || piece2.serie == false)) || piece1.serie == piece2.serie) && + (((typeof piece1.index == undefined || piece1.index == -1 || piece1.index == false) && (typeof piece2.index == undefined || piece2.index == -1 || piece2.index == false)) || piece1.index == piece2.index) && + (((typeof piece1.subSection == undefined || piece1.subSection == -1 || piece1.subSection == false) && (typeof piece2.subSection == undefined || piece2.subSection == -1 || piece2.subSection == false)) || piece1.subSection == piece2.subSection); + }, + + executeIfChanged : function(env, changes) { + if (!env.newopt) + return true; + + for (var i = 0; i < changes.length; i++) { + if (changes[i][changes[i].length - 1] == "*") { + for (var j in env.newopt) + if (j.substring(0, changes[i].length - 1) + "*" == changes[i]) + return true; + } + else if (changes[i] == 'series' && (env.newopt.series || env.newopt.defaultSeries)) + return true; + else if (changes[i] == 'axis' && (env.newopt.axis || env.newopt.defaultAxis)) + return true; + else if (changes[i] == 'width' && (env.oldwidth != env.width)) + return true; + else if (changes[i] == 'height' && (env.oldheight != env.height)) + return true; + else if (changes[i].substring(0, 9) == "features.") { + changes[i] = changes[i].substring(9); + if (env.newopt.features && env.newopt.features[changes[i]]) + return true; + } + else if (typeof env.newopt[changes[i]] != 'undefined') + return true; + } + return false; + }, + + /** + * Can be called for a whole serie or for a given index of the serie. + * returns the color for that item considering valuesPalette, seriesPalette and inheritance + */ + getItemColor : function(env, serie, index) { + var props = this.areaProps(env, 'Series', serie, index); + if (props.color) return props.color; + if (index !== false && props.valuesPalette) return props.valuesPalette[index % props.valuesPalette.length]; + if (env.opt.seriesPalette) { + var serieIndex = 0; + for(seriekey in env.opt.values) { + if (serie == seriekey) return env.opt.seriesPalette[serieIndex % env.opt.seriesPalette.length]; + else serieIndex++; + } + } + }, + + /** + * Given an expandKey as array of array it sets the color to the nested tree unless it is already defined. + * So [ [ 'parent', 'child' ], [ 'item' ] ] will try to put color in props.parent.child and props.item unless + * they already exists. + */ + colorize : function(env, props, expandKeys, color) { + if (color) { + for (k in expandKeys) { + var p = props; + var i = 0; + for (i = 0; i < expandKeys[k].length - 1; i++) { + if (!p[expandKeys[k][i]]) p[expandKeys[k][i]] = {}; + p = p[expandKeys[k][i]]; + } + if (!p[expandKeys[k][expandKeys[k].length-1]]) p[expandKeys[k][expandKeys[k].length-1]] = color; + } + } + }, + + /** + * Ottiene le proprietà di una "Area" definita nella configurazione (options), + * identificata da section / serie / index / subsection, e facendo il merge + * di tutti i defaults innestati. + */ + areaProps : function(env, section, serie, index, subsection) { + var props; + + var sectionProps = env.opt[section.toLowerCase()]; + // TODO fare una cache e fix del toLowerCase (devono solo fare la prima lettera + if (!subsection) { + if (typeof serie == 'undefined' || !serie) + props = sectionProps; + + else { + var cacheKey = section+'/'+serie+'/'+index; + if (env.cache && env.cache.areaPropsCache && env.cache.areaPropsCache[cacheKey]) { + props = env.cache.areaPropsCache[cacheKey]; + } + else { + props = this._clone(env.opt['default' + section]); + if (sectionProps && sectionProps[serie]) + props = $.extend(true, props, sectionProps[serie]); + + if ((typeof index != 'undefined') && index >= 0 && props['values'] && props['values'][index]) + props = $.extend(true, props, props['values'][index]); + + if (env.cache) { + if (!env.cache.areaPropsCache) env.cache.areaPropsCache = {}; + env.cache.areaPropsCache[cacheKey] = props; + } + } + } + + } else { + var subsectionKey = subsection.toLowerCase(); + props = this._clone(env.opt[subsectionKey]); + + if (typeof serie == 'undefined' || !serie) { + if (sectionProps && sectionProps[subsectionKey]) + props = $.extend(true, props, sectionProps[subsectionKey]); + + } else { + if (env.opt['default' + section] && env.opt['default' + section][subsectionKey]) + props = $.extend(true, props, env.opt['default' + section][subsectionKey]); + + if (sectionProps && sectionProps[serie] && sectionProps[serie][subsectionKey]) + props = $.extend(true, props, sectionProps[serie][subsectionKey]); + + if ((typeof index != 'undefined') && index > 0 && props['values'] && props['values'][index]) + props = $.extend(true, props, props['values'][index]); + } + } + + return props; + }, + + _absrectpath : function(x1, y1, x2, y2, r) { + if (r) { + // we can use 'a' or 'Q' for the same result. + var res = [ + ['M',x1,y1+r], ['a', r, r, 0, 0, 1, r, -r], //['Q',x1,y1, x1+r,y1], + ['L',x2-r,y1], ['a', r, r, 0, 0, 1, r, r], //['Q',x2,y1, x2,y1+r], + ['L',x2,y2-r], ['a', r, r, 0, 0, 1, -r, r], // ['Q',x2,y2, x2-r,y2], + ['L',x1+r,y2], ['a', r, r, 0, 0, 1, -r, -r], // ['Q',x1,y2, x1,y2-r], + ['z'] + ]; + return res; + } else return [['M', x1, y1], ['L', x1, y2], ['L', x2, y2], ['L', x2, y1], ['z']]; + }, + + _linepathAnchors : function(p1x, p1y, p2x, p2y, p3x, p3y, rounded) { + var method = 1; + if (rounded && rounded.length) { + method = rounded[1]; + rounded = rounded[0]; + } + if (!rounded) + rounded = 1; + var l1 = (p2x - p1x) / 2, + l2 = (p3x - p2x) / 2, + a = Math.atan(Math.abs(p2x - p1x) / Math.abs(p2y - p1y)), + b = Math.atan(Math.abs(p3x - p2x) / Math.abs(p2y - p3y)); + a = (p1y < p2y && p2x > p1x) || (p1y > p2y && p2x < p1x) ? Math.PI - a : a; + b = (p3y < p2y && p3x > p2x) || (p3y > p2y && p3x < p2x) ? Math.PI - b : b; + if (method == 2) { + // If added by Bago to avoid curves beyond min or max + if ((a - Math.PI / 2) * (b - Math.PI / 2) > 0) { + a = 0; + b = 0; + } else { + if (Math.abs(a - Math.PI / 2) < Math.abs(b - Math.PI / 2)) + b = Math.PI - a; + else + a = Math.PI - b; + } + } + + var alpha = Math.PI / 2 - ((a + b) % (Math.PI * 2)) / 2, + dx1 = l1 * Math.sin(alpha + a) / 2 / rounded, + dy1 = l1 * Math.cos(alpha + a) / 2 / rounded, + dx2 = l2 * Math.sin(alpha + b) / 2 / rounded, + dy2 = l2 * Math.cos(alpha + b) / 2 / rounded; + return { + x1: p2x - dx1, + y1: p2y + dy1, + x2: p2x + dx2, + y2: p2y + dy2 + }; + }, + + _linepath : function ( points, rounded ) { + var path = []; + if (rounded) { + var anc = false; + for (var j = 0, jj = points.length; j < jj ; j++) { + var x = points[j][0], y = points[j][1]; + if (x != null && y != null) { + if (anc) { + if (j + 1 != jj && points[j + 1][0] != null && points[j + 1][1] != null) { + var a = this._linepathAnchors(points[j - 1][0], points[j - 1][1], points[j][0], points[j][1], points[j + 1][0], points[j + 1][1], rounded); + path.push([ "C", anc[0], anc[1], a.x1, a.y1, points[j][0], points[j][1] ]); + // path.push([ "M", anc[0], anc[1] ]); + // path.push([ "L", a.x1, a.y1 ]); + // path.push([ "M", points[j][0], points[j][1] ]); + anc = [ a.x2, a.y2 ]; + } else { + path.push([ "C", anc[0], anc[1], points[j][0], points[j][1], points[j][0], points[j][1] ]); + anc = [ points[j][0], points[j][1] ]; + } + } else { + path.push([ "M", points[j][0], points[j][1] ]); + anc = [ points[j][0], points[j][1] ]; + } + } else anc = false; + } + + } else { + var prevx = null; + var prevy = null; + for (var i = 0; i < points.length; i++) { + var x = points[i][0], y = points[i][1]; + if (x != null && y != null) { + path.push([prevx == null || prevy == null ? "M" : "L", x, y]); + } + prevx = x; + prevy = y; + } + } + + return path; + }, + + _lineareapath : function (points1, points2, rounded) { + var path = this._linepath(points1, rounded); + var path2 = this._linepath(points2.reverse(), rounded); + var finalPath = []; + var firstPushed = null; + for (var i = 0; i <= path.length; i++) { + if (i == path.length || path[i][0] == "M") { + if (firstPushed != null) { + for (var j = path.length - i; j <= path.length - firstPushed; j++) { + if (path2[j][0] == "M") finalPath.push([ "L", path2[j][1], path2[j][2] ]); + else finalPath.push(path2[j]); + } + finalPath.push(['z']); + firstPushed = null; + } + if (i != path.length) finalPath.push(path[i]); + } else { + finalPath.push(path[i]); + if (firstPushed == null) firstPushed = i; + } + } + return finalPath; + }, + + /** + * Prende la coordinata X di un passo di un path + */ + getX : function(p, pos) { + switch (p[0]) { + case 'CIRCLE': + return p[1]; + case 'RECT': + return p[!pos ? 1 : 3]; + case 'SLICE': + return p[1]; + default: + return p[p.length - 2]; + } + }, + + /** + * Prende la coordinata Y di un passo di un path + */ + getY : function(p, pos) { + switch (p[0]) { + case 'CIRCLE': + return p[2]; + case 'RECT': + return p[!pos ? 2 : 4]; + case 'SLICE': + return p[2]; + default: + return p[p.length - 1]; + } + }, + + /** + * Prende il centro di un path + * + * @param offset un offset [x,y] da applicare. Da notare che gli assi potrebbero essere dipendenti dalla figura + * (ad esempio per lo SLICE x e' l'asse che passa dal centro del cerchio, y l'ortogonale). + */ + getCenter: function(path, offset) { + if (!path.path) + return false; + if (path.path.length == 0) + return false; + if (!offset) + offset = [0, 0]; + + if (path.center) + return [path.center[0] + offset[0], path.center[1] + offset[1]]; + + var p = path.path[0]; + switch (p[0]) { + case 'CIRCLE': + return [p[1] + offset[0], p[2] + offset[1]]; + case 'RECT': + return [(p[1] + p[2])/2 + offset[0], (p[3] + p[4])/2 + offset[1]]; + case 'SLICE': + var popangle = p[5] + (p[6] - p[5]) / 2; + var rad = Math.PI / 180; + return [ + p[1] + (p[4] + ((p[3] - p[4]) / 2) + offset[0]) * Math.cos(-popangle * rad) + offset[1] * Math.cos((-popangle-90) * rad), + p[2] + (p[4] + ((p[3] - p[4]) / 2) + offset[0]) * Math.sin(-popangle * rad) + offset[1] * Math.sin((-popangle-90) * rad) + ]; + } + + // WARN Complex paths not supported + alert('ElyCharts: getCenter with complex path not supported'); + + return false; + }, + + /** + * Sposta il path passato di un offset [x,y] + * Il risultato e' il nuovo path + * + * @param offset un offset [x,y] da applicare. Da notare che gli assi potrebbero essere dipendenti dalla figura + * (ad esempio per lo SLICE x e' l'asse che passa dal centro del cerchio, y l'ortogonale). + * @param marginlimit se true non sposta oltre i margini del grafico (applicabile solo su path standard o RECT) + * @param simple se true lo spostamento e' sempre fatto sul sistema [x, y] complessivo (altrimenti alcuni elementi, come lo SLICE, + * si muovono sul proprio sistema di coordinate - la x muove lungo il raggio e la y lungo l'ortogonale) + */ + movePath : function(env, path, offset, marginlimit, simple) { + var p = [], i; + if (path.length == 1 && path[0][0] == 'RECT') + return [ [path[0][0], this._movePathX(env, path[0][1], offset[0], marginlimit), this._movePathY(env, path[0][2], offset[1], marginlimit), this._movePathX(env, path[0][3], offset[0], marginlimit), this._movePathY(env, path[0][4], offset[1], marginlimit), path[0][5]] ]; + if (path.length == 1 && path[0][0] == 'SLICE') { + if (!simple) { + var popangle = path[0][5] + (path[0][6] - path[0][5]) / 2; + var rad = Math.PI / 180; + var x = path[0][1] + offset[0] * Math.cos(- popangle * rad) + offset[1] * Math.cos((-popangle-90) * rad); + var y = path[0][2] + offset[0] * Math.sin(- popangle * rad) + offset[1] * Math.cos((-popangle-90) * rad); + return [ [path[0][0], x, y, path[0][3], path[0][4], path[0][5], path[0][6] ] ]; + } + else + return [ [ path[0][0], path[0][1] + offset[0], path[0][2] + offset[1], path[0][3], path[0][4], path[0][5], path[0][6] ] ]; + } + if (path.length == 1 && path[0][0] == 'CIRCLE') + return [ [ path[0][0], path[0][1] + offset[0], path[0][2] + offset[1], path[0][3] ] ]; + if (path.length == 1 && path[0][0] == 'TEXT') + return [ [ path[0][0], path[0][1], path[0][2] + offset[0], path[0][3] + offset[1] ] ]; + if (path.length == 1 && path[0][0] == 'LINE') { + for (i = 0; i < path[0][1].length; i++) + p.push( [ this._movePathX(env, path[0][1][i][0], offset[0], marginlimit), this._movePathY(env, path[0][1][i][1], offset[1], marginlimit) ] ); + return [ [ path[0][0], p, path[0][2] ] ]; + } + if (path.length == 1 && path[0][0] == 'LINEAREA') { + for (i = 0; i < path[0][1].length; i++) + p.push( [ this._movePathX(env, path[0][1][i][0], offset[0], marginlimit), this._movePathY(env, path[0][1][i][1], offset[1], marginlimit) ] ); + var pp = []; + for (i = 0; i < path[0][2].length; i++) + pp.push( [ this._movePathX(env, path[0][2][i][0], offset[0], marginlimit), this._movePathY(env, path[0][2][i][1], offset[1], marginlimit) ] ); + return [ [ path[0][0], p, pp, path[0][3] ] ]; + } + + var newpath = []; + // http://www.w3.org/TR/SVG/paths.html#PathData + for (var j = 0; j < path.length; j++) { + var o = path[j]; + switch (o[0]) { + // TODO the translation for lowercase actions are all wrong! + // relative movements do not need to be adjusted for moving (or at most, only the first one have to). + // TODO relative movements this way cannot be forced to stay in marginlimit! + case 'M': case 'm': case 'L': case 'l': case 'T': case 't': + // (x y)+ + newpath.push([o[0], this._movePathX(env, o[1], offset[0], marginlimit), this._movePathY(env, o[2], offset[1], marginlimit)]); + break; + case 'A': case 'a': + // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ + newpath.push([o[0], o[1], o[2], o[3], o[4], o[5], this._movePathX(env, o[6], offset[0], marginlimit), this._movePathY(env, o[7], offset[1], marginlimit)]); + break; + case 'C': case 'c': + // Fixed for uppercase C in 2.1.5 + // (x1 y1 x2 y2 x y)+ + newpath.push([o[0], + this._movePathX(env, o[1], offset[0], marginlimit), this._movePathY(env, o[2], offset[1], marginlimit), + this._movePathX(env, o[3], offset[0], marginlimit), this._movePathY(env, o[4], offset[1], marginlimit), + this._movePathX(env, o[5], offset[0], marginlimit), this._movePathY(env, o[6], offset[1], marginlimit) + ]); + break; + case 'S': case 's': case 'Q': case 'q': + // Fixed for uppercase Q in 2.1.5 + // (x1 y1 x y)+ + // newpath.push([o[0], o[1], o[2], this._movePathX(env, o[3], offset[0], marginlimit), this._movePathY(env, o[4], offset[1], marginlimit)]); + newpath.push([o[0], + this._movePathX(env, o[1], offset[0], marginlimit), this._movePathY(env, o[2], offset[1], marginlimit), + this._movePathX(env, o[3], offset[0], marginlimit), this._movePathY(env, o[4], offset[1], marginlimit) + ]); + break; + case 'z': case 'Z': + newpath.push([o[0]]); + break; + } + } + + return newpath; + }, + + _movePathX : function(env, x, dx, marginlimit) { + if (x == null) return null; + if (!marginlimit) + return x + dx; + x = x + dx; + return dx > 0 && x > env.width - env.opt.margins[1] ? env.width - env.opt.margins[1] : (dx < 0 && x < env.opt.margins[3] ? env.opt.margins[3] : x); + }, + + _movePathY : function(env, y, dy, marginlimit) { + if (y == null) return null; + if (!marginlimit) + return y + dy; + y = y + dy; + return dy > 0 && y > env.height - env.opt.margins[2] ? env.height - env.opt.margins[2] : (dy < 0 && y < env.opt.margins[0] ? env.opt.margins[0] : y); + }, + + /** + * Ritorna le proprieta SVG da impostare per visualizzare il path non SVG passato (se applicabile, per CIRCLE e TEXT non lo e') + */ + getSVGProps : function(env, origPath, prevprops) { + var path = this._preparePathShow(env, origPath); + var props = prevprops ? prevprops : {}; + var type = 'path', value; + + if (path.length == 1 && path[0][0] == 'RECT') + value = common._absrectpath(path[0][1], path[0][2], path[0][3], path[0][4], path[0][5]); + else if (path.length == 1 && path[0][0] == 'SLICE') { + type = 'slice'; + value = [ path[0][1], path[0][2], path[0][3], path[0][4], path[0][5], path[0][6] ]; + } else if (path.length == 1 && path[0][0] == 'LINE') + value = common._linepath( path[0][1], path[0][2] ); + else if (path.length == 1 && path[0][0] == 'LINEAREA') + value = common._lineareapath( path[0][1], path[0][2], path[0][3] ); + else if (path.length == 1 && (path[0][0] == 'CIRCLE' || path[0][0] == 'TEXT' || path[0][0] == 'DOMELEMENT' || path[0][0] == 'RELEMENT')) + return prevprops ? prevprops : false; + else + value = path; + + if (type != 'path' || (value && value.length > 0)) + props[type] = value; + else if (!prevprops) + return false; + return props; + }, + + /** + * Disegna il path passato + * Gestisce la feature pixelWorkAround + */ + showPath : function(env, path, paper) { + + if (!paper) + paper = env.paper; + if (path.length == 1 && path[0][0] == 'CIRCLE') { + path = this._preparePathShow(env, path); + return paper.circle(path[0][1], path[0][2], path[0][3]); + } + if (path.length == 1 && path[0][0] == 'TEXT') { + path = this._preparePathShow(env, path); + return paper.text(path[0][2], path[0][3], path[0][1]); + } + + var props = this.getSVGProps(env, path); + + // Props must be with some data in it + var hasdata = false; + for (var k in props) { + hasdata = true; + break; + } + return props && hasdata ? paper.path().attr(props) : false; + }, + + /** + * Applica al path le modifiche per poterlo visualizzare + * Per ora applica solo pixelWorkAround + */ + _preparePathShow : function(env, path) { + return env.opt.features.pixelWorkAround.active ? this.movePath(env, this._clone(path), [.5, .5], false, true) : path; + }, + + /** + * Ritorna gli attributi Raphael completi di un piece + * Per attributi completi si intende l'insieme di attributi specificato, + * assieme a tutti gli attributi calcolati che determinano lo stato + * iniziale di un piece (e permettono di farlo ritornare a tale stato). + * In genere viene aggiunto il path SVG, per il circle vengono aggiunti + * i dati x,y,r + */ + getPieceFullAttr : function(env, piece) { + if (!piece.fullattr) { + piece.fullattr = this._clone(piece.attr); + if (piece.path) + switch (piece.path[0][0]) { + case 'CIRCLE': + var ppath = this._preparePathShow(env, piece.path); + piece.fullattr.cx = ppath[0][1]; + piece.fullattr.cy = ppath[0][2]; + piece.fullattr.r = ppath[0][3]; + break; + case 'TEXT': case 'DOMELEMENT': case 'RELEMENT': + break; + default: + piece.fullattr = this.getSVGProps(env, piece.path, piece.fullattr); + } + if (typeof piece.fullattr.opacity == 'undefined') + piece.fullattr.opacity = 1; + } + return piece.fullattr; + }, + + + _show : function(env, origPieces) { + if ($.elycharts.featuresmanager) $.elycharts.featuresmanager.beforeShow(env, origPieces); + + pieces = this._getSortedPathData(origPieces); + + this._animationStackStart(env); + + var previousElement = false; + for (var i = 0; i < pieces.length; i++) { + var piece = pieces[i]; + if ((typeof piece.show == 'undefined' || piece.show) && (typeof piece.parent == 'undefined' || typeof piece.parent.show == 'undefined' || piece.parent.show)) { + // If there is piece.animation.element, this is the old element that must be transformed to the new one + piece.element = piece.animation && piece.animation.element ? piece.animation.element : false; + piece.hide = false; + + if (!piece.path) { + // Element should not be shown or must be hidden: nothing to prepare + piece.hide = true; + + } else if (piece.path.length == 1 && piece.path[0][0] == 'TEXT') { + // TEXT + // Animation is not supported, so if there's an old element i must hide it (with force = true to hide it for sure, even if there's a new version of same element) + if (piece.element) { + common.animationStackPush(env, piece, piece.element, false, piece.animation.speed, piece.animation.easing, piece.animation.delay, true); + piece.animation.element = false; + } + piece.element = this.showPath(env, piece.path); + // If this is a transition i must position new element + if (piece.element && env.newopt && previousElement) + piece.element.insertAfter(previousElement); + + } else if (piece.path.length == 1 && piece.path[0][0] == 'DOMELEMENT') { + // DOMELEMENT + // Already shown + // Animation not supported + + } else if (piece.path.length == 1 && piece.path[0][0] == 'RELEMENT') { + // RAPHAEL ELEMENT + // Already shown + // Animation is not supported, so if there's an old element i must hide it (with force = true to hide it for sure, even if there's a new version of same element) + if (piece.element) { + common.animationStackPush(env, piece, piece.element, false, piece.animation.speed, piece.animation.easing, piece.animation.delay, true); + piece.animation.element = false; + } + + piece.element = piece.path[0][1]; + if (piece.element && previousElement) + piece.element.insertAfter(previousElement); + piece.attr = false; + + } else { + // OTHERS + if (!piece.element) { + if (piece.animation && piece.animation.startPath && piece.animation.startPath.length) + piece.element = this.showPath(env, piece.animation.startPath); + else + piece.element = this.showPath(env, piece.path); + + // If this is a transition i must position new element + if (piece.element && env.newopt && previousElement) + piece.element.insertAfter(previousElement); + } + } + + if (piece.element) { + if (piece.attr) { + if (!piece.animation) { + // Standard piece visualization + if (typeof piece.attr.opacity == 'undefined') + piece.attr.opacity = 1; + piece.element.attr(piece.attr); + + } else { + // Piece animation + if (!piece.animation.element) + piece.element.attr(piece.animation.startAttr ? piece.animation.startAttr : piece.attr); + //if (typeof animationAttr.opacity == 'undefined') + // animationAttr.opacity = 1; + common.animationStackPush(env, piece, piece.element, this.getPieceFullAttr(env, piece), piece.animation.speed, piece.animation.easing, piece.animation.delay); + } + } else if (piece.hide) + // Hide the piece + common.animationStackPush(env, piece, piece.element, false, piece.animation.speed, piece.animation.easing, piece.animation.delay); + + previousElement = piece.element; + } + } + } + + this._animationStackEnd(env); + + if ($.elycharts.featuresmanager) $.elycharts.featuresmanager.afterShow(env, origPieces); + }, + + /** + * Given an array of pieces, return an array of single pathdata contained in pieces, sorted by zindex + */ + _getSortedPathData : function(pieces) { + res = []; + + for (var i = 0; i < pieces.length; i++) { + var piece = pieces[i]; + if (piece.paths) { + for (var j = 0; j < piece.paths.length; j++) { + piece.paths[j].pos = res.length; + piece.paths[j].parent = piece; + res.push(piece.paths[j]); + } + } else { + piece.pos = res.length; + piece.parent = false; + res.push(piece); + } + } + return res.sort(function (a, b) { + var za = typeof a.attr == 'undefined' || typeof a.attr.zindex == 'undefined' ? ( !a.parent || typeof a.parent.attr == 'undefined' || typeof a.parent.attr.zindex == 'undefined' ? 0 : a.parent.attr.zindex ) : a.attr.zindex; + var zb = typeof b.attr == 'undefined' || typeof b.attr.zindex == 'undefined' ? ( !b.parent || typeof b.parent.attr == 'undefined' || typeof b.parent.attr.zindex == 'undefined' ? 0 : b.parent.attr.zindex ) : b.attr.zindex; + return za < zb ? -1 : (za > zb ? 1 : (a.pos < b.pos ? -1 : (a.pos > b.pos ? 1 : 0))); + }); + }, + + _animationStackStart : function(env) { + if (!env.animationStackDepth || env.animationStackDepth == 0) { + env.animationStackDepth = 0; + env.animationStack = {}; + } + env.animationStackDepth ++; + }, + + _animationStackEnd : function(env) { + env.animationStackDepth --; + if (env.animationStackDepth == 0) { + for (var delay in env.animationStack) { + this._animationStackAnimate(env.animationStack[delay], delay); + delete env.animationStack[delay]; + } + env.animationStack = {}; + } + }, + + /** + * Inserisce l'animazione richiesta nello stack di animazioni. + * Nel caso lo stack non sia inizializzato esegue subito l'animazione. + */ + animationStackPush : function(env, piece, element, newattr, speed, easing, delay, force) { + if (typeof delay == 'undefined') + delay = 0; + + if (!env.animationStackDepth || env.animationStackDepth == 0) { + this._animationStackAnimate([{piece : piece, object : element, props : newattr, speed: speed, easing : easing, force : force}], delay); + + } else { + if (!env.animationStack[delay]) + env.animationStack[delay] = []; + + env.animationStack[delay].push({piece : piece, object : element, props : newattr, speed: speed, easing : easing, force : force}); + } + }, + + _animationStackAnimate : function(stack, delay) { + var caller = this; + var func = function() { + var a = stack.pop(); + var anim = caller._animationStackAnimateElement(a); + + while (stack.length > 0) { + var b = stack.pop(); + caller._animationStackAnimateElement(b, a, anim); + } + } + if (delay > 0) + setTimeout(func, delay); + else + func(); + }, + + _animationStackAnimateElement : function (a, awith, awithanim) { + //console.warn('call', a.piece.animationInProgress, a.force, a.piece.path, a.piece); + + if (a.force || !a.piece.animationInProgress) { + + // Metodo non documentato per bloccare l'animazione corrente + a.object.stop(); + if (!a.props) + a.props = { opacity : 0 }; // TODO Sarebbe da rimuovere l'elemento alla fine + + if (!a.speed || a.speed <= 0) { + //console.warn('direct'); + a.object.attr(a.props); + a.piece.animationInProgress = false; + return; + } + + a.piece.animationInProgress = true; + //console.warn('START', a.piece.animationInProgress, a.piece.path, a.piece); + + // NOTA onEnd non viene chiamato se l'animazione viene bloccata con stop + var onEnd = function() { + //console.warn('END', a.piece.animationInProgress, a.piece); + a.piece.animationInProgress = false + } + + if (Raphael.animation) { + var anim = Raphael.animation(a.props, a.speed, a.easing ? a.easing : 'linear', onEnd); + if (awith) { + // console.warn('animateWith', awith, awithanim, anim); + a.object.animateWith(awith, awithanim, anim); + } else { + // console.warn('animate', anim); + a.object.animate(anim); + } + return anim; + } else { + if (awith) { + // console.warn('animateWith', awith, awithanim, anim); + a.object.animateWith(awith, a.props, a.speed, a.easing ? a.easing : 'linear', onEnd); + } else { + // console.warn('animate', anim); + a.object.animate(a.props, a.speed, a.easing ? a.easing : 'linear', onEnd); + } + return null; + } + } + //else console.warn('SKIP', a.piece.animationInProgress, a.piece.path, a.piece); + return null; + } +} + +var common = $.elycharts.common; + +/*********************************************************************** + * FEATURESMANAGER + **********************************************************************/ + +$.elycharts.featuresmanager = { + + managers : [], + initialized : false, + + register : function(manager, priority) { + $.elycharts.featuresmanager.managers.push([priority, manager]); + $.elycharts.featuresmanager.initialized = false; + }, + + init : function() { + $.elycharts.featuresmanager.managers.sort(function(a, b) { return a[0] < b[0] ? -1 : (a[0] == b[0] ? 0 : 1) }); + $.elycharts.featuresmanager.initialized = true; + }, + + clear : function(env) { + if (!$.elycharts.featuresmanager.initialized) + this.init(); + // reverse cycle over manager + for (var i = $.elycharts.featuresmanager.managers.length - 1; i >= 0; i--) + if ($.elycharts.featuresmanager.managers[i][1].clear) + $.elycharts.featuresmanager.managers[i][1].clear(env); + }, + + beforeShow : function(env, pieces) { + if (!$.elycharts.featuresmanager.initialized) + this.init(); + for (var i = 0; i < $.elycharts.featuresmanager.managers.length; i++) + if ($.elycharts.featuresmanager.managers[i][1].beforeShow) + $.elycharts.featuresmanager.managers[i][1].beforeShow(env, pieces); + }, + + afterShow : function(env, pieces) { + if (!$.elycharts.featuresmanager.initialized) + this.init(); + for (var i = 0; i < $.elycharts.featuresmanager.managers.length; i++) + if ($.elycharts.featuresmanager.managers[i][1].afterShow) + $.elycharts.featuresmanager.managers[i][1].afterShow(env, pieces); + }, + + onMouseOver : function(env, serie, index, mouseAreaData) { + if (!$.elycharts.featuresmanager.initialized) + this.init(); + for (var i = 0; i < $.elycharts.featuresmanager.managers.length; i++) + if ($.elycharts.featuresmanager.managers[i][1].onMouseOver) + $.elycharts.featuresmanager.managers[i][1].onMouseOver(env, serie, index, mouseAreaData); + }, + + onMouseOut : function(env, serie, index, mouseAreaData) { + if (!$.elycharts.featuresmanager.initialized) + this.init(); + for (var i = 0; i < $.elycharts.featuresmanager.managers.length; i++) + if ($.elycharts.featuresmanager.managers[i][1].onMouseOut) + $.elycharts.featuresmanager.managers[i][1].onMouseOut(env, serie, index, mouseAreaData); + }, + + onMouseEnter : function(env, serie, index, mouseAreaData) { + if (!$.elycharts.featuresmanager.initialized) + this.init(); + for (var i = 0; i < $.elycharts.featuresmanager.managers.length; i++) + if ($.elycharts.featuresmanager.managers[i][1].onMouseEnter) + $.elycharts.featuresmanager.managers[i][1].onMouseEnter(env, serie, index, mouseAreaData); + }, + + onMouseChanged : function(env, serie, index, mouseAreaData) { + if (!$.elycharts.featuresmanager.initialized) + this.init(); + for (var i = 0; i < $.elycharts.featuresmanager.managers.length; i++) + if ($.elycharts.featuresmanager.managers[i][1].onMouseChanged) + $.elycharts.featuresmanager.managers[i][1].onMouseChanged(env, serie, index, mouseAreaData); + }, + + onMouseExit : function(env, serie, index, mouseAreaData) { + if (!$.elycharts.featuresmanager.initialized) + this.init(); + for (var i = 0; i < $.elycharts.featuresmanager.managers.length; i++) + if ($.elycharts.featuresmanager.managers[i][1].onMouseExit) + $.elycharts.featuresmanager.managers[i][1].onMouseExit(env, serie, index, mouseAreaData); + } +} + +})(jQuery); + +/*********************************************** + +* OGGETTI USATI: + +PIECE: +Contiene un elemento da visualizzare nel grafico. E' un oggetto con queste proprietà: + +- section,[serie],[index],[subsection]: Dati che permettono di identificare che tipo + di elemento è e a quale blocco della configurazione appartiene. + Ad esempio gli elementi principali del chart hanno + section="Series", serie=nome della serie, subSection = 'Plot' +- [paths]: Contiene un array di pathdata, nel caso questo piece è costituito da + piu' sottoelementi (ad esempio i Dots, o gli elementi di un Pie o Funnel) +- [PATHDATA.*]: Se questo piece e' costituito da un solo elemento, i suoi dati sono + memorizzati direttamente nella root di PIECE. +- show: Proprieta' usata internamente per decidere se questo piece dovrà essere + visualizzato o meno (in genere nel caso di una transizione che non ha variato + questo piece, che quindi puo' essere lasciato allo stato precedente) +- hide: Proprieta' usata internamente per decidere se l'elemento va nascosto, + usato in caso di transizione se l'elemento non è piu' presente. + +PATHDATA: +I dati utili per visualizzare un path nel canvas: + +- PATH: Il path che permette di disegnare l'elemento. Se NULL l'elemento è vuoto/ da + non visualizzare (instanziato solo come placeholder) +- attr: gli attributi Raphael dell'elemento. NULL se path è NULL. +- [center]: centro del path +- [rect]: rettangolo che include il path + +PATH: +Un array in cui ogni elemento determina un passo del percorso per disegnare il grafico. +E' una astrazione sul PATH SVG effettivo, e puo' avere alcuni valori speciali: +[ [ 'TEXT', testo, x, y ] ] +[ [ 'CIRCLE', x, y, raggio ] ] +[ [ 'RECT', x1, y1, x2, y2, rounded ] ] (x1,y1 dovrebbero essere sempre le coordinate in alto a sx) +[ [ 'SLICE', x, y, raggio, raggio int, angolo1, angolo2 ] ] (gli angoli sono in gradi) +[ [ 'RELEMENT', element ] ] (elemento Raphael gia' disegnato) +[ [ 'DOMELEMENT', element ] ] (elemento DOM - in genere un DIV html - già disegnato) +[ ... Path SVG ... ] + +------------------------------------------------------------------------ + +Z-INDEX: +0 : base +10 : tooltip +20 : interactive area (tutti gli elementi innescati dalla interactive area dovrebbero essere < 20) +25 : label / balloons (potrebbero essere resi cliccabili dall'esterno, quindi > 20) + +------------------------------------------------------------------------ + +USEFUL RESOURCES: + +http://docs.jquery.com/Plugins/Authoring +http://www.learningjquery.com/2007/10/a-plugin-development-pattern +http://dean.edwards.name/packer/2/usage/#special-chars + +http://raphaeljs.com/reference.html#attr + +TODO +* ottimizzare common.areaProps +* rifare la posizione del tooltip del pie +* ripristinare shadow + +*********************************************/ +/********* Source File: src/elycharts_manager_anchor.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var common = $.elycharts.common; + +/*********************************************************************** + * FEATURE: ANCHOR + * + * Permette di collegare i dati del grafico con delle aree esterne, + * identificate dal loro selettore CSS, e di interagire con esse. + **********************************************************************/ + +$.elycharts.anchormanager = { + + afterShow : function(env, pieces) { + // Prendo le aree gestite da mouseAreas, e metto i miei listener + // Non c'e' bisogno di gestire il clean per una chiamata successiva, lo fa gia' il mouseareamanager + // Tranne per i bind degli eventi jquery + + if (!env.opt.anchors) + return; + + if (!env.anchorBinds) + env.anchorBinds = []; + + while (env.anchorBinds.length) { + var b = env.anchorBinds.pop(); + $(b[0]).unbind(b[1], b[2]); + } + + for (var i = 0; i < env.mouseAreas.length; i++) { + var serie = env.mouseAreas[i].piece ? env.mouseAreas[i].piece.serie : false; + var anc; + if (serie) + anc = env.opt.anchors[serie][env.mouseAreas[i].index]; + else + anc = env.opt.anchors[env.mouseAreas[i].index]; + + if (anc && env.mouseAreas[i].props.anchor && env.mouseAreas[i].props.anchor.highlight) { + + (function(env, mouseAreaData, anc, caller) { + + var f1 = function() { caller.anchorMouseOver(env, mouseAreaData); }; + var f2 = function() { caller.anchorMouseOut(env, mouseAreaData); }; + if (!env.mouseAreas[i].props.anchor.useMouseEnter) { + env.anchorBinds.push([anc, 'mouseover', f1]); + env.anchorBinds.push([anc, 'mouseout', f2]); + $(anc).mouseover(f1); + $(anc).mouseout(f2); + } else { + env.anchorBinds.push([anc, 'mouseenter', f1]); + env.anchorBinds.push([anc, 'mouseleave', f2]); + $(anc).mouseenter(f1); + $(anc).mouseleave(f2); + } + })(env, env.mouseAreas[i], anc, this); + } + } + + env.onAnchors = []; + }, + + anchorMouseOver : function(env, mouseAreaData) { + $.elycharts.highlightmanager.onMouseOver(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + }, + + anchorMouseOut : function(env, mouseAreaData) { + $.elycharts.highlightmanager.onMouseOut(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + }, + + onMouseOver : function(env, serie, index, mouseAreaData) { + if (!env.opt.anchors) + return; + + if (mouseAreaData.props.anchor && mouseAreaData.props.anchor.addClass) { + //var serie = mouseAreaData.piece ? mouseAreaData.piece.serie : false; + var anc; + if (serie) + anc = env.opt.anchors[serie][mouseAreaData.index]; + else + anc = env.opt.anchors[mouseAreaData.index]; + if (anc) { + $(anc).addClass(mouseAreaData.props.anchor.addClass); + env.onAnchors.push([anc, mouseAreaData.props.anchor.addClass]); + } + } + }, + + onMouseOut : function(env, serie, index, mouseAreaData) { + if (!env.opt.anchors) + return; + + while (env.onAnchors.length > 0) { + var o = env.onAnchors.pop(); + $(o[0]).removeClass(o[1]); + } + } +} + +$.elycharts.featuresmanager.register($.elycharts.anchormanager, 30); + +})(jQuery); +/********* Source File: src/elycharts_manager_animation.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var common = $.elycharts.common; + +/*********************************************************************** + * ANIMATIONMANAGER + **********************************************************************/ + +$.elycharts.animationmanager = { + + beforeShow : function(env, pieces) { + if (!env.newopt) + this.startAnimation(env, pieces); + else + this.stepAnimation(env, pieces); + }, + + stepAnimation : function(env, pieces) { + pieces = this._stepAnimationInt(env, pieces); + }, + + _stepAnimationInt : function(env, pieces, section, serie, internal) { + for (var i = 0; i < pieces.length; i++) { + var animationProps = common.areaProps(env, section ? section : pieces[i].section, serie ? serie : pieces[i].serie); + if (animationProps && animationProps.stepAnimation) + animationProps = animationProps.stepAnimation; + else + animationProps = env.opt.features.animation.stepAnimation; + + if (typeof pieces[i].paths == 'undefined') { + if (animationProps && animationProps.active && pieces[i].animation) { + pieces[i].animation.speed = animationProps && animationProps.speed ? animationProps.speed : 300; + pieces[i].animation.easing = animationProps && animationProps.easing ? animationProps.easing : ''; + pieces[i].animation.delay = animationProps && animationProps.delay ? animationProps.delay : 0; + if (!pieces[i].animation.element) + pieces[i].animation.startAttr = {opacity : 0}; + } + } else { + this._stepAnimationInt(env, pieces[i].paths, pieces[i].section, pieces[i].serie, true); + } + } + }, + + startAnimation : function(env, pieces) { + for (var i = 0; i < pieces.length; i++) + if (pieces[i].paths || pieces[i].path) { + var props = common.areaProps(env, pieces[i].section, pieces[i].serie); + if (props && props.startAnimation) + props = props.startAnimation; + else + props = env.opt.features.animation.startAnimation; + + if (props && props.active) { + if (props.type == 'simple' || pieces[i].section != 'Series') + this.animationSimple(env, props, pieces[i]); + if (props.type == 'grow') + this.animationGrow(env, props, pieces[i]); + if (props.type == 'avg') + this.animationAvg(env, props, pieces[i]); + if (props.type == 'reg') + this.animationReg(env, props, pieces[i]); + } + } + }, + + /** + * Inserisce i dati base di animazione del piece e la transizione di attributi + */ + _animationPiece : function(piece, animationProps, subSection) { + if (piece.paths) { + for (var i = 0; i < piece.paths.length; i++) + this._animationPiece(piece.paths[i], animationProps, subSection); + } else if (piece.path) { + piece.animation = { + speed : animationProps.speed, + easing : animationProps.easing, + delay : animationProps.delay, + startPath : [], + startAttr : common._clone(piece.attr) + }; + if (animationProps.propsTo) + piece.attr = $.extend(true, piece.attr, animationProps.propsTo); + if (animationProps.propsFrom) + piece.animation.startAttr = $.extend(true, piece.animation.startAttr, animationProps.propsFrom); + if (subSection && animationProps[subSection.toLowerCase() + 'PropsFrom']) + piece.animation.startAttr = $.extend(true, piece.animation.startAttr, animationProps[subSection.toLowerCase() + 'PropsFrom']); + + if (typeof piece.animation.startAttr.opacity != 'undefined' && typeof piece.attr.opacity == 'undefined') + piece.attr.opacity = 1; + } + }, + + animationSimple : function(env, props, piece) { + this._animationPiece(piece, props, piece.subSection); + }, + + animationGrow : function(env, props, piece) { + this._animationPiece(piece, props, piece.subSection); + var i, npath, y; + + switch (env.opt.type) { + case 'line': + y = env.height - env.opt.margins[2]; + switch (piece.subSection) { + case 'Plot': + if (!piece.paths) { + npath = [ 'LINE', [], piece.path[0][2]]; + for (i = 0; i < piece.path[0][1].length; i++) + npath[1].push([ piece.path[0][1][i][0], piece.path[0][1][i][1] == null ? null : y ]); + piece.animation.startPath.push(npath); + + } else { + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) + piece.paths[i].animation.startPath.push([ 'RECT', piece.paths[i].path[0][1], y, piece.paths[i].path[0][3], y ]); + } + break; + case 'Fill': + npath = [ 'LINEAREA', [], [], piece.path[0][3]]; + for (i = 0; i < piece.path[0][1].length; i++) { + npath[1].push([ piece.path[0][1][i][0], piece.path[0][1][i][1] == null ? null : y ]); + npath[2].push([ piece.path[0][2][i][0], piece.path[0][2][i][1] == null ? null : y ]); + } + piece.animation.startPath.push(npath); + + break; + case 'Dot': + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) + piece.paths[i].animation.startPath.push(['CIRCLE', piece.paths[i].path[0][1], y, piece.paths[i].path[0][3]]); + break; + } + break; + + case 'pie': + if (piece.subSection == 'Plot') + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path && piece.paths[i].path[0][0] == 'SLICE') + piece.paths[i].animation.startPath.push([ 'SLICE', piece.paths[i].path[0][1], piece.paths[i].path[0][2], piece.paths[i].path[0][4] + piece.paths[i].path[0][3] * 0.1, piece.paths[i].path[0][4], piece.paths[i].path[0][5], piece.paths[i].path[0][6] ]); + + break; + + case 'funnel': + alert('Unsupported animation GROW for funnel'); + break; + + case 'barline': + var x; + if (piece.section == 'Series' && piece.subSection == 'Plot') { + if (!props.subType) + x = env.opt.direction != 'rtl' ? env.opt.margins[3] : env.width - env.opt.margins[1]; + else if (props.subType == 1) + x = env.opt.direction != 'rtl' ? env.width - env.opt.margins[1] : env.opt.margins[3]; + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) { + if (!props.subType || props.subType == 1) + piece.paths[i].animation.startPath.push([ 'RECT', x, piece.paths[i].path[0][2], x, piece.paths[i].path[0][4], piece.paths[i].path[0][5] ]); + else { + y = (piece.paths[i].path[0][2] + piece.paths[i].path[0][4]) / 2; + piece.paths[i].animation.startPath.push([ 'RECT', piece.paths[i].path[0][1], y, piece.paths[i].path[0][3], y, piece.paths[i].path[0][5] ]); + } + } + } + + break; + } + }, + + _animationAvgXYArray : function(arr) { + var res = [], avg = 0, i; + var count = 0; + for (i = 0; i < arr.length; i++) if (arr[i][1] != null) { + avg += arr[i][1]; + count++; + } + avg = avg / count; + for (i = 0; i < arr.length; i++) + res.push([ arr[i][0], arr[i][1] == null ? null : avg ]); + return res; + }, + + animationAvg : function(env, props, piece) { + this._animationPiece(piece, props, piece.subSection); + + var avg = 0, i, l; + switch (env.opt.type) { + case 'line': + switch (piece.subSection) { + case 'Plot': + if (!piece.paths) { + // LINE + piece.animation.startPath.push([ 'LINE', this._animationAvgXYArray(piece.path[0][1]), piece.path[0][2] ]); + + } else { + // BAR + l = 0; + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) { + l ++; + avg += piece.paths[i].path[0][2]; + } + avg = avg / l; + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) + piece.paths[i].animation.startPath.push([ "RECT", piece.paths[i].path[0][1], avg, piece.paths[i].path[0][3], piece.paths[i].path[0][4] ]); + } + break; + + case 'Fill': + piece.animation.startPath.push([ 'LINEAREA', this._animationAvgXYArray(piece.path[0][1]), this._animationAvgXYArray(piece.path[0][2]), piece.path[0][3] ]); + + break; + + case 'Dot': + l = 0; + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) { + l ++; + avg += piece.paths[i].path[0][2]; + } + avg = avg / l; + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) + piece.paths[i].animation.startPath.push(['CIRCLE', piece.paths[i].path[0][1], avg, piece.paths[i].path[0][3]]); + break; + } + break; + + case 'pie': + var delta = 360 / piece.paths.length; + + if (piece.subSection == 'Plot') + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path && piece.paths[i].path[0][0] == 'SLICE') + piece.paths[i].animation.startPath.push([ 'SLICE', piece.paths[i].path[0][1], piece.paths[i].path[0][2], piece.paths[i].path[0][3], piece.paths[i].path[0][4], i * delta, (i + 1) * delta ]); + + break; + + case 'funnel': + alert('Unsupported animation AVG for funnel'); + break; + + case 'barline': + alert('Unsupported animation AVG for barline'); + break; + } + }, + + _animationRegXYArray : function(arr) { + var res = []; + var c = arr.length; + + var start = 0; + var end = c - 1; + + while (arr[start][1] == null) start++; + while (arr[end][1] == null) end--; + + var y1 = arr[0][1]; + var y2 = arr[c - 1][1]; + + for (var i = 0; i < arr.length; i++) { + if (arr[i][1] == null) res.push([ arr[i][0], null ]); + else { + res.push([ arr[i][0], arr[start][1] + (arr[end][1] - arr[start][1]) / (end - start) * ( i - start) ]); + } + } + + return res; + }, + + animationReg : function(env, props, piece) { + this._animationPiece(piece, props, piece.subSection); + var i, c, y1, y2; + + switch (env.opt.type) { + case 'line': + switch (piece.subSection) { + case 'Plot': + if (!piece.paths) { + // LINE + piece.animation.startPath.push([ 'LINE', this._animationRegXYArray(piece.path[0][1]), piece.path[0][2] ]); + + } else { + // BAR + c = piece.paths.length; + if (c > 1) { + for (i = 0; !piece.paths[i].path && i < piece.paths.length; i++) {} + y1 = piece.paths[i].path ? common.getY(piece.paths[i].path[0]) : 0; + for (i = piece.paths.length - 1; !piece.paths[i].path && i >= 0; i--) {} + y2 = piece.paths[i].path ? common.getY(piece.paths[i].path[0]) : 0; + + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) + piece.paths[i].animation.startPath.push([ "RECT", piece.paths[i].path[0][1], y1 + (y2 - y1) / (c - 1) * i, piece.paths[i].path[0][3], piece.paths[i].path[0][4] ]); + } + } + break; + + case 'Fill': + piece.animation.startPath.push([ 'LINEAREA', this._animationRegXYArray(piece.path[0][1]), this._animationRegXYArray(piece.path[0][2]), piece.path[0][3] ]); + break; + + case 'Dot': + c = piece.paths.length; + if (c > 1) { + for (i = 0; !piece.paths[i].path && i < piece.paths.length; i++) {} + y1 = piece.paths[i].path ? common.getY(piece.paths[i].path[0]) : 0; + for (i = piece.paths.length - 1; !piece.paths[i].path && i >= 0; i--) {} + y2 = piece.paths[i].path ? common.getY(piece.paths[i].path[0]) : 0; + + for (i = 0; i < piece.paths.length; i++) + if (piece.paths[i].path) + piece.paths[i].animation.startPath.push(['CIRCLE', piece.paths[i].path[0][1], y1 + (y2 - y1) / (c - 1) * i, piece.paths[i].path[0][3]]); + } + break; + } + break; + + case 'pie': + alert('Unsupported animation REG for pie'); + break; + + case 'funnel': + alert('Unsupported animation REG for funnel'); + break; + + case 'barline': + alert('Unsupported animation REG for barline'); + break; + } + } +} + +$.elycharts.featuresmanager.register($.elycharts.animationmanager, 10); + +/*********************************************************************** + * FRAMEANIMATIONMANAGER + **********************************************************************/ + +$.elycharts.frameanimationmanager = { + + beforeShow : function(env, pieces) { + if (env.opt.features.frameAnimation.active) + $(env.container.get(0)).css(env.opt.features.frameAnimation.cssFrom); + }, + + afterShow : function(env, pieces) { + if (env.opt.features.frameAnimation.active) + env.container.animate(env.opt.features.frameAnimation.cssTo, env.opt.features.frameAnimation.speed, env.opt.features.frameAnimation.easing); + } +}; + +$.elycharts.featuresmanager.register($.elycharts.frameanimationmanager, 90); + +})(jQuery); +/********* Source File: src/elycharts_manager_highlight.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var common = $.elycharts.common; + +/*********************************************************************** + * FEATURE: HIGHLIGHT + * + * Permette di evidenziare in vari modi l'area in cui si passa con il + * mouse. + **********************************************************************/ + +$.elycharts.highlightmanager = { + + removeHighlighted : function(env, full) { + if (env.highlighted) + while (env.highlighted.length > 0) { + var o = env.highlighted.pop(); + if (o.piece) { + if (full) + common.animationStackPush(env, o.piece, o.piece.element, common.getPieceFullAttr(env, o.piece), o.cfg.restoreSpeed, o.cfg.restoreEasing, 0, true); + } else + o.element.remove(); + } + }, + + afterShow : function(env, pieces) { + if (env.highlighted && env.highlighted.length > 0) + this.removeHighlighted(env, false); + env.highlighted = []; + }, + + onMouseOver : function(env, serie, index, mouseAreaData) { + var path, element; + // TODO Se non e' attivo l'overlay (per la serie o per tutto) e' inutile fare il resto + + // Cerco i piece da evidenziare (tutti quelli che sono costituiti da path multipli) + for (var i = 0; i < mouseAreaData.pieces.length; i++) + + // Il loop sotto estrae solo i pieces con array di path (quindi non i line o i fill del linechart ... ma il resto si) + if (mouseAreaData.pieces[i].section == 'Series' && mouseAreaData.pieces[i].paths + && (!serie || mouseAreaData.pieces[i].serie == serie) + && mouseAreaData.pieces[i].paths[index] && mouseAreaData.pieces[i].paths[index].element) { + var piece = mouseAreaData.pieces[i].paths[index]; + element = piece.element; + path = piece.path; + var attr = common.getElementOriginalAttrs(element); + var newattr = false; // In caso la geometria dell'oggetto è modificata mediante attr (es: per circle) qui memorizza i nuovi attributi + var props = serie ? mouseAreaData.props : common.areaProps(env, mouseAreaData.pieces[i].section, mouseAreaData.pieces[i].serie); + var pelement, ppiece, ppath; + if (path && props.highlight) { + if (props.highlight.scale) { + var scale = props.highlight.scale; + if (typeof scale == 'number') + scale = [scale, scale]; + + if (path[0][0] == 'RECT') { + var w = path[0][3] - path[0][1]; + var h = path[0][4] - path[0][2]; + path = [ [ 'RECT', path[0][1], path[0][2] - h * (scale[1] - 1), path[0][3] + w * (scale[0] - 1), path[0][4] ] ]; + common.animationStackPush(env, piece, element, common.getSVGProps(env, path), props.highlight.scaleSpeed, props.highlight.scaleEasing); + } + else if (path[0][0] == 'CIRCLE') { + // I pass directly new radius + newattr = {r : path[0][3] * scale[0]}; + common.animationStackPush(env, piece, element, newattr, props.highlight.scaleSpeed, props.highlight.scaleEasing); + } + else if (path[0][0] == 'SLICE') { + // Per lo slice x e' il raggio, y e' l'angolo + var d = (path[0][6] - path[0][5]) * (scale[1] - 1) / 2; + if (d > 90) + d = 90; + path = [ [ 'SLICE', path[0][1], path[0][2], path[0][3] * scale[0], path[0][4], path[0][5] - d, path[0][6] + d ] ]; + common.animationStackPush(env, piece, element, common.getSVGProps(env, path), props.highlight.scaleSpeed, props.highlight.scaleEasing); + + } else if (env.opt.type == 'funnel') { + var dx = (piece.rect[2] - piece.rect[0]) * (scale[0] - 1) / 2; + var dy = (piece.rect[3] - piece.rect[1]) * (scale[1] - 1) / 2; + + // Specifico di un settore del funnel + // SHOULD ALREADY BE DONE BY core common.animationStackStart(env); + path = [ common.movePath(env, [ path[0]], [-dx, -dy])[0], + common.movePath(env, [ path[1]], [+dx, -dy])[0], + common.movePath(env, [ path[2]], [+dx, +dy])[0], + common.movePath(env, [ path[3]], [-dx, +dy])[0], + path[4] ]; + common.animationStackPush(env, piece, element, common.getSVGProps(env, path), props.highlight.scaleSpeed, props.highlight.scaleEasing, 0, true); + + // Se c'e' un piece precedente lo usa, altrimenti cerca un topSector per la riduzione + pelement = false; + if (index > 0) { + ppiece = mouseAreaData.pieces[i].paths[index - 1]; + pelement = ppiece.element; + ppath = ppiece.path; + } else { + ppiece = common.findInPieces(mouseAreaData.pieces, 'Sector', 'top'); + if (ppiece) { + pelement = ppiece.element; + ppath = ppiece.path; + } + } + if (pelement) { + //pattr = common.getElementOriginalAttrs(pelement); + ppath = [ + ppath[0], ppath[1], + common.movePath(env, [ ppath[2]], [+dx, -dy])[0], + common.movePath(env, [ ppath[3]], [-dx, -dy])[0], + ppath[4] ]; + common.animationStackPush(env, ppiece, pelement, common.getSVGProps(env, ppath), props.highlight.scaleSpeed, props.highlight.scaleEasing, 0, true); + env.highlighted.push({piece : ppiece, cfg : props.highlight}); + } + + // Se c'e' un piece successivo lo usa, altrimenti cerca un bottomSector per la riduzione + pelement = false; + if (index < mouseAreaData.pieces[i].paths.length - 1) { + ppiece = mouseAreaData.pieces[i].paths[index + 1]; + pelement = ppiece.element; + ppath = ppiece.path; + } else { + ppiece = common.findInPieces(mouseAreaData.pieces, 'Sector', 'bottom'); + if (ppiece) { + pelement = ppiece.element; + ppath = ppiece.path; + } + } + if (pelement) { + //var pattr = common.getElementOriginalAttrs(pelement); + ppath = [ + common.movePath(env, [ ppath[0]], [-dx, +dy])[0], + common.movePath(env, [ ppath[1]], [+dx, +dy])[0], + ppath[2], ppath[3], + ppath[4] ]; + common.animationStackPush(env, ppiece, pelement, common.getSVGProps(env, ppath), props.highlight.scaleSpeed, props.highlight.scaleEasing, 0, true); + env.highlighted.push({piece : ppiece, cfg : props.highlight}); + } + // SHOULD ALREADY BE DONE BY core: common.animationStackEnd(env); + } + /* Con scale non va bene + if (!attr.scale) + attr.scale = [1, 1]; + element.attr({scale : [scale[0], scale[1]]}); */ + } + if (props.highlight.newProps) { + for (var a in props.highlight.newProps) + if (typeof attr[a] == 'undefined') + attr[a] = false; + common.animationStackPush(env, piece, element, props.highlight.newProps); + } + if (props.highlight.move) { + var offset = $.isArray(props.highlight.move) ? props.highlight.move : [props.highlight.move, 0]; + path = common.movePath(env, path, offset); + common.animationStackPush(env, piece, element, common.getSVGProps(env, path), props.highlight.moveSpeed, props.highlight.moveEasing); + } + + //env.highlighted.push({element : element, attr : attr}); + env.highlighted.push({piece : piece, cfg : props.highlight}); + + if (props.highlight.overlayProps) { + // NOTA: path e' il path modificato dai precedenti (cosi' l'overlay tiene conto della cosa), deve guardare anche a newattr + //BIND: mouseAreaData.listenerDisabled = true; + element = common.showPath(env, path); + if (newattr) + element.attr(newattr); + element.attr(props.highlight.overlayProps); + //BIND: $(element.node).unbind().mouseover(mouseAreaData.mouseover).mouseout(mouseAreaData.mouseout); + // Se metto immediatamente il mouseAreaData.listenerDisabled poi va comunque un mouseout dalla vecchia area e va + // in loop. TODO Rivedere e sistemare anche per tooltip + //BIND: setTimeout(function() { mouseAreaData.listenerDisabled = false; }, 10); + attr = false; + env.highlighted.push({element : element, attr : attr, cfg : props.highlight}); + } + } + } + + if (env.opt.features.highlight.indexHighlight && env.opt.type == 'line') { + var t = env.opt.features.highlight.indexHighlight; + if (t == 'auto') + t = (env.indexCenter == 'bar' ? 'bar' : 'line'); + + var delta1 = (env.width - env.opt.margins[3] - env.opt.margins[1]) / (env.opt.labels.length > 0 ? env.opt.labels.length : 1); + var delta2 = (env.width - env.opt.margins[3] - env.opt.margins[1]) / (env.opt.labels.length > 1 ? env.opt.labels.length - 1 : 1); + var lineCenter = true; + + switch (t) { + case 'bar': + path = [ ['RECT', env.opt.margins[3] + index * delta1, env.opt.margins[0] , + env.opt.margins[3] + (index + 1) * delta1, env.height - env.opt.margins[2] ] ]; + break; + + case 'line': + lineCenter = false; + case 'barline': + var x = Math.round((lineCenter ? delta1 / 2 : 0) + env.opt.margins[3] + index * (lineCenter ? delta1 : delta2)); + path = [[ 'M', x, env.opt.margins[0]], ['L', x, env.height - env.opt.margins[2]]]; + } + if (path) { + //BIND: mouseAreaData.listenerDisabled = true; + element = common.showPath(env, path).attr(env.opt.features.highlight.indexHighlightProps); + //BIND: $(element.node).unbind().mouseover(mouseAreaData.mouseover).mouseout(mouseAreaData.mouseout); + //BIND: setTimeout(function() { mouseAreaData.listenerDisabled = false; }, 10); + env.highlighted.push({element : element, attr : false, cfg : env.opt.features.highlight}); + } + } + }, + + onMouseOut : function(env, serie, index, mouseAreaData) { + this.removeHighlighted(env, true); + } + +}; + +$.elycharts.featuresmanager.register($.elycharts.highlightmanager, 21); + +})(jQuery); +/********* Source File: src/elycharts_manager_label.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var common = $.elycharts.common; + +/*********************************************************************** + * FEATURE: LABELS + * + * Permette di visualizzare in vari modi le label del grafico. + * In particolare per pie e funnel permette la visualizzazione all'interno + * delle fette. + * Per i line chart le label sono visualizzate già nella gestione assi. + * + * TODO: + * - Comunque per i line chart si potrebbe gestire la visualizzazione + * all'interno delle barre, o sopra i punti. + **********************************************************************/ + +$.elycharts.labelmanager = { + + beforeShow : function(env, pieces) { + + if (!common.executeIfChanged(env, ['labels', 'values', 'series'])) + return; + + if (env.opt.labels && (env.opt.type == 'pie' || env.opt.type == 'funnel')) { + var /*lastSerie = false, */lastIndex = false; + var paths; + + for (var i = 0; i < pieces.length; i++) { + if (pieces[i].section == 'Series' && pieces[i].subSection == 'Plot') { + var props = common.areaProps(env, 'Series', pieces[i].serie); + if (env.emptySeries && env.opt.series.empty) + props.label = $.extend(true, props.label, env.opt.series.empty.label); + if (props && props.label && props.label.active) { + paths = []; + for (var index = 0; index < pieces[i].paths.length; index++) + if (pieces[i].paths[index].path) { + //lastSerie = pieces[i].serie; + lastIndex = index; + paths.push(this.showLabel(env, pieces[i], pieces[i].paths[index], pieces[i].serie, index, pieces)); + } else + paths.push({ path : false, attr : false }); + pieces.push({ section : pieces[i].section, serie : pieces[i].serie, subSection : 'Label', paths: paths }); + } + } + else if (pieces[i].section == 'Sector' && pieces[i].serie == 'bottom' && !pieces[i].subSection && lastIndex < env.opt.labels.length - 1) { + paths = []; + paths.push(this.showLabel(env, pieces[i], pieces[i], 'Series', env.opt.labels.length - 1, pieces)); + pieces.push({ section : pieces[i].section, serie : pieces[i].serie, subSection : 'Label', paths: paths }); + } + } + + } + }, + + showLabel : function(env, piece, path, serie, index, pieces) { + var pp = common.areaProps(env, 'Series', serie, index); + if (env.opt.labels[index] || pp.label.label) { + var p = path; + var label = pp.label.label ? pp.label.label : env.opt.labels[index]; + var center = common.getCenter(p, pp.label.offset); + if (!pp.label.html) { + var attr = pp.label.props; + if (pp.label.frameAnchor) { + attr = common._clone(pp.label.props); + attr['text-anchor'] = pp.label.frameAnchor[0]; + attr['alignment-baseline'] = pp.label.frameAnchor[1]; + } + /*pieces.push({ + path : [ [ 'TEXT', label, center[0], center[1] ] ], attr : attr, + section: 'Series', serie : serie, index : index, subSection : 'Label' + });*/ + return { path : [ [ 'TEXT', label, center[0], center[1] ] ], attr : attr }; + + } else { + var opacity = 1; + var style = common._clone(pp.label.style); + var set_opacity = (typeof style.opacity != 'undefined') + if (set_opacity) { + opacity = style.opacity; + style.opacity = 0; + } + style.position = 'absolute'; + style['z-index'] = 25; + + var el; + if (typeof label == 'string') + el = $('
    ' + label + '
    ').css(style).prependTo(env.container); + else + el = $(label).css(style).prependTo(env.container); + + // Centramento corretto label + if (env.opt.features.debug.active && el.height() == 0) + alert('DEBUG: Al gestore label e\' stata passata una label ancora senza dimensioni, quindi ancora non disegnata. Per questo motivo il posizionamento potrebbe non essere correto.'); + var posX = center[0]; + var posY = center[1]; + if (!pp.label.frameAnchor || pp.label.frameAnchor[0] == 'middle') + posX -= el.width() / 2; + else if (pp.label.frameAnchor && pp.label.frameAnchor[0] == 'end') + posX -= el.width(); + if (!pp.label.frameAnchor || pp.label.frameAnchor[1] == 'middle') + posY -= el.height() / 2; + else if (pp.label.frameAnchor && pp.label.frameAnchor[1] == 'top') + posY -= el.height(); + if (set_opacity) + el.css({ margin: posY + 'px 0 0 ' + posX + 'px', opacity : opacity}); + else + el.css({ margin: posY + 'px 0 0 ' + posX + 'px'}); + + /*pieces.push({ + path : [ [ 'DOMELEMENT', el ] ], attr : false, + section: 'Series', serie : serie, index : index, subSection : 'Label' + });*/ + return { path : [ [ 'DOMELEMENT', el ] ], attr : false }; + + } + } + return false; + } +} + +$.elycharts.featuresmanager.register($.elycharts.labelmanager, 5); + +})(jQuery); +/********* Source File: src/elycharts_manager_legend.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var common = $.elycharts.common; + +/*********************************************************************** + * FEATURE: LEGEND + **********************************************************************/ + +$.elycharts.legendmanager = { + + afterShow : function(env, pieces) { + // TODO the whole thing should simply return "pieces" whose visibility is handled by core, so to enable animations and + // make things more generic. + if (env.legenditems) { + for (item in env.legenditems) { + env.legenditems[item].remove(); + } + env.legenditems = false; + } + if (!env.opt.legend || env.opt.legend.length == 0) + return; + + var props = env.opt.features.legend; + + if (props === false) return; + + var propsx = props.x; + if (propsx == 'auto') { + var autox = 1; + propsx = 0; + } + var propswidth = props.width; + if (propswidth == 'auto') { + var autowidth = 1; + propswidth = env.width; + } + + var wauto = 0; + var items = []; + // env.opt.legend normalmente è { serie : 'Legend', ... }, per i pie invece { serie : ['Legend', ...], ... } + var legendCount = 0; + var serie, data, h, w, x, y, xd; + for (serie in env.opt.legend) { + if (env.opt.type != 'pie') + legendCount ++; + else + legendCount += env.opt.legend[serie].length; + } + var i = 0; + for (serie in env.opt.legend) { + if (env.opt.type != 'pie') + data = [ env.opt.legend[serie] ]; + else + data = env.opt.legend[serie]; + + for (var j = 0; j < data.length; j++) { + var sprops = common.areaProps(env, 'Series', serie, env.opt.type == 'pie' ? j : false); + var computedProps = $.extend(true, {}, props); + + if (sprops.legend) + computedProps = $.extend(true, computedProps, sprops.legend); + + var color = common.getItemColor(env, serie, env.opt.type == 'pie' ? j : false); + if (color) { + common.colorize(env, computedProps, [['dotProps', 'fill']], color); + } + + // legacy support for legend dot color inherited from pie "fill" + // TODO maybe we should simply remove this and leave the "color" support only + if (!computedProps.dotProps.fill && env.opt.type == 'pie') { + if (sprops.plotProps && sprops.plotProps.fill) + computedProps.dotProps.fill = sprops.plotProps.fill; + } + + var hMargin = props.margins ? props.margins[0] + props.margins[2] : 0; + var wMargin = props.margins ? props.margins[1] + props.margins[3] : 0; + var tMargin = props.margins ? props.margins[0] : 0; + var lMargin = props.margins ? props.margins[3] : 0; + + if (!props.horizontal) { + // Posizione dell'angolo in alto a sinistra + h = (props.height - hMargin) / legendCount; + w = propswidth - wMargin; + x = Math.floor(propsx + lMargin); + y = Math.floor(props.y + tMargin + h * i); + } else { + h = props.height - hMargin; + if (!props.itemWidth || props.itemWidth == 'fixed') { + w = (propswidth - wMargin) / legendCount; + x = Math.floor(propsx + lMargin + w * i); + } else { + w = (propswidth - wMargin) - wauto; + x = propsx + lMargin + wauto; + } + y = Math.floor(props.y + tMargin); + } + + if (computedProps.dotType == "rect") { + items.push(common.showPath(env, [ [ 'RECT', props.dotMargins[0] + x, y + Math.floor((h - computedProps.dotHeight) / 2), props.dotMargins[0] + x + computedProps.dotWidth, y + Math.floor((h - computedProps.dotHeight) / 2) + computedProps.dotHeight, computedProps.dotR ] ]).attr(computedProps.dotProps)); + xd = props.dotMargins[0] + computedProps.dotWidth + props.dotMargins[1]; + } else if (computedProps.dotType == "circle") { + items.push(common.showPath(env, [ [ 'CIRCLE', props.dotMargins[0] + x + computedProps.dotR, y + (h / 2), computedProps.dotR ] ]).attr(computedProps.dotProps)); + xd = props.dotMargins[0] + computedProps.dotR * 2 + props.dotMargins[1]; + } + + var text = data[j]; + var t = common.showPath(env, [ [ 'TEXT', text, x + xd, y + Math.ceil(h / 2) + (Raphael.VML ? 2 : 0) ] ]).attr({"text-anchor" : "start"}).attr(computedProps.textProps); //.hide(); + items.push(t); + while (t.getBBox().width > (w - xd) && t.getBBox().width > 10) { + text = text.substring(0, text.length - 1); + t.attr({text : text}); + } + t.show(); + + if (props.horizontal && props.itemWidth == 'auto') + wauto += xd + t.getBBox().width + 4; + else if (!props.horizontal && autowidth) + wauto = t.getBBox().width + xd > wauto ? t.getBBox().width + xd : wauto; + else + wauto += w; + + i++; + } + } + + if (autowidth) + propswidth = wauto + props.margins[3] + props.margins[1] - 1; + if (autox) { + propsx = Math.floor((env.width - propswidth) / 2); + for (i in items) { + if (items[i].attrs.x) + items[i].attr('x', items[i].attrs.x + propsx); + else + items[i].attr('path', common.movePath(env, items[i].attrs.path, [propsx, 0])); + } + } + var borderPath = [ [ 'RECT', propsx, props.y, propsx + propswidth, props.y + props.height, props.r ] ]; + var border = common.showPath(env, borderPath).attr(props.borderProps); + + // The legend rectangle is written as the last one because it depends on the sizes of the contents but it should + // be drawn behind the others, so at the end we bring to front all items but the border + for(i in items) items[i].toFront(); + + items.unshift(border); + + env.legenditems = items; + } +} + +$.elycharts.featuresmanager.register($.elycharts.legendmanager, 90); + +})(jQuery); +/********* Source File: src/elycharts_manager_mouse.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var featuresmanager = $.elycharts.featuresmanager; +var common = $.elycharts.common; + +/*********************************************************************** + * MOUSEMANAGER + **********************************************************************/ + +$.elycharts.mousemanager = { + + clear : function(env) { + if (env.mouseLayer) { + env.mouseLayer.remove(); + env.mouseLayer = null; + env.mousePaper.clear(); + env.mousePaper.remove(); + env.mousePaper = null; + env.mouseTimer = null; + env.mouseAreas = null; + // NOTE: do we also need to unbind mouseover/mouseout from areas or is this handled automatically by Raphael? + } + }, + + afterShow : function(env, pieces) { + if (!env.opt.interactive) + return; + + this.clear(env); + + env.mouseLayer = $('
    ').css({position : 'absolute', 'z-index' : 20, opacity : 1}).prependTo(env.container); + env.mousePaper = common._RaphaelInstance(env.mouseLayer.get(0), env.width, env.height); + var paper = env.mousePaper; + + if (env.opt.features.debug.active && typeof DP_Debug != 'undefined') { + env.paper.text(env.width, env.height - 5, 'DEBUG').attr({ 'text-anchor' : 'end', stroke: 'red', opacity: .1 }); + paper.text(env.width, env.height - 5, 'DEBUG').attr({ 'text-anchor' : 'end', stroke: 'red', opacity: .1 }).click(function() { + DP_Debug.dump(env.opt, '', false, 4); + }); + } + + var i, j; + + // Adding mouseover only in right area, based on pieces + env.mouseAreas = []; + if (env.opt.features.mousearea.type == 'single') { + // SINGLE: Every serie's index is an area + for (i = 0; i < pieces.length; i++) { + if (pieces[i].mousearea) { + // pathstep + if (!pieces[i].paths) { + // path standard, generating an area for each point + if (pieces[i].path.length >= 1 && (pieces[i].path[0][0] == 'LINE' || pieces[i].path[0][0] == 'LINEAREA')) + for (j = 0; j < pieces[i].path[0][1].length; j++) { + var props = common.areaProps(env, pieces[i].section, pieces[i].serie); + if (props.mouseareaShowOnNull || pieces[i].section != 'Series' || env.opt.values[pieces[i].serie][j] != null) + env.mouseAreas.push({ + path : [ [ 'CIRCLE', pieces[i].path[0][1][j][0], pieces[i].path[0][1][j][1], 10 ] ], + piece : pieces[i], + pieces : pieces, + index : j, + props : props + }); + } + + else // Code below is only for standard path - it should be useless now (now there are only LINE and LINEAREA) + // TODO DELETE + for (j = 0; j < pieces[i].path.length; j++) { + env.mouseAreas.push({ + path : [ [ 'CIRCLE', common.getX(pieces[i].path[j]), common.getY(pieces[i].path[j]), 10 ] ], + piece : pieces[i], + pieces : pieces, + index : j, + props : common.areaProps(env, pieces[i].section, pieces[i].serie) + }); + } + + // paths + } else if (pieces[i].paths) { + // Set of paths (bar graph?), generating overlapped areas + for (j = 0; j < pieces[i].paths.length; j++) + if (pieces[i].paths[j].path) + env.mouseAreas.push({ + path : pieces[i].paths[j].path, + piece : pieces[i], + pieces : pieces, + index : j, + props : common.areaProps(env, pieces[i].section, pieces[i].serie) + }); + } + } + } + } else { + // INDEX: Each index (in every serie) is an area + var indexCenter = env.opt.features.mousearea.indexCenter; + if (indexCenter == 'auto') + indexCenter = env.indexCenter; + var start, delta; + if (indexCenter == 'bar') { + delta = (env.width - env.opt.margins[3] - env.opt.margins[1]) / (env.opt.labels.length > 0 ? env.opt.labels.length : 1); + start = env.opt.margins[3]; + } else { + delta = (env.width - env.opt.margins[3] - env.opt.margins[1]) / (env.opt.labels.length > 1 ? env.opt.labels.length - 1 : 1); + start = env.opt.margins[3] - delta / 2; + } + + for (var idx in env.opt.labels) { + // idx can be a string and concatenation results in bad sums. + var index = parseInt(idx); + env.mouseAreas.push({ + path : [ [ 'RECT', start + index * delta, env.height - env.opt.margins[2], start + (index + 1) * delta, env.opt.margins[0] ] ], + piece : false, + pieces : pieces, + index : parseInt(index), + props : env.opt.defaultSeries // TODO common.areaProps(env, 'Plot') + }); + } + } + + var syncenv = false; + if (!env.opt.features.mousearea.syncTag) { + env.mouseareaenv = { chartEnv : false, mouseObj : false, caller : false, inArea : -1, timer : false }; + syncenv = env.mouseareaenv; + } else { + if (!$.elycharts.mouseareaenv) + $.elycharts.mouseareaenv = {}; + if (!$.elycharts.mouseareaenv[env.opt.features.mousearea.syncTag]) + $.elycharts.mouseareaenv[env.opt.features.mousearea.syncTag] = { chartEnv : false, mouseObj : false, caller : false, inArea : -1, timer : false }; + syncenv = $.elycharts.mouseareaenv[env.opt.features.mousearea.syncTag]; + } + for (i = 0; i < env.mouseAreas.length; i++) { + env.mouseAreas[i].area = common.showPath(env, env.mouseAreas[i].path, paper).attr({stroke: "#000", fill: "#fff", opacity: 0}); + + (function(env, obj, objidx, caller, syncenv) { + var piece = obj.piece; + var index = obj.index; + + obj.mouseover = function(e) { + //BIND: if (obj.listenerDisabled) return; + obj.event = e; + clearTimeout(syncenv.timer); + caller.onMouseOverArea(env, piece, index, obj); + + if (syncenv.chartEnv && syncenv.chartEnv.id != env.id) { + // Chart changed, removing old one + syncenv.caller.onMouseExitArea(syncenv.chartEnv, syncenv.mouseObj.piece, syncenv.mouseObj.index, syncenv.mouseObj); + caller.onMouseEnterArea(env, piece, index, obj); + } + else if (syncenv.inArea != objidx) { + if (syncenv.inArea < 0) + caller.onMouseEnterArea(env, piece, index, obj); + else + caller.onMouseChangedArea(env, piece, index, obj); + } + syncenv.chartEnv = env; + syncenv.mouseObj = obj; + syncenv.caller = caller; + syncenv.inArea = objidx; + }; + obj.mouseout = function(e) { + //BIND: if (obj.listenerDisabled) return; + obj.event = e; + clearTimeout(syncenv.timer); + caller.onMouseOutArea(env, piece, index, obj); + syncenv.timer = setTimeout(function() { + syncenv.timer = false; + caller.onMouseExitArea(env, piece, index, obj); + syncenv.chartEnv = false; + syncenv.inArea = -1; + }, env.opt.features.mousearea.areaMoveDelay); + }; + + $(obj.area.node).mouseover(obj.mouseover); + $(obj.area.node).mouseout(obj.mouseout); + })(env, env.mouseAreas[i], i, this, syncenv); + } + }, + + // Called when mouse enter an area + onMouseOverArea : function(env, piece, index, mouseAreaData) { + //console.warn('over', piece.serie, index); + if (env.opt.features.mousearea.onMouseOver) + env.opt.features.mousearea.onMouseOver(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + featuresmanager.onMouseOver(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + }, + + // Called when mouse exit from an area + onMouseOutArea : function(env, piece, index, mouseAreaData) { + //console.warn('out', piece.serie, index); + if (env.opt.features.mousearea.onMouseOut) + env.opt.features.mousearea.onMouseOut(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + featuresmanager.onMouseOut(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + }, + + // Called when mouse enter an area from empty space (= it was in no area before) + onMouseEnterArea : function(env, piece, index, mouseAreaData) { + //console.warn('enter', piece.serie, index); + if (env.opt.features.mousearea.onMouseEnter) + env.opt.features.mousearea.onMouseEnter(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + featuresmanager.onMouseEnter(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + }, + + // Called when mouse enter an area and it was on another area + onMouseChangedArea : function(env, piece, index, mouseAreaData) { + //console.warn('changed', piece.serie, index); + if (env.opt.features.mousearea.onMouseChanged) + env.opt.features.mousearea.onMouseChanged(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + featuresmanager.onMouseChanged(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + }, + + // Called when mouse leaves an area and does not enter in another one (timeout check) + onMouseExitArea : function(env, piece, index, mouseAreaData) { + //console.warn('exit', piece.serie, index); + if (env.opt.features.mousearea.onMouseExit) + env.opt.features.mousearea.onMouseExit(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + featuresmanager.onMouseExit(env, mouseAreaData.piece ? mouseAreaData.piece.serie : false, mouseAreaData.index, mouseAreaData); + } + +} + +$.elycharts.featuresmanager.register($.elycharts.mousemanager, 0); + +})(jQuery); +/********* Source File: src/elycharts_manager_tooltip.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var common = $.elycharts.common; + +/*********************************************************************** + * FEATURE: TOOLTIP + **********************************************************************/ + +$.elycharts.tooltipmanager = { + + clear : function(env) { + if (env.tooltipContainer) { + env.tooltipFrame.clear(); + env.tooltipFrame.remove(); + env.tooltipFrame = null; + env.tooltipFrameElement = null; + env.tooltipContent.remove(); + env.tooltipContent = null; + env.tooltipContainer.remove(); + env.tooltipContainer = null; + } + }, + + afterShow : function(env, pieces) { + this.clear(env); + + if (!$.elycharts.tooltipid) + $.elycharts.tooltipid = 0; + $.elycharts.tooltipid ++; + + // Preparo il tooltip + env.tooltipContainer = $('').appendTo(document.body); + env.tooltipFrame = common._RaphaelInstance('elycharts_tooltip_' + $.elycharts.tooltipid + '_frame', 500, 500); + env.tooltipContent = $('#elycharts_tooltip_' + $.elycharts.tooltipid + '_content'); + }, + + _prepareShow : function(env, props, mouseAreaData, tip) { + + // Il dimensionamento del tooltip e la view del frame SVG, lo fa solo se width ed height sono specificati + if (props.width && props.width != 'auto' && props.height && props.height != 'auto') { + var delta = props.frameProps && props.frameProps['stroke-width'] ? props.frameProps['stroke-width'] : 0; + env.tooltipContainer.width(props.width + delta + 1).height(props.height + delta + 1); + if (!env.tooltipFrameElement && props.frameProps) { + var framePath = [ [ 'RECT', delta / 2, delta / 2, props.width, props.height, props.roundedCorners ] ]; + env.tooltipFrameElement = common.showPath(env, framePath, env.tooltipFrame).attr(props.frameProps); + // env.tooltipFrameElement = env.tooltipFrame.rect(delta / 2, delta / 2, props.width, props.height, props.roundedCorners); + } + } + + if (env.tooltipFrameElement) { + env.tooltipFrameElement.attr(props.frameProps); + } + + if (props.padding) + env.tooltipContent.css({ padding : props.padding[0] + 'px ' + props.padding[1] + 'px' }); + env.tooltipContent.css(props.contentStyle); + env.tooltipContent.html(tip); + + //BIND: env.tooltipContainer.unbind().mouseover(mouseAreaData.mouseover).mouseout(mouseAreaData.mouseout); + + // WARN: Prendendo env.paper.canvas non va bene... + //var offset = $(env.paper.canvas).offset(); + var offset = $(env.container).offset(); + + if (env.opt.features.tooltip.fixedPos) { + offset.top += env.opt.features.tooltip.fixedPos[1]; + offset.left += env.opt.features.tooltip.fixedPos[0]; + + } else { + var coord = this.getXY(env, props, mouseAreaData); + if (!coord[2]) { + offset.left += coord[0]; + while (offset.top + coord[1] < 0) + coord[1] += 20; + offset.top += coord[1]; + } else { + offset.left = coord[0]; + offset.top = coord[1]; + } + } + + return { top : offset.top, left : offset.left }; + }, + + /** + * Ritorna [x, y] oppure [x, y, true] se le coordinate sono relative alla pagina (e non al grafico) + */ + getXY : function(env, props, mouseAreaData) { + // NOTA Posizione mouse: mouseAreaData.event.pageX/pageY + var x = 0, y = 0; + if (mouseAreaData.path[0][0] == 'RECT') { + // L'area e' su un rettangolo (un bar o un indice completo), il tooltip lo faccio subito sopra + // Nota: per capire se e' sull'indice completo basta guardare mouseAreaData.piece == null + x = common.getX(mouseAreaData.path[0]) - props.offset[1]; + y = common.getY(mouseAreaData.path[0]) - props.height - props.offset[0]; + } + else if (mouseAreaData.path[0][0] == 'CIRCLE') { + // L'area e' su un cerchio (punto di un line) + x = common.getX(mouseAreaData.path[0]) - props.offset[1]; + y = common.getY(mouseAreaData.path[0]) - props.height - props.offset[0]; + } + else if (mouseAreaData.path[0][0] == 'SLICE') { + // L'area è su una fetta di torta (pie) + var path = mouseAreaData.path[0]; + + // Genera la posizione del tip considerando che deve stare all'interno di un cerchio che è sempre dalla parte opposta dell'area + // e deve essere il piu' vicino possibile all'area + var w = props.width && props.width != 'auto' ? props.width : 100; + var h = props.height && props.height != 'auto' ? props.height : 100; + // Raggio del cerchio che contiene il tip + var cr = Math.sqrt(Math.pow(w,2) + Math.pow(h,2)) / 2; + if (cr > env.opt.r) + cr = env.opt.r; + + var tipangle = path[5] + (path[6] - path[5]) / 2 + 180; + var rad = Math.PI / 180; + x = path[1] + cr * Math.cos(- tipangle * rad) - w / 2; + y = path[2] + cr * Math.sin(- tipangle * rad) - h / 2; + } + else if (mouseAreaData.piece && mouseAreaData.piece.paths && mouseAreaData.index >= 0 && mouseAreaData.piece.paths[mouseAreaData.index] && mouseAreaData.piece.paths[mouseAreaData.index].rect) { + // L'area ha una forma complessa, ma abbiamo il rettangolo di contenimento (funnel) + var rect = mouseAreaData.piece.paths[mouseAreaData.index].rect; + x = rect[0] - props.offset[1]; + y = rect[1] - props.height - props.offset[0]; + } + + if (env.opt.features.tooltip.positionHandler) + return env.opt.features.tooltip.positionHandler(env, props, mouseAreaData, x, y); + else + return [x, y]; + }, + + getTip : function(env, serie, index) { + var tip = false; + if (env.opt.tooltips) { + if (typeof env.opt.tooltips == 'function') + tip = env.opt.tooltips(env, serie, index, serie && env.opt.values[serie] && env.opt.values[serie][index] ? env.opt.values[serie][index] : false, env.opt.labels && env.opt.labels[index] ? env.opt.labels[index] : false); + else { + if (serie && env.opt.tooltips[serie] && env.opt.tooltips[serie][index]) + tip = env.opt.tooltips[serie][index]; + else if (!serie && env.opt.tooltips[index]) + tip = env.opt.tooltips[index]; + } + } + return tip; + }, + + _getProps : function(env, serie, index, mouseAreaData) { + var props = mouseAreaData.props.tooltip; + if (env.emptySeries && env.opt.series.empty) + props = $.extend(true, props, env.opt.series.empty.tooltip); + if (!props || !props.active) + return false; + + if (props.frameProps) { + var color = common.getItemColor(env, serie, index); + if (color) { + props = common._clone(props); + common.colorize(env, props, [['frameProps', 'stroke']], color); + } + } + return props; + }, + + _fadeOut : function(env) { + env.tooltipContainer.fadeOut(env.opt.features.tooltip.fadeDelay); + }, + + onMouseEnter : function(env, serie, index, mouseAreaData) { + var props = this._getProps(env, serie, index, mouseAreaData); + if (!props) return false; + + var tip = this.getTip(env, serie, index); + if (!tip) { + this._fadeOut(env); + return true; + } + + //if (!env.opt.tooltips || (serie && (!env.opt.tooltips[serie] || !env.opt.tooltips[serie][index])) || (!serie && !env.opt.tooltips[index])) + // return this.onMouseExit(env, serie, index, mouseAreaData); + //var tip = serie ? env.opt.tooltips[serie][index] : env.opt.tooltips[index]; + + env.tooltipContainer.css(this._prepareShow(env, props, mouseAreaData, tip)).fadeIn(env.opt.features.tooltip.fadeDelay); + + return true; + }, + + onMouseChanged : function(env, serie, index, mouseAreaData) { + var props = this._getProps(env, serie, index, mouseAreaData); + if (!props) return false; + + var tip = this.getTip(env, serie, index); + if (!tip) { + this._fadeOut(env); + return true; + } + + /*if (!env.opt.tooltips || (serie && (!env.opt.tooltips[serie] || !env.opt.tooltips[serie][index])) || (!serie && !env.opt.tooltips[index])) + return this.onMouseExit(env, serie, index, mouseAreaData); + var tip = serie ? env.opt.tooltips[serie][index] : env.opt.tooltips[index];*/ + + env.tooltipContainer.clearQueue(); + + // NOTE: this is needed because sometimes we "fadeOut" during mouseChanged so we also have to fadeIn in that cases. + // For simplicity we always fadeIn every time. + env.tooltipContainer.fadeIn(env.opt.features.tooltip.fadeDelay); + // Nota: Non passo da animationStackPush, i tooltip non sono legati a piece + env.tooltipContainer.animate(this._prepareShow(env, props, mouseAreaData, tip), env.opt.features.tooltip.moveDelay, 'linear' /*swing*/); + + return true; + }, + + onMouseExit : function(env, serie, index, mouseAreaData) { + var props = this._getProps(env, serie, index, mouseAreaData); + if (!props) return false; + + this._fadeOut(env); + + return true; + } +} + +$.elycharts.featuresmanager.register($.elycharts.tooltipmanager, 20); + +})(jQuery); +/********* Source File: src/elycharts_chart_line.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var common = $.elycharts.common; + +/*********************************************************************** + * CHART: LINE/BAR + **********************************************************************/ + +$.elycharts.line = { + init : function($env) { + }, + + _getColorizationKey : function($type) { + if ($type == 'line') return [ + ['plotProps', 'stroke'], + ['dotProps', 'fill'], + ['fillProps', 'fill'] + ]; else return [ + ['plotProps', 'stroke'], + ['plotProps', 'fill'] + ]; + }, + + draw : function(env) { + if (common.executeIfChanged(env, ['values', 'series'])) { + env.plots = {}; + env.axis = { x : {} }; + env.barno = 0; + env.indexCenter = 'line'; + } + + var opt = env.opt; + var plots = env.plots; + var axis = env.axis; + var paper = env.paper; + + var values = env.opt.values; + var labels = env.opt.labels; + var i, cum, props, serie, plot, labelsCount; + + // Valorizzazione di tutte le opzioni utili e le impostazioni interne di ogni grafico e dell'ambiente di lavoro + if (common.executeIfChanged(env, ['values', 'series'])) { + var idx = 0; + var prevVisibleSerie = false; + for (serie in values) { + plot = { + index : idx, + type : false, + visible : false + }; + plots[serie] = plot; + if (values[serie]) { + props = common.areaProps(env, 'Series', serie); + plot.type = props.type; + if (props.type == 'bar') + env.indexCenter = 'bar'; + + if (props.visible) { + plot.visible = true; + if (!labelsCount || labelsCount < values[serie].length) + labelsCount = values[serie].length; + + // Values + // showValues: manage NULL elements (doing an avg of near points) for line serie + var showValues = [] + for (i = 0; i < values[serie].length; i++) { + var val = values[serie][i]; + if (props.avgOverNulls && val == null) { + if (props.type == 'bar') + val = 0; + else { + for (var j = i + 1; j < values[serie].length && values[serie][j] == null; j++) {} + var next = j < values[serie].length ? values[serie][j] : null; + for (var k = i -1; k >= 0 && values[serie][k] == null; k--) {} + var prev = k >= 0 ? values[serie][k] : null; + val = next != null ? (prev != null ? (next * (i - k) + prev * (j - i)) / (j - k) : next) : prev; + } + } + showValues.push(val); + } + + if (props.stacked && !(typeof props.stacked == 'string')) + props.stacked = prevVisibleSerie; + + if (typeof props.stacked == 'undefined' || props.stacked == serie || props.stacked < 0 || !plots[props.stacked] || !plots[props.stacked].visible || plots[props.stacked].type != plot.type) { + // NOT Stacked + plot.ref = serie; + if (props.type == 'bar') + plot.barno = env.barno ++; + plot.from = []; + if (!props.cumulative) + plot.to = showValues; + else { + plot.to = []; + cum = 0; + for (i = 0; i < showValues.length; i++) + plot.to.push(cum += showValues[i] != null ? showValues[i] : 0); + } + for (i = 0; i < showValues.length; i++) + plot.from.push(plot.to[i] != null ? 0 : null); + + } else { + // Stacked + plot.ref = props.stacked; + if (props.type == 'bar') + plot.barno = plots[props.stacked].barno; + plot.from = plots[props.stacked].stack; + plot.to = []; + cum = 0; + if (!props.cumulative) + for (i = 0; i < showValues.length; i++) + plot.to.push(plot.from[i] + (showValues[i] != null ? showValues[i] : 0)); + else + for (i = 0; i < showValues.length; i++) + plot.to.push(plot.from[i] + (cum += (showValues[i] != null ? showValues[i] : 0))); + plots[props.stacked].stack = plot.to; + } + + plot.stack = plot.to; + plot.max = Math.max.apply(Math, plot.from.concat(plot.to)); + plot.min = Math.min.apply(Math, plot.from.concat(plot.to)); + + // Assi (DEP: values, series) + if (props.axis) { + if (!axis[props.axis]) + axis[props.axis] = { plots : [] }; + axis[props.axis].plots.push(serie); + if (typeof axis[props.axis].max == 'undefined') + axis[props.axis].max = plot.max; + else + axis[props.axis].max = Math.max(axis[props.axis].max, plot.max); + if (typeof axis[props.axis].min == 'undefined') + axis[props.axis].min = plot.min; + else + axis[props.axis].min = Math.min(axis[props.axis].min, plot.min); + } + + prevVisibleSerie = serie; + } + } + } + } + + // Labels normalization (if not set or less than values) + if (!labels) + labels = []; + while (labelsCount > labels.length) + labels.push(null); + labelsCount = labels.length; + env.opt.labels = labels; + + // Prepare axis scale (values, series, axis) + if (common.executeIfChanged(env, ['values', 'series', 'axis'])) { + for (var lidx in axis) { + props = common.areaProps(env, 'Axis', lidx); + axis[lidx].props = props; + + if (typeof props.max != 'undefined') + axis[lidx].max = props.max; + if (typeof props.min != 'undefined') + axis[lidx].min = props.min; + + if (axis[lidx].min == axis[lidx].max) + axis[lidx].max = axis[lidx].min + 1; + + if (props.normalize && props.normalize > 0) { + var v = Math.abs(axis[lidx].max); + if (axis[lidx].min && Math.abs(axis[lidx].min) > v) + v = Math.abs(axis[lidx].min); + if (v) { + var basev = Math.floor(Math.log(v)/Math.LN10) - (props.normalize - 1); + // NOTE: On firefox Math.pow(10, -X) sometimes results in number noise (0.89999...), it's better to do 1/Math.pow(10,X) + basev = basev >= 0 ? Math.pow(10, basev) : 1 / Math.pow(10, -basev); + v = Math.ceil(v / basev / (opt.features.grid.ny ? opt.features.grid.ny : 1)) * basev * (opt.features.grid.ny ? opt.features.grid.ny : 1); + // Calculation above, with decimal number sometimes insert some noise in numbers (eg: 8.899999... instead of 0.9), so i need to round result with proper precision + v = Math.round(v / basev) * basev; + // I need to store the normalization base for further roundin (eg: in axis label, sometimes calculation results in "number noise", so i need to round them with proper precision) + axis[lidx].normalizationBase = basev; + if (axis[lidx].max) + axis[lidx].max = Math.ceil(axis[lidx].max / v) * v; + if (axis[lidx].min) + axis[lidx].min = Math.floor(axis[lidx].min / v) * v; + } + } + if (axis[lidx].plots) + for (var ii = 0; ii < axis[lidx].plots.length; ii++) { + plots[axis[lidx].plots[ii]].max = axis[lidx].max; + plots[axis[lidx].plots[ii]].min = axis[lidx].min; + } + } + } + + var pieces = []; + + this.grid(env, pieces); + + // DEP: * + var deltaX = (env.width - opt.margins[3] - opt.margins[1]) / (labels.length > 1 ? labels.length - 1 : 1); + var deltaBarX = (env.width - opt.margins[3] - opt.margins[1]) / (labels.length > 0 ? labels.length : 1); + + for (serie in values) { + props = common.areaProps(env, 'Series', serie); + plot = plots[serie]; + + common.colorize(env, props, this._getColorizationKey(props.type), common.getItemColor(env, serie)); + + // TODO Settare una props in questo modo potrebbe incasinare la gestione degli update parziali (se iso "lineCenter: auto" e passo da un grafico con indexCenter = bar a uno con indexCenter = line) + if (props.lineCenter && props.lineCenter == 'auto') + props.lineCenter = (env.indexCenter == 'bar'); + else if (props.lineCenter && env.indexCenter == 'line') + env.indexCenter = 'bar'; + + if (values[serie] && props.visible) { + var deltaY = (env.height - opt.margins[2] - opt.margins[0]) / (plot.max - plot.min); + + if (props.type == 'line') { + // LINE CHART + var linePath = [ 'LINE', [], props.rounded ]; + var fillPath = [ 'LINEAREA', [], [], props.rounded ]; + var dotPieces = []; + + for (i = 0, ii = labels.length; i < ii; i++) + if (plot.to.length > i) { + var indexProps = common.areaProps(env, 'Series', serie, i); + + common.colorize(env, indexProps, this._getColorizationKey(props.type), common.getItemColor(env, serie, i)); + + var x = Math.round((props.lineCenter ? deltaBarX / 2 : 0) + opt.margins[3] + i * (props.lineCenter ? deltaBarX : deltaX)); + + var y = null; + if (plot.to[i] != null) { + var d = plot.to[i] > plot.max ? plot.max : (plot.to[i] < plot.min ? plot.min : plot.to[i]); + y = Math.round(env.height - opt.margins[2] - deltaY * (d - plot.min)); + } + var yy = null; + if (plot.from[i] != null) { + var dd = plot.from[i] > plot.max ? plot.max : (plot.from[i] < plot.min ? plot.min : plot.from[i]); + yy = Math.round(env.height - opt.margins[2] - deltaY * (dd - plot.min)) + (Raphael.VML ? 1 : 0); + } + + linePath[1].push([x, y]); + + if (props.fill) { + fillPath[1].push([x, y]); + fillPath[2].push([x, yy]); + } + if (indexProps.dot) { + if (values[serie][i] == null && !indexProps.dotShowOnNull) + dotPieces.push({path : false, attr : false}); + else + dotPieces.push({path : [ [ 'CIRCLE', x, y, indexProps.dotProps.size ] ], attr : indexProps.dotProps}); // TODO Size should not be in dotProps (not an svg props) + } + } + + if (props.fill) + pieces.push({ section : 'Series', serie : serie, subSection : 'Fill', path : [ fillPath ], attr : props.fillProps }); + else + pieces.push({ section : 'Series', serie : serie, subSection : 'Fill', path : false, attr : false }); + pieces.push({ section : 'Series', serie : serie, subSection : 'Plot', path : [ linePath ], attr : props.plotProps , mousearea : 'pathsteps'}); + + if (dotPieces.length) + pieces.push({ section : 'Series', serie : serie, subSection : 'Dot', paths : dotPieces }); + else + pieces.push({ section : 'Series', serie : serie, subSection : 'Dot', path : false, attr : false }); + + } else { + pieceBar = []; + // BAR CHART + for (i = 0, ii = labels.length; i < ii; i++) + if (plot.to.length > i) { + if (plot.from[i] != plot.to[i]) { + var indexProps = common.areaProps(env, 'Series', serie, i); + common.colorize(env, indexProps, this._getColorizationKey(props.type), common.getItemColor(env, serie, i)); + + var bwid = Math.floor((deltaBarX - opt.barMargins) / (1 + (env.barno - 1) * (100 - opt.barOverlapPerc) / 100)); + var bpad = bwid * (100 - props.barWidthPerc) / 200; + var boff = opt.barMargins / 2 + plot.barno * (bwid * (100 - opt.barOverlapPerc) / 100); + + var x1 = Math.floor(opt.margins[3] + i * deltaBarX + boff + bpad); + var y1 = Math.round(env.height - opt.margins[2] - deltaY * (plot.to[i] - plot.min)); + var y2 = Math.round(env.height - opt.margins[2] - deltaY * (plot.from[i] - plot.min)); + + pieceBar.push({path : [ [ 'RECT', x1, y1, x1 + bwid - bpad * 2, y2 ] ], attr : indexProps.plotProps }); + } else + pieceBar.push({path : false, attr : false }); + } + + if (pieceBar.length) + pieces.push({ section : 'Series', serie : serie, subSection : 'Plot', paths: pieceBar, mousearea : 'paths' }); + else + pieces.push({ section : 'Series', serie : serie, subSection : 'Plot', path: false, attr: false, mousearea : 'paths' }); + } + + } else { + // Grafico non visibile / senza dati, deve comunque inserire i piece vuoti (NELLO STESSO ORDINE SOPRA!) + if (props.type == 'line') + pieces.push({ section : 'Series', serie : serie, subSection : 'Fill', path : false, attr : false }); + pieces.push({ section : 'Series', serie : serie, subSection : 'Plot', path: false, attr: false, mousearea : 'paths' }); + if (props.type == 'line') + pieces.push({ section : 'Series', serie : serie, subSection : 'Dot', path : false, attr : false }); + } + } + return pieces; + }, + + grid : function(env, pieces) { + + // DEP: axis, [=> series, values], labels, margins, width, height, grid* + if (common.executeIfChanged(env, ['values', 'series', 'axis', 'labels', 'margins', 'width', 'height', 'features.grid'])) { + var opt = env.opt; + var props = env.opt.features.grid; + var paper = env.paper; + var axis = env.axis; + var labels = env.opt.labels; + var deltaX = (env.width - opt.margins[3] - opt.margins[1]) / (labels.length > 1 ? labels.length - 1 : 1); + var deltaBarX = (env.width - opt.margins[3] - opt.margins[1]) / (labels.length > 0 ? labels.length : 1); + var i, j, x, y, lw, labx, laby, labe, val, txt; + // Label X axis + var paths = []; + var labelsCenter = props.labelsCenter; + if (labelsCenter == 'auto') + labelsCenter = (env.indexCenter == 'bar'); + + if (axis.x && axis.x.props.labels) { + // used in case of labelsHideCovered, contains a "rotated" representation of the rect coordinates occupied by the last shown label + var lastShownLabelRect = false; + // labelsAnchor is "auto" by default. Can be "start","middle" or "end". If "auto" then it is automatically set depending on labelsRotate. + var labelsAnchor = axis.x.props.labelsAnchor || 'auto'; + // Automatic labelsAnchor is "middle" on no rotation, otherwise the anchor is the higher side of the label. + if (labelsAnchor == 'auto') + labelsAnchor = axis.x.props.labelsRotate > 0 ? "start" : (axis.x.props.labelsRotate == 0 ? "middle" : "end"); + // labelsPos is "auto" by default. Can be "start", "middle" or "end". If "auto" then it is automatically set depending on labelsCenter and labelsRotate and labelsAnchor. + var labelsPos = axis.x.props.labelsPos || 'auto'; + // in labelsCenter (bar) it is middle when there is no rotation, equals to labelsAnchor on rotation. + // in !labelsCenter (line) is is always 'start'; + if (labelsPos == 'auto') + labelsPos = labelsCenter ? (axis.x.props.labelsRotate == 0 ? labelsAnchor : 'middle') : 'start'; + + for (i = 0; i < labels.length; i++) + if ((typeof labels[i] != 'boolean' && labels[i] != null) || labels[i]) { + if (!axis.x.props.labelsSkip || i >= axis.x.props.labelsSkip) { + val = labels[i]; + + if (axis.x.props.labelsFormatHandler) + val = axis.x.props.labelsFormatHandler(val); + txt = (axis.x.props.prefix ? axis.x.props.prefix : "") + val + (axis.x.props.suffix ? axis.x.props.suffix : ""); + + labx = opt.margins[3] + i * (labelsCenter ? deltaBarX : deltaX) + (axis.x.props.labelsMargin ? axis.x.props.labelsMargin : 0); + if (labelsPos == 'middle') labx += (labelsCenter ? deltaBarX : deltaX) / 2; + if (labelsPos == 'end') labx += (labelsCenter ? deltaBarX : deltaX); + + laby = env.height - opt.margins[2] + axis.x.props.labelsDistance; + labe = paper.text(labx, laby, txt).attr(axis.x.props.labelsProps).toBack(); + + labe.attr({"text-anchor" : labelsAnchor}); + + // will contain the boundingbox size, or false if it is hidden. + var boundingbox = false; + var bbox = labe.getBBox(); + var p1 = {x: bbox.x, y: bbox.y}; + var p2 = {x: bbox.x+bbox.width, y: bbox.y+bbox.height}; + var o1 = {x: labx, y: laby}; + + rotate = function (p, rad) { + var X = p.x * Math.cos(rad) - p.y * Math.sin(rad), + Y = p.x * Math.sin(rad) + p.y * Math.cos(rad); + return {x: X, y: Y}; + }; + // calculate collision between non rotated rects with vertext p1-p2 and t1-t2 + // this algorythm works only for horizontal rects (alpha = 0) + // "dist" is the length added as a margin to the rects before collision detection + collide = function(r1,r2,dist) { + xor = function(a,b) { + return ( a || b ) && !( a && b ); + } + if (r1.alpha != r2.alpha) throw "collide doens't support rects with different rotations"; + var r1p1r = rotate({x: r1.p1.x-dist, y:r1.p1.y-dist}, -r1.alpha); + var r1p2r = rotate({x: r1.p2.x+dist, y:r1.p2.y+dist}, -r1.alpha); + var r2p1r = rotate({x: r2.p1.x-dist, y:r2.p1.y-dist}, -r2.alpha); + var r2p2r = rotate({x: r2.p2.x+dist, y:r2.p2.y+dist}, -r2.alpha); + return !xor(Math.min(r1p1r.x,r1p2r.x) > Math.max(r2p1r.x,r2p2r.x), Math.max(r1p1r.x,r1p2r.x) < Math.min(r2p1r.x,r2p2r.x)) && + !xor(Math.min(r1p1r.y,r1p2r.y) > Math.max(r2p1r.y,r2p2r.y), Math.max(r1p1r.y,r1p2r.y) < Math.min(r2p1r.y,r2p2r.y)); + } + // compute equivalent orizontal rotated rect + rotated = function(rect, origin, alpha) { + translate = function (p1, p2) { + return {x: p1.x+p2.x, y: p1.y+p2.y}; + }; + negate = function(p1) { + return {x: -p1.x, y: -p1.y}; + }; + var p1trt = translate(rotate(translate(rect.p1,negate(origin)), alpha),origin); + var p2trt = translate(rotate(translate(rect.p2,negate(origin)), alpha),origin); + return { p1: p1trt, p2: p2trt, alpha: rect.alpha+alpha }; + } + bbox = function(rect) { + if (rect.alpha == 0) { + return { x: rect.p1.x, y: rect.p1.y, width: rect.p2.x-rect.p1.x, height: rect.p2.y-rect.p1.y }; + } else { + var points = []; + points.push({ x: 0, y: 0 }); + points.push({ x: rect.p2.x-rect.p1.x, y: 0 }); + points.push({ x: 0, y: rect.p2.y-rect.p1.y }); + points.push({ x: rect.p2.x-rect.p1.x, y: rect.p2.y-rect.p1.y }); + var bb = []; + bb['left'] = 0; bb['right'] = 0; bb['top'] = 0; bb['bottom'] = 0; + for (_px = 0; _px < points.length; _px++) { + var p = points[_px]; + var newX = parseInt((p.x * Math.cos(rect.alpha)) + (p.y * Math.sin(rect.alpha))); + var newY = parseInt((p.x * Math.sin(rect.alpha)) + (p.y * Math.cos(rect.alpha))); + bb['left'] = Math.min(bb['left'], newX); + bb['right'] = Math.max(bb['right'], newX); + bb['top'] = Math.min(bb['top'], newY); + bb['bottom'] = Math.max(bb['bottom'], newY); + } + var newWidth = parseInt(Math.abs(bb['right'] - bb['left'])); + var newHeight = parseInt(Math.abs(bb['bottom'] - bb['top'])); + var newX = ((rect.p1.x + rect.p2.x) / 2) - newWidth / 2; + var newY = ((rect.p1.y + rect.p2.y) / 2) - newHeight / 2; + return { x: newX, y: newY, width: newWidth, height: newHeight }; + } + } + + var alpha = Raphael.rad(axis.x.props.labelsRotate); + // compute used "rect" so to be able to check if there is overlapping with previous ones. + var rect = rotated({p1: p1, p2: p2, alpha: 0}, o1, alpha); + + //console.log('bbox ',p1, p2, rect, props.nx, val, rect.p1, rect.p2, rect.alpha, boundingbox, env.width); + // se collide con l'ultimo mostrato non lo mostro. + var dist = axis.x.props.labelsMarginRight ? axis.x.props.labelsMarginRight / 2 : 0; + if (axis.x.props.labelsHideCovered && lastShownLabelRect && collide(rect, lastShownLabelRect, dist)) { + labe.hide(); + // labels[i] = false; + } else { + boundingbox = bbox(rect); + // Manage label overflow + if (props.nx == 'auto' && (boundingbox.x < 0 || boundingbox.x+boundingbox.width > env.width)) { + labe.hide(); + // labels[i] = false; + } else { + lastShownLabelRect = rect; + } + } + + // Apply rotation to the element. + if (axis.x.props.labelsRotate) { + if (Raphael.animation) { + labe.transform(Raphael.format('r{0},{1},{2}', axis.x.props.labelsRotate, labx, laby)).toBack(); + } else { + labe.rotate(axis.x.props.labelsRotate, labx, laby).toBack(); + } + } + + paths.push({ path : [ [ 'RELEMENT', labe ] ], attr : false }); + } + } + } + pieces.push({ section : 'Axis', serie : 'x', subSection : 'Label', paths : paths }); + + // Title X Axis + if (axis.x && axis.x.props.title) { + x = opt.margins[3] + Math.floor((env.width - opt.margins[1] - opt.margins[3]) / 2); + y = env.height - opt.margins[2] + axis.x.props.titleDistance * (Raphael.VML ? axis.x.props.titleDistanceIE : 1); + //paper.text(x, y, axis.x.props.title).attr(axis.x.props.titleProps); + pieces.push({ section : 'Axis', serie : 'x', subSection : 'Title', path : [ [ 'TEXT', axis.x.props.title, x, y ] ], attr : axis.x.props.titleProps }); + } else + pieces.push({ section : 'Axis', serie : 'x', subSection : 'Title', path : false, attr : false }); + + // Label + Title L/R Axis + for (var jj in ['l', 'r']) { + j = ['l', 'r'][jj]; + if (axis[j] && axis[j].props.labels && props.ny) { + paths = []; + for (i = axis[j].props.labelsSkip ? axis[j].props.labelsSkip : 0; i <= props.ny; i++) { + var deltaY = (env.height - opt.margins[2] - opt.margins[0]) / props.ny; + // TODO we should never set "props". We should use local variables for derived value (so to correctly deal with updates) + if (j == 'r') { + labx = env.width - opt.margins[1] + axis[j].props.labelsDistance; + if (!axis[j].props.labelsProps["text-anchor"]) + axis[j].props.labelsProps["text-anchor"] = "start"; + } else { + labx = opt.margins[3] - axis[j].props.labelsDistance; + if (!axis[j].props.labelsProps["text-anchor"]) + axis[j].props.labelsProps["text-anchor"] = "end"; + } + if (axis[j].props.labelsAnchor && axis[j].props.labelsAnchor != 'auto') + axis[j].props.labelsProps["text-anchor"] = axis[j].props.labelsAnchor; + // NOTE: Parenthesis () around division are useful to keep right number precision + val = (axis[j].min + (i * ((axis[j].max - axis[j].min) / props.ny))); + // Rounding with proper precision for "number sharpening" + if (axis[j].normalizationBase) + // I use (1 / ( 1 / norm ) ) to avoid some noise + val = Math.round(val / axis[j].normalizationBase) / ( 1 / axis[j].normalizationBase ); + + if (axis[j].props.labelsFormatHandler) + val = axis[j].props.labelsFormatHandler(val); + if (axis[j].props.labelsCompactUnits) + val = common.compactUnits(val, axis[j].props.labelsCompactUnits); + txt = (axis[j].props.prefix ? axis[j].props.prefix : "") + val + (axis[j].props.suffix ? axis[j].props.suffix : ""); + laby = env.height - opt.margins[2] - i * deltaY; + //var labe = paper.text(labx, laby + (axis[j].props.labelsMargin ? axis[j].props.labelsMargin : 0), txt).attr(axis[j].props.labelsProps).toBack(); + paths.push( { path : [ [ 'TEXT', txt, labx, laby + (axis[j].props.labelsMargin ? axis[j].props.labelsMargin : 0) ] ], attr : axis[j].props.labelsProps }); + } + pieces.push({ section : 'Axis', serie : j, subSection : 'Label', paths : paths }); + } else + pieces.push({ section : 'Axis', serie : j, subSection : 'Label', paths : [] }); + + if (axis[j] && axis[j].props.title) { + if (j == 'r') + x = env.width - opt.margins[1] + axis[j].props.titleDistance * (Raphael.VML ? axis[j].props.titleDistanceIE : 1); + else + x = opt.margins[3] - axis[j].props.titleDistance * (Raphael.VML ? axis[j].props.titleDistanceIE : 1); + //paper.text(x, opt.margins[0] + Math.floor((env.height - opt.margins[0] - opt.margins[2]) / 2), axis[j].props.title).attr(axis[j].props.titleProps).attr({rotation : j == 'l' ? 270 : 90}); + var attr = common._clone(axis[j].props.titleProps); + var rotation = j == 'l' ? 270 : 90; + var y = opt.margins[0] + Math.floor((env.height - opt.margins[0] - opt.margins[2]) / 2); + // Raphael 2 does not support .rotation + if (Raphael.animation) { + var labe = paper.text(x, y, axis[j].props.title).attr(attr).transform(Raphael.format('r{0}', rotation)).toBack(); + pieces.push({ section : 'Axis', serie : j, subSection : 'Title', path : [ [ 'RELEMENT', labe ] ], attr : false }); + } else { + attr.rotation = rotation; + pieces.push({ section : 'Axis', serie : j, subSection : 'Title', path : [ [ 'TEXT', axis[j].props.title, x, y ] ], attr : attr }); + } + } else + pieces.push({ section : 'Axis', serie : j, subSection : 'Title', path : false, attr : false }); + } + + // Grid + if (props.nx || props.ny) { + var path = [], bandsH = [], bandsV = [], + nx = props.nx == 'auto' ? (labelsCenter ? labels.length : labels.length - 1) : props.nx, + ny = props.ny, + rowHeight = (env.height - opt.margins[2] - opt.margins[0]) / (ny ? ny : 1), + columnWidth = (env.width - opt.margins[1] - opt.margins[3]) / (nx ? nx : 1), + forceBorderX1 = typeof props.forceBorder == 'object' ? props.forceBorder[3] : props.forceBorder, + forceBorderX2 = typeof props.forceBorder == 'object' ? props.forceBorder[1] : props.forceBorder, + forceBorderY1 = typeof props.forceBorder == 'object' ? props.forceBorder[0] : props.forceBorder, + forceBorderY2 = typeof props.forceBorder == 'object' ? props.forceBorder[2] : props.forceBorder, + drawH = ny > 0 ? (typeof props.draw == 'object' ? props.draw[0] : props.draw) : false, + drawV = nx > 0 ? typeof props.draw == 'object' ? props.draw[1] : props.draw : false; + + if (ny > 0) + for (i = 0; i < ny + 1; i++) { + if ( + forceBorderY1 && i == 0 || // Show top line only if forced + forceBorderY2 && i == ny || // Show bottom line only if forced + drawH && i > 0 && i < ny // Show other lines if draw = true + ) { + path.push(["M", opt.margins[3] - props.extra[3], opt.margins[0] + Math.round(i * rowHeight) ]); + path.push(["L", env.width - opt.margins[1] + props.extra[1], opt.margins[0] + Math.round(i * rowHeight)]); + } + if (i < ny) { + if (i % 2 == 0 && props.evenHProps || i % 2 == 1 && props.oddHProps) + bandsH.push({path : [ [ 'RECT', + opt.margins[3] - props.extra[3], opt.margins[0] + Math.round(i * rowHeight), // x1, y1 + env.width - opt.margins[1] + props.extra[1], opt.margins[0] + Math.round((i + 1) * rowHeight) // x2, y2 + ] ], attr : i % 2 == 0 ? props.evenHProps : props.oddHProps }); + else + bandsH.push({ path : false, attr: false}) + } + } + + for (i = 0; i < nx + 1; i++) { + if ( + forceBorderX1 && i == 0 || // Always show first line if forced + forceBorderX2 && i == nx || // Always show last line if forced + drawV && ( // To show other lines draw must be true + (props.nx != 'auto' && i > 0 && i < nx) || // If nx = [number] show other lines (first and last are managed above with forceBorder) + (props.nx == 'auto' && (typeof labels[i] != 'boolean' || labels[i])) // if nx = 'auto' show all lines if a label is associated + ) + // Show all lines if props.nx is a number, or if label != false, AND draw must be true + ) { + path.push(["M", opt.margins[3] + Math.round(i * columnWidth), opt.margins[0] - props.extra[0] ]); //(t ? props.extra[0] : 0)]); + path.push(["L", opt.margins[3] + Math.round(i * columnWidth), env.height - opt.margins[2] + props.extra[2] ]); //(t ? props.extra[2] : 0)]); + } + if (i < nx) { + if (i % 2 == 0 && props.evenVProps || i % 2 == 1 && props.oddVProps) + bandsV.push({path : [ [ 'RECT', + opt.margins[3] + Math.round(i * columnWidth), opt.margins[0] - props.extra[0], // x1, y1 + opt.margins[3] + Math.round((i + 1) * columnWidth), env.height - opt.margins[2] + props.extra[2], // x2, y2 + ] ], attr : i % 2 == 0 ? props.evenVProps : props.oddVProps }); + else + bandsV.push({ path : false, attr: false}) + } + } + + pieces.push({ section : 'Grid', path : path.length ? path : false, attr : path.length ? props.props : false }); + pieces.push({ section : 'GridBandH', paths : bandsH }); + pieces.push({ section : 'GridBandV', paths : bandsV }); + + var tpath = []; + + // Ticks asse X + if (props.ticks.active && (typeof props.ticks.active != 'object' || props.ticks.active[0])) { + for (i = 0; i < nx + 1; i++) { + if (props.nx != 'auto' || typeof labels[i] != 'boolean' || labels[i]) { + tpath.push(["M", opt.margins[3] + Math.round(i * columnWidth), env.height - opt.margins[2] - props.ticks.size[1] ]); + tpath.push(["L", opt.margins[3] + Math.round(i * columnWidth), env.height - opt.margins[2] + props.ticks.size[0] ]); + } + } + } + // Ticks asse L + if (props.ticks.active && (typeof props.ticks.active != 'object' || props.ticks.active[1])) + for (i = 0; i < ny + 1; i++) { + tpath.push(["M", opt.margins[3] - props.ticks.size[0], opt.margins[0] + Math.round(i * rowHeight) ]); + tpath.push(["L", opt.margins[3] + props.ticks.size[1], opt.margins[0] + Math.round(i * rowHeight)]); + } + // Ticks asse R + if (props.ticks.active && (typeof props.ticks.active != 'object' || props.ticks.active[2])) + for (i = 0; i < ny + 1; i++) { + tpath.push(["M", env.width - opt.margins[1] - props.ticks.size[1], opt.margins[0] + Math.round(i * rowHeight) ]); + tpath.push(["L", env.width - opt.margins[1] + props.ticks.size[0], opt.margins[0] + Math.round(i * rowHeight)]); + } + + pieces.push({ section : 'Ticks', path : tpath.length ? tpath : false, attr : tpath.length ? props.ticks.props : false }); + } + } + } +} + +})(jQuery); +/********* Source File: src/elycharts_chart_pie.js*********/ +/********************************************************************** + * ELYCHARTS + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ + +(function($) { + +var common = $.elycharts.common; + +/*********************************************************************** + * CHART: PIE + **********************************************************************/ + +$.elycharts.pie = { + init : function($env) { + }, + + draw : function(env) { + //var paper = env.paper; + var opt = env.opt; + + var w = env.width - env.opt.margins[1] - env.opt.margins[3]; + var h = env.height - env.opt.margins[0] - env.opt.margins[2]; + var r = env.opt.r ? env.opt.r : Math.floor((w < h ? w : h) / 2 * (env.opt.rPerc ? env.opt.rPerc / 100 : 0.8)); + var cx = (env.opt.cx ? env.opt.cx : Math.floor(w / 2)) + env.opt.margins[3]; + var cy = (env.opt.cy ? env.opt.cy : Math.floor(h / 2)) + env.opt.margins[0]; + + var cnt = 0, i, ii, serie, plot, props; + for (serie in opt.values) { + plot = { + visible : false, + total : 0, + values : [] + }; + env.plots[serie] = plot; + var serieProps = common.areaProps(env, 'Series', serie); + common.colorize(env, serieProps, [['plotProps','stroke'],['plotProps','fill']], common.getItemColor(env, serie)); + if (serieProps.visible) { + plot.visible = true; + cnt ++; + plot.values = opt.values[serie]; + for (i = 0, ii = plot.values.length; i < ii; i++) + if (plot.values[i] > 0) { + props = common.areaProps(env, 'Series', serie, i); + common.colorize(env, props, [['plotProps','stroke'],['plotProps','fill']], common.getItemColor(env, serie, i)); + if (typeof props.inside == 'undefined' || props.inside < 0) + plot.total += plot.values[i]; + } + for (i = 0; i < ii; i++) + if (plot.values[i] < plot.total * opt.valueThresold) { + plot.total = plot.total - plot.values[i]; + plot.values[i] = 0; + } + } + } + + var rstep = r / cnt; + var rstart = -rstep, rend = 0; + + var pieces = []; + for (serie in opt.values) { + plot = env.plots[serie]; + var paths = []; + if (plot.visible) { + rstart += rstep; + rend += rstep; + var angle = env.opt.startAngle, angleplus = 0, anglelimit = 0; + + if (plot.total == 0) { + env.emptySeries = true; + props = common.areaProps(env, 'Series', 'empty'); + common.colorize(env, props, [['plotProps','stroke'],['plotProps','fill']], common.getItemColor(env, serie)); + paths.push({ path : [ [ 'CIRCLE', cx, cy, r ] ], attr : props.plotProps }); + + } else { + env.emptySeries = false; + for (i = 0, ii = plot.values.length; i < ii; i++) { + var value = plot.values[i]; + if (value > 0) { + props = common.areaProps(env, 'Series', serie, i); + common.colorize(env, props, [['plotProps','stroke'],['plotProps','fill']], common.getItemColor(env, serie, i)); + if (typeof props.inside == 'undefined' || props.inside < 0) { + angle += anglelimit; + angleplus = 360 * value / plot.total; + anglelimit = angleplus; + } else { + angleplus = 360 * values[props.inside] / plot.total * value / values[props.inside]; + } + var rrstart = rstart, rrend = rend; + if (props.r) { + if (props.r > 0) { + if (props.r <= 1) + rrend = rstart + rstep * props.r; + else + rrend = rstart + props.r; + } else { + if (props.r >= -1) + rrstart = rstart + rstep * (-props.r); + else + rrstart = rstart - props.r; + } + } + if (!env.opt.clockwise) + paths.push({ path : [ [ 'SLICE', cx, cy, rrend, rrstart, angle, angle + angleplus ] ], attr : props.plotProps }); + else + paths.push({ path : [ [ 'SLICE', cx, cy, rrend, rrstart, - angle - angleplus, - angle ] ], attr : props.plotProps }); + } else + paths.push({ path : false, attr : false }); + } + } + } else { + // Even if serie is not visible it's better to put some empty path (for better transitions). It's not mandatory, just better + if (opt.values[serie] && opt.values[serie].length) + for (i = 0, ii = opt.values[serie].length; i < ii; i++) + paths.push({ path : false, attr : false }); + } + + pieces.push({ section : 'Series', serie : serie, subSection : 'Plot', paths : paths , mousearea : 'paths'}); + } + + return pieces; + } +} + +})(jQuery); diff --git a/content/usr/local/share/nzbget/webui/lib/elycharts.min.js b/content/usr/local/share/nzbget/webui/lib/elycharts.min.js new file mode 100644 index 0000000..e33f06a --- /dev/null +++ b/content/usr/local/share/nzbget/webui/lib/elycharts.min.js @@ -0,0 +1,8 @@ +/*!********************************************************************* + * ELYCHARTS v2.1.6-SNAPSHOT $Id$ + * A Javascript library to generate interactive charts with vectorial graphics. + * + * Copyright (c) 2010-2014 Void Labs s.n.c. (http://void.it) + * Licensed under the MIT (http://creativecommons.org/licenses/MIT/) license. + **********************************************************************/ +(function(a){if(!a.elycharts){a.elycharts={}}a.elycharts.templates={common:{margins:[10,10,10,10],interactive:true,defaultSeries:{visible:true,tooltip:{active:true,width:100,height:50,roundedCorners:5,padding:[6,6],offset:[20,0],frameProps:{fill:"white","stroke-width":2},contentStyle:{"font-family":"Arial","font-size":"12px","line-height":"16px",color:"black"}},highlight:{scaleSpeed:100,scaleEasing:"",moveSpeed:100,moveEasing:"",restoreSpeed:0,restoreEasing:""},anchor:{},startAnimation:{type:"simple",speed:600,delay:0,propsFrom:{},propsTo:{},easing:""},label:{active:false,html:false,props:{fill:"black",stroke:"none","font-family":"Arial","font-size":"16px"},style:{cursor:"default"}}},series:{empty:{label:{active:false},tooltip:{active:false}}},features:{tooltip:{fadeDelay:100,moveDelay:300},mousearea:{type:"single",indexCenter:"auto",areaMoveDelay:500,syncTag:false,onMouseEnter:false,onMouseExit:false,onMouseChanged:false,onMouseOver:false,onMouseOut:false},highlight:{indexHighlightProps:{opacity:1}},animation:{startAnimation:{speed:600,delay:0,easing:""},stepAnimation:{speed:600,delay:0,easing:""}},frameAnimation:{active:false,cssFrom:{opacity:0},cssTo:{opacity:1},speed:"slow",easing:"linear"},pixelWorkAround:{active:Raphael.svg},label:{},shadows:{active:false,offset:[2,2],props:{"stroke-width":0,"stroke-opacity":0,fill:"black","fill-opacity":0.3}},balloons:{active:false,style:{},padding:[5,5],left:10,line:[[0,0],[0,0]],lineProps:{}},legend:{horizontal:false,x:"auto",y:10,width:"auto",height:20,itemWidth:"fixed",margins:[0,0,0,0],dotMargins:[10,5],borderProps:{fill:"white",stroke:"black","stroke-width":1},dotType:"rect",dotWidth:10,dotHeight:10,dotR:0,dotProps:{type:"rect",width:10,height:10},textProps:{font:"12px Arial",fill:"#000"}},debug:{active:false}},enableInternalCaching:true,nop:0},line:{template:"common",barMargins:0,barOverlapPerc:0,avgOverNulls:true,defaultAxis:{normalize:2,min:0,labels:false,labelsDistance:8,labelsRotate:0,labelsProps:{font:"10px Arial",fill:"#000"},titleDistance:25,titleDistanceIE:0.75,titleProps:{font:"12px Arial",fill:"#000","font-weight":"bold"}},axis:{x:{titleDistanceIE:1.2}},defaultSeries:{type:"line",axis:"l",cumulative:false,rounded:1,lineCenter:"auto",plotProps:{"stroke-width":1,"stroke-linejoin":"round"},barWidthPerc:100,fill:false,fillProps:{stroke:"none","stroke-width":0,"stroke-opacity":0,opacity:0.3},dot:false,dotProps:{size:4,stroke:"#000",zindex:5},dotShowOnNull:false,mouseareaShowOnNull:false,startAnimation:{plotPropsFrom:false,fillPropsFrom:false,dotPropsFrom:false,shadowPropsFrom:false}},features:{grid:{nx:"auto",ny:4,draw:false,forceBorder:false,props:{stroke:"#e0e0e0","stroke-width":1},extra:[0,0,0,0],labelsCenter:"auto",evenVProps:false,oddVProps:false,evenHProps:false,oddHProps:false,ticks:{active:[false,false,false],size:[10,10],props:{stroke:"#e0e0e0","stroke-width":1}}}},nop:0},pie:{template:"common",startAngle:0,clockwise:false,valueThresold:0.006,margins:[0,0,0,0],defaultSeries:{}},funnel:{template:"common",rh:0,method:"width",topSector:0,topSectorProps:{fill:"#d0d0d0"},bottomSector:0.1,bottomSectorProps:{fill:"#d0d0d0"},edgeProps:{fill:"#c0c0c0","stroke-width":1,opacity:1},nop:0},barline:{template:"common",direction:"ltr"}}})(jQuery);(function(c){if(!c.elycharts){c.elycharts={}}c.elycharts.lastId=0;c.fn.chart=function(j){if(!this.length){return this}var n=this.data("elycharts_env");if(typeof j=="string"){if(j.toLowerCase()=="config"){return n?n.opt:false}if(j.toLowerCase()=="clear"){if(n){if(c.elycharts.featuresmanager){c.elycharts.featuresmanager.clear(n)}n.paper.clear();n.cache=false;if(n.autoresize){c(window).unbind("resize",n.autoresize)}this.html("");this.data("elycharts_env",false)}}return this}if(!n){if(j){j=d(j)}if(!j||!j.type||!c.elycharts.templates[j.type]){alert("ElyCharts ERROR: chart type is not specified");return false}n=i(this,j);this.data("elycharts_env",n)}else{if(!j){j={}}j=b(j,n.opt);n.oldopt=f._clone(n.opt);n.opt=c.extend(true,n.opt,j);n.newopt=j;n.oldwidth=n.width;n.oldheight=n.height}n.cache=j.enableInternalCaching?{}:false;e(n,j);if(n.opt.autoresize){if(!n.autoresize){var m=this;n.autoresize=h(function(){m.chart()});c(window).bind("resize",n.autoresize)}}else{if(n.autoresize){c(window).unbind("resize",n.autoresize);n.autoresize=false}}var l=c.elycharts[n.opt.type].draw(n);if(n.pieces){l=g(n,n.pieces,l)}f._show(n,l);n.pieces=l;return this};function g(t,m,l,u,r,q){var o=[],s;var n=0;for(var p=0;p=l.length||!f.samePiecePath(m[p],l[n]))){if(!q){m[p].show=false;o.push(m[p])}else{s={path:false,attr:false};s.show=true;s.animation={element:m[p].element?m[p].element:false,speed:0,easing:"",delay:0};o.push(s)}}else{s=l?l[n]:{path:false,attr:false};s.show=true;if(typeof m[p].paths=="undefined"){s.animation={element:m[p].element?m[p].element:false,speed:0,easing:"",delay:0}}else{s.paths=g(t,m[p].paths,l[n].paths,m[p].section,m[p].serie,true)}o.push(s);n++}}if(l){for(;n180;a1=(a1%360)*Math.PI/180;a2=(a2%360)*Math.PI/180;if(a1==a2&&u!=t){a2+=359.99*Math.PI/180}return{path:q?[["M",o+s*Math.cos(a1),v+s*Math.sin(a1)],["A",s,s,0,+p,1,o+s*Math.cos(a2),v+s*Math.sin(a2)],["L",o+q*Math.cos(a2),v+q*Math.sin(a2)],["A",q,q,0,+p,0,o+q*Math.cos(a1),v+q*Math.sin(a1)],["z"]]:[["M",o,v],["l",s*Math.cos(a1),s*Math.sin(a1)],["A",s,s,0,+p,1,o+s*Math.cos(a2),v+s*Math.sin(a2)],["z"]]}};return m},_clone:function(m){if(m==null||typeof(m)!="object"){return m}if(m.constructor==Array){return[].concat(m)}var j=new m.constructor();for(var l in m){j[l]=this._clone(m[l])}return j},compactUnits:function(n,l){for(var m=l.length-1;m>=0;m--){var j=n/Math.pow(1000,m+1);if(j>=1&&j*10%10==0){return j+l[m]}}return n},getElementOriginalAttrs:function(l){var j=c(l.node).data("original-attr");if(!j){j=l.attr();c(l.node).data("original-attr",j)}return j},findInPieces:function(n,p,m,j,o){for(var l=0;l=0&&q.values&&q.values[n]){q=c.extend(true,q,q.values[n])}if(m.cache){if(!m.cache.areaPropsCache){m.cache.areaPropsCache={}}m.cache.areaPropsCache[o]=q}}}}else{var p=j.toLowerCase();q=this._clone(m.opt[p]);if(typeof l=="undefined"||!l){if(s&&s[p]){q=c.extend(true,q,s[p])}}else{if(m.opt["default"+r]&&m.opt["default"+r][p]){q=c.extend(true,q,m.opt["default"+r][p])}if(s&&s[l]&&s[l][p]){q=c.extend(true,q,s[l][p])}if((typeof n!="undefined")&&n>0&&q.values&&q.values[n]){q=c.extend(true,q,q.values[n])}}}return q},_absrectpath:function(l,o,j,n,p){if(p){var m=[["M",l,o+p],["a",p,p,0,0,1,p,-p],["L",j-p,o],["a",p,p,0,0,1,p,p],["L",j,n-p],["a",p,p,0,0,1,-p,p],["L",l+p,n],["a",p,p,0,0,1,-p,-p],["z"]];return m}else{return[["M",l,o],["L",l,n],["L",j,n],["L",j,o],["z"]]}},_linepathAnchors:function(m,l,x,v,s,r,o){var j=1;if(o&&o.length){j=o[1];o=o[0]}if(!o){o=1}var p=(x-m)/2,n=(s-x)/2,y=Math.atan(Math.abs(x-m)/Math.abs(v-l)),w=Math.atan(Math.abs(s-x)/Math.abs(v-r));y=(lm)||(l>v&&xx)||(r>v&&s0){y=0;w=0}else{if(Math.abs(y-Math.PI/2)0&&j>m.width-m.opt.margins[1]?m.width-m.opt.margins[1]:(l<0&&j0&&n>l.height-l.opt.margins[2]?l.height-l.opt.margins[2]:(j<0&&n0)){m[l]=o}else{if(!j){return false}}return m},showPath:function(n,o,p){if(!p){p=n.paper}if(o.length==1&&o[0][0]=="CIRCLE"){o=this._preparePathShow(n,o);return p.circle(o[0][1],o[0][2],o[0][3])}if(o.length==1&&o[0][0]=="TEXT"){o=this._preparePathShow(n,o);return p.text(o[0][2],o[0][3],o[0][1])}var m=this.getSVGProps(n,o);var l=false;for(var j in m){l=true;break}return m&&l?p.path().attr(m):false},_preparePathShow:function(j,l){return j.opt.features.pixelWorkAround.active?this.movePath(j,this._clone(l),[0.5,0.5],false,true):l},getPieceFullAttr:function(l,j){if(!j.fullattr){j.fullattr=this._clone(j.attr);if(j.path){switch(j.path[0][0]){case"CIRCLE":var m=this._preparePathShow(l,j.path);j.fullattr.cx=m[0][1];j.fullattr.cy=m[0][2];j.fullattr.r=m[0][3];break;case"TEXT":case"DOMELEMENT":case"RELEMENT":break;default:j.fullattr=this.getSVGProps(l,j.path,j.fullattr)}}if(typeof j.fullattr.opacity=="undefined"){j.fullattr.opacity=1}}return j.fullattr},_show:function(o,m){if(c.elycharts.featuresmanager){c.elycharts.featuresmanager.beforeShow(o,m)}pieces=this._getSortedPathData(m);this._animationStackStart(o);var j=false;for(var l=0;lq?1:(p.posj.pos?1:0)))})},_animationStackStart:function(j){if(!j.animationStackDepth||j.animationStackDepth==0){j.animationStackDepth=0;j.animationStack={}}j.animationStackDepth++},_animationStackEnd:function(l){l.animationStackDepth--;if(l.animationStackDepth==0){for(var j in l.animationStack){this._animationStackAnimate(l.animationStack[j],j);delete l.animationStack[j]}l.animationStack={}}},animationStackPush:function(o,n,m,j,q,r,l,p){if(typeof l=="undefined"){l=0}if(!o.animationStackDepth||o.animationStackDepth==0){this._animationStackAnimate([{piece:n,object:m,props:j,speed:q,easing:r,force:p}],l)}else{if(!o.animationStack[l]){o.animationStack[l]=[]}o.animationStack[l].push({piece:n,object:m,props:j,speed:q,easing:r,force:p})}},_animationStackAnimate:function(j,m){var l=this;var n=function(){var p=j.pop();var q=l._animationStackAnimateElement(p);while(j.length>0){var o=j.pop();l._animationStackAnimateElement(o,p,q)}};if(m>0){setTimeout(n,m)}else{n()}},_animationStackAnimateElement:function(j,o,n){if(j.force||!j.piece.animationInProgress){j.object.stop();if(!j.props){j.props={opacity:0}}if(!j.speed||j.speed<=0){j.object.attr(j.props);j.piece.animationInProgress=false;return}j.piece.animationInProgress=true;var l=function(){j.piece.animationInProgress=false};if(Raphael.animation){var m=Raphael.animation(j.props,j.speed,j.easing?j.easing:"linear",l);if(o){j.object.animateWith(o,n,m)}else{j.object.animate(m)}return m}else{if(o){j.object.animateWith(o,j.props,j.speed,j.easing?j.easing:"linear",l)}else{j.object.animate(j.props,j.speed,j.easing?j.easing:"linear",l)}return null}}return null}};var f=c.elycharts.common;c.elycharts.featuresmanager={managers:[],initialized:false,register:function(l,j){c.elycharts.featuresmanager.managers.push([j,l]);c.elycharts.featuresmanager.initialized=false},init:function(){c.elycharts.featuresmanager.managers.sort(function(l,j){return l[0]=0;j--){if(c.elycharts.featuresmanager.managers[j][1].clear){c.elycharts.featuresmanager.managers[j][1].clear(l)}}},beforeShow:function(l,m){if(!c.elycharts.featuresmanager.initialized){this.init()}for(var j=0;j0){var g=e.onAnchors.pop();b(g[0]).removeClass(g[1])}}};b.elycharts.featuresmanager.register(b.elycharts.anchormanager,30)})(jQuery);(function(b){var a=b.elycharts.common;b.elycharts.animationmanager={beforeShow:function(c,d){if(!c.newopt){this.startAnimation(c,d)}else{this.stepAnimation(c,d)}},stepAnimation:function(c,d){d=this._stepAnimationInt(c,d)},_stepAnimationInt:function(e,h,j,g,c){for(var d=0;d1){for(e=0;!g.paths[e].path&&e=0;e--){}d=g.paths[e].path?a.getY(g.paths[e].path[0]):0;for(e=0;e1){for(e=0;!g.paths[e].path&&e=0;e--){}d=g.paths[e].path?a.getY(g.paths[e].path[0]):0;for(e=0;e0){var e=d.highlighted.pop();if(e.piece){if(c){a.animationStackPush(d,e.piece,e.piece.element,a.getPieceFullAttr(d,e.piece),e.cfg.restoreSpeed,e.cfg.restoreEasing,0,true)}}else{e.element.remove()}}}},afterShow:function(c,d){if(c.highlighted&&c.highlighted.length>0){this.removeHighlighted(c,false)}c.highlighted=[]},onMouseOver:function(D,u,j,H){var s,c;for(var B=0;B90){E=90}s=[["SLICE",s[0][1],s[0][2],s[0][3]*J[0],s[0][4],s[0][5]-E,s[0][6]+E]];a.animationStackPush(D,e,c,a.getSVGProps(D,s),f.highlight.scaleSpeed,f.highlight.scaleEasing)}else{if(D.opt.type=="funnel"){var p=(e.rect[2]-e.rect[0])*(J[0]-1)/2;var n=(e.rect[3]-e.rect[1])*(J[1]-1)/2;s=[a.movePath(D,[s[0]],[-p,-n])[0],a.movePath(D,[s[1]],[+p,-n])[0],a.movePath(D,[s[2]],[+p,+n])[0],a.movePath(D,[s[3]],[-p,+n])[0],s[4]];a.animationStackPush(D,e,c,a.getSVGProps(D,s),f.highlight.scaleSpeed,f.highlight.scaleEasing,0,true);r=false;if(j>0){G=H.pieces[B].paths[j-1];r=G.element;y=G.path}else{G=a.findInPieces(H.pieces,"Sector","top");if(G){r=G.element;y=G.path}}if(r){y=[y[0],y[1],a.movePath(D,[y[2]],[+p,-n])[0],a.movePath(D,[y[3]],[-p,-n])[0],y[4]];a.animationStackPush(D,G,r,a.getSVGProps(D,y),f.highlight.scaleSpeed,f.highlight.scaleEasing,0,true);D.highlighted.push({piece:G,cfg:f.highlight})}r=false;if(j0?D.opt.labels.length:1);var z=(D.width-D.opt.margins[3]-D.opt.margins[1])/(D.opt.labels.length>1?D.opt.labels.length-1:1);var m=true;switch(q){case"bar":s=[["RECT",D.opt.margins[3]+j*A,D.opt.margins[0],D.opt.margins[3]+(j+1)*A,D.height-D.opt.margins[2]]];break;case"line":m=false;case"barline":var l=Math.round((m?A/2:0)+D.opt.margins[3]+j*(m?A:z));s=[["M",l,D.opt.margins[0]],["L",l,D.height-D.opt.margins[2]]]}if(s){c=a.showPath(D,s).attr(D.opt.features.highlight.indexHighlightProps);D.highlighted.push({element:c,attr:false,cfg:D.opt.features.highlight})}}},onMouseOut:function(e,f,d,c){this.removeHighlighted(e,true)}};b.elycharts.featuresmanager.register(b.elycharts.highlightmanager,21)})(jQuery);(function(b){var a=b.elycharts.common;b.elycharts.labelmanager={beforeShow:function(f,g){if(!a.executeIfChanged(f,["labels","values","series"])){return}if(f.opt.labels&&(f.opt.type=="pie"||f.opt.type=="funnel")){var j=false;var h;for(var d=0;d"+s+"").css(g).prependTo(o.container)}else{i=b(s).css(g).prependTo(o.container)}if(o.opt.features.debug.active&&i.height()==0){alert("DEBUG: Al gestore label e' stata passata una label ancora senza dimensioni, quindi ancora non disegnata. Per questo motivo il posizionamento potrebbe non essere correto.")}var e=d[0];var c=d[1];if(!j.label.frameAnchor||j.label.frameAnchor[0]=="middle"){e-=i.width()/2}else{if(j.label.frameAnchor&&j.label.frameAnchor[0]=="end"){e-=i.width()}}if(!j.label.frameAnchor||j.label.frameAnchor[1]=="middle"){c-=i.height()/2}else{if(j.label.frameAnchor&&j.label.frameAnchor[1]=="top"){c-=i.height()}}if(f){i.css({margin:c+"px 0 0 "+e+"px",opacity:n})}else{i.css({margin:c+"px 0 0 "+e+"px"})}return{path:[["DOMELEMENT",i]],attr:false}}}return false}};b.elycharts.featuresmanager.register(b.elycharts.labelmanager,5)})(jQuery);(function(b){var a=b.elycharts.common;b.elycharts.legendmanager={afterShow:function(L,l){if(L.legenditems){for(item in L.legenditems){L.legenditems[item].remove()}L.legenditems=false}if(!L.opt.legend||L.opt.legend.length==0){return}var d=L.opt.features.legend;if(d===false){return}var e=d.x;if(e=="auto"){var D=1;e=0}var c=d.width;if(c=="auto"){var o=1;c=L.width}var H=0;var A=[];var f=0;var B,M,K,r,q,p,G;for(B in L.opt.legend){if(L.opt.type!="pie"){f++}else{f+=L.opt.legend[B].length}}var J=0;for(B in L.opt.legend){if(L.opt.type!="pie"){M=[L.opt.legend[B]]}else{M=L.opt.legend[B]}for(var I=0;I(r-G)&&u.getBBox().width>10){v=v.substring(0,v.length-1);u.attr({text:v})}u.show();if(d.horizontal&&d.itemWidth=="auto"){H+=G+u.getBBox().width+4}else{if(!d.horizontal&&o){H=u.getBBox().width+G>H?u.getBBox().width+G:H}else{H+=r}}J++}}if(o){c=H+d.margins[3]+d.margins[1]-1}if(D){e=Math.floor((L.width-c)/2);for(J in A){if(A[J].attrs.x){A[J].attr("x",A[J].attrs.x+e)}else{A[J].attr("path",a.movePath(L,A[J].attrs.path,[e,0]))}}}var s=[["RECT",e,d.y,e+c,d.y+d.height,d.r]];var E=a.showPath(L,s).attr(d.borderProps);for(J in A){A[J].toFront()}A.unshift(E);L.legenditems=A}};b.elycharts.featuresmanager.register(b.elycharts.legendmanager,90)})(jQuery);(function(c){var a=c.elycharts.featuresmanager;var b=c.elycharts.common;c.elycharts.mousemanager={clear:function(d){if(d.mouseLayer){d.mouseLayer.remove();d.mouseLayer=null;d.mousePaper.clear();d.mousePaper.remove();d.mousePaper=null;d.mouseTimer=null;d.mouseAreas=null}},afterShow:function(n,g){if(!n.opt.interactive){return}this.clear(n);n.mouseLayer=c("
    ").css({position:"absolute","z-index":20,opacity:1}).prependTo(n.container);n.mousePaper=b._RaphaelInstance(n.mouseLayer.get(0),n.width,n.height);var f=n.mousePaper;if(n.opt.features.debug.active&&typeof DP_Debug!="undefined"){n.paper.text(n.width,n.height-5,"DEBUG").attr({"text-anchor":"end",stroke:"red",opacity:0.1});f.text(n.width,n.height-5,"DEBUG").attr({"text-anchor":"end",stroke:"red",opacity:0.1}).click(function(){DP_Debug.dump(n.opt,"",false,4)})}var l,h;n.mouseAreas=[];if(n.opt.features.mousearea.type=="single"){for(l=0;l=1&&(g[l].path[0][0]=="LINE"||g[l].path[0][0]=="LINEAREA")){for(h=0;h0?n.opt.labels.length:1);e=n.opt.margins[3]}else{r=(n.width-n.opt.margins[3]-n.opt.margins[1])/(n.opt.labels.length>1?n.opt.labels.length-1:1);e=n.opt.margins[3]-r/2}for(var q in n.opt.labels){var m=parseInt(q);n.mouseAreas.push({path:[["RECT",e+m*r,n.height-n.opt.margins[2],e+(m+1)*r,n.opt.margins[0]]],piece:false,pieces:g,index:parseInt(m),props:n.opt.defaultSeries})}}var p=false;if(!n.opt.features.mousearea.syncTag){n.mouseareaenv={chartEnv:false,mouseObj:false,caller:false,inArea:-1,timer:false};p=n.mouseareaenv}else{if(!c.elycharts.mouseareaenv){c.elycharts.mouseareaenv={}}if(!c.elycharts.mouseareaenv[n.opt.features.mousearea.syncTag]){c.elycharts.mouseareaenv[n.opt.features.mousearea.syncTag]={chartEnv:false,mouseObj:false,caller:false,inArea:-1,timer:false}}p=c.elycharts.mouseareaenv[n.opt.features.mousearea.syncTag]}for(l=0;l