nzbget-19.1/0000755000175000017500000000000013130203062012537 5ustar andreasandreasnzbget-19.1/COPYING0000644000175000017500000004313113130203062013574 0ustar andreasandreas GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. nzbget-19.1/osx/0000755000175000017500000000000013130203062013350 5ustar andreasandreasnzbget-19.1/webui/0000755000175000017500000000000013130203062013652 5ustar andreasandreasnzbget-19.1/webui/status.js0000644000175000017500000012361613130203062015544 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2017 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, see . */ /* * In this module: * 1) Status Infos on main page (speed, time, paused state etc.); * 2) Statistics and Status dialog; * 3) Limit dialog (speed and active news servers); * 4) Filter menu. */ /*** STATUS INFOS ON MAIN PAGE AND STATISTICS DIALOG ****************************************/ var Status = (new function($) { 'use strict'; // Properties (public) this.status; // Controls var $CHPauseDownload; var $CHPausePostProcess; var $CHPauseScan; var $StatusPausing; var $StatusPaused; var $StatusLeft; var $StatusSpeed; var $StatusSpeedIcon; var $StatusTimeIcon; var $StatusTime; var $StatusURLs; var $PlayBlock; var $PlayButton; var $PauseButton; var $PlayAnimation; var $StatDialog; var $ScheduledPauseDialog; var $PauseForInput; var $PauseForPreview; // State var status; var lastPlayState = 0; var lastAnimState = 0; var playInitialized = false; var modalShown = false; var titleGen = []; var validTimePatterns = [ /^=\d{1,2}(:[0-5][0-9])?$/, // 24h exact /^=\d{1,2}(:[0-5][0-9])?(AM|PM)$/i, // 12h exact /^\d+(:[0-5][0-9])?$/, // 24h relative /^\d+(h|m)?$/i, // relative minutes or hours ]; this.init = function() { $CHPauseDownload = $('#CHPauseDownload'); $CHPausePostProcess = $('#CHPausePostProcess'); $CHPauseScan = $('#CHPauseScan'); $PlayBlock = $('#PlayBlock'); $PlayButton = $('#PlayButton'); $PauseButton = $('#PauseButton'); $PlayAnimation = $('#PlayAnimation'); $StatusPausing = $('#StatusPausing'); $StatusPaused = $('#StatusPaused'); $StatusLeft = $('#StatusLeft'); $StatusSpeed = $('#StatusSpeed'); $StatusSpeedIcon = $('#StatusSpeedIcon'); $StatusTimeIcon = $('#StatusTimeIcon'); $StatusTime = $('#StatusTime'); $StatusURLs = $('#StatusURLs'); $ScheduledPauseDialog = $('#ScheduledPauseDialog') $PauseForInput = $('#PauseForInput'); $PauseForPreview = $('#PauseForPreview'); if (UISettings.setFocus) { $ScheduledPauseDialog.on('shown', function() { $('#PauseForInput').focus(); }); } $PlayAnimation.hover(function() { $PlayBlock.addClass('hover'); }, function() { $PlayBlock.removeClass('hover'); }); $PauseForInput.keyup(function(e) { if (e.which == 13) return; calculateSeconds($(this).val()); }); // temporary pause the play animation if any modal is shown (to avoid artifacts in safari) $('body >.modal').on('show', modalShow); $('body > .modal').on('hide', modalHide); StatDialog.init(); FilterMenu.init(); initTitle(); } this.update = function() { var _this = this; RPC.call('status', [], function(curStatus) { status = curStatus; _this.status = status; StatDialog.update(); }); } this.redraw = function() { redrawInfo(); StatDialog.redraw(); } function redrawInfo() { Util.show($CHPauseDownload, status.DownloadPaused); Util.show($CHPausePostProcess, status.PostPaused); Util.show($CHPauseScan, status.ScanPaused); updatePlayAnim(); updatePlayButton(); if (status.ServerStandBy) { $StatusSpeed.html('--- MB/s'); if (status.ResumeTime > 0) { $StatusTime.html(Util.formatTimeLeft(status.ResumeTime - status.ServerTime)); } else if (status.RemainingSizeMB > 0 || status.RemainingSizeLo > 0) { if (status.AverageDownloadRate > 0) { $StatusTime.html(Util.formatTimeLeft(status.RemainingSizeMB*1024/(status.AverageDownloadRate/1024))); } else { $StatusTime.html('--h --m'); } } else { $StatusTime.html('0h 0m'); } } else { $StatusSpeed.html(Util.formatSpeed(status.DownloadRate)); if (status.DownloadRate > 0) { $StatusTime.html(Util.formatTimeLeft( (status.DownloadPaused ? status.ForcedSizeMB : status.RemainingSizeMB) *1024/(status.DownloadRate/1024))); } else { $StatusTime.html('--h --m'); } } var limit = status.DownloadLimit > 0; if (!limit) { for (var i=0; i < Status.status.NewsServers.length; i++) { limit = !Status.status.NewsServers[i].Active; if (limit) { break; } } } $StatusSpeedIcon.toggleClass('icon-plane', !limit); $StatusSpeedIcon.toggleClass('icon-truck', limit); var statWarning = (status.ServerStandBy && status.ResumeTime > 0) || status.QuotaReached; $StatusTime.toggleClass('orange', statWarning); $StatusTimeIcon.toggleClass('icon-time', !statWarning); $StatusTimeIcon.toggleClass('icon-time-orange', statWarning); updateTitle(); } function updatePlayButton() { var Play = !status.DownloadPaused; if (Play === lastPlayState) { return; } lastPlayState = Play; var hideBtn = Play ? $PlayButton : $PauseButton; var showBtn = !Play ? $PlayButton : $PauseButton; if (playInitialized) { hideBtn.fadeOut(500); showBtn.fadeIn(500); if (!Play && !status.ServerStandBy) { PopupNotification.show('#Notif_Downloads_Pausing'); } } else { hideBtn.hide(); showBtn.show(); } if (Play) { $PlayAnimation.removeClass('pause').addClass('play'); } else { $PlayAnimation.removeClass('play').addClass('pause'); } playInitialized = true; } function updatePlayAnim() { // Animate if either any downloads or post-processing is in progress var Anim = (!status.ServerStandBy || status.FeedActive || status.QueueScriptCount > 0 || (status.PostJobCount > 0 && !status.PostPaused) || (status.UrlCount > 0 && (!status.DownloadPaused || Options.option('UrlForce') === 'yes'))) && (UISettings.refreshInterval !== 0) && !UISettings.connectionError && UISettings.activityAnimation; if (Anim === lastAnimState) { return; } lastAnimState = Anim; if (!modalShown) { if (Anim) { $PlayAnimation.fadeIn(1000); } else { $PlayAnimation.fadeOut(1000); } } } this.playClick = function() { //PopupNotification.show('#Notif_Play'); if (lastPlayState) { // pause all activities RPC.call('pausedownload', [], function(){RPC.call('pausepost', [], function(){RPC.call('pausescan', [], Refresher.update)})}); } else { // resume all activities RPC.call('resumedownload', [], function(){RPC.call('resumepost', [], function(){RPC.call('resumescan', [], Refresher.update)})}); } } this.pauseClick = function(data) { switch (data) { case 'download': var method = status.DownloadPaused ? 'resumedownload' : 'pausedownload'; break; case 'post': var method = status.PostPaused ? 'resumepost' : 'pausepost'; break; case 'scan': var method = status.ScanPaused ? 'resumescan' : 'pausescan'; break; } RPC.call(method, [], Refresher.update); } this.statDialogClick = function() { StatDialog.showModal(); } this.scheduledPauseClick = function(seconds) { RPC.call('pausedownload', [], function(){RPC.call('pausepost', [], function(){RPC.call('pausescan', [], function(){RPC.call('scheduleresume', [seconds], Refresher.update)})})}); } this.scheduledPauseDialogClick = function() { $PauseForInput.val(''); $PauseForPreview.addClass('invisible'); $ScheduledPauseDialog.modal(); } this.pauseForClick = function() { var val = $PauseForInput.val(); var seconds = calculateSeconds(val); if (isNaN(seconds) || seconds <= 0) { return; } $ScheduledPauseDialog.modal('hide'); this.scheduledPauseClick(seconds); } function isTimeInputValid(str) { for (var i = 0; i < validTimePatterns.length; i++) { if (validTimePatterns[i].test(str)) return true; } } function calculateSeconds(parsable) { parsable = parsable.toLowerCase(); if (!isTimeInputValid(parsable)) { $PauseForPreview.addClass('invisible'); return; } var now = new Date(), future = new Date(); var hours = 0, minutes = 0; var mode = /^=/.test(parsable) ? 'exact' : 'relative'; var indicator = (parsable.match(/h|m|am|pm$/i) || [])[0]; var parsedTime = parsable.match(/(\d+):?(\d+)?/) || []; var primaryValue = parsedTime[1]; var secondaryValue = parsedTime[2]; var is12H = (indicator === 'am' || indicator === 'pm') if (indicator === undefined && secondaryValue === undefined) { if (mode === 'exact') hours = parseInt(primaryValue); else minutes = parseInt(primaryValue); } else if (indicator === 'm') { minutes = parseInt(primaryValue); } else { hours = parseInt(primaryValue); if (secondaryValue) minutes = parseInt(secondaryValue); if (indicator === 'pm' && hours < 12) hours += 12; } if ((mode !== 'exact' && (is12H || (hours > 0 && minutes > 59))) || (mode === 'exact' && (hours < 0 || hours > 23 || minutes < 0 || minutes > 59))) { $PauseForPreview.addClass('invisible'); return; } if (mode === 'exact') { future.setHours(hours, minutes, 0, 0); if (future < now) future.setDate(now.getDate() + 1); } else { future.setHours(now.getHours() + hours, now.getMinutes() + minutes); } $PauseForPreview.find('strong') .text((future.getDay() !== now.getDay()) ? future.toLocaleString() : future.toLocaleTimeString()) .end() .removeClass('invisible'); return (future - now)/1000; } function modalShow() { modalShown = true; if (lastAnimState) { $PlayAnimation.hide(); } } function modalHide() { if (lastAnimState) { $PlayAnimation.show(); } modalShown = false; } this.serverName = function(server) { 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); } return name; } function initTitle() { function format(pattern, paramFunc) { if (UISettings.connectionError) { var value = '?'; var isEmpty = false; } else { var param = paramFunc(); var value = param[0]; var isEmpty = param[1]; } if (isEmpty && pattern != '%VAR%') { return ''; } switch (pattern) { case '%VAR%': return value; case '%VAR-%': return '' + value + ' - '; case '%(VAR)%': return '(' + value + ')'; case '%(VAR-)%': return '(' + value + ') - '; case '%[VAR]%': return '[' + value + ']'; case '%[VAR-]%': return '[' + value + '] - '; } return Downloads.groups.length > 0 ? '' + Downloads.groups.length + ' - ' : ''; }; function fill(varname, paramFunc) { titleGen['%' + varname + '%'] = function() { return format('%VAR%', paramFunc); }; titleGen['%' + varname + '-%'] = function() { return format('%VAR-%', paramFunc); }; titleGen['%(' + varname + ')%'] = function() { return format('%(VAR)%', paramFunc); }; titleGen['%(' + varname + '-)%'] = function() { return format('%(VAR-)%', paramFunc); }; titleGen['%[' + varname + ']%'] = function() { return format('%[VAR]%', paramFunc); }; titleGen['%[' + varname + '-]%'] = function() { return format('%[VAR-]%', paramFunc); }; } fill('COUNT', function() { return [Downloads.groups.length, Downloads.groups.length == 0]; }); fill('SPEED', function() { return [$StatusSpeed.text(), status.ServerStandBy]; }); fill('TIME', function() { return [$StatusTime.text(), status.ServerStandBy]; }); fill('PAUSE', function() { return ['||', !status.DownloadPaused]; }); } function updateTitle() { var title = UISettings.windowTitle; for (var name in titleGen) { if (title.indexOf(name) > -1) { var value = titleGen[name](); title = title.replace(name, value); } } title = title.trim(); window.document.title = title; } this.updateTitle = updateTitle; }(jQuery)); /*** STATISTICS DIALOG *******************************************************/ var StatDialog = (new function($) { 'use strict'; // Controls var $StatDialog; var $StatDialog_DataVersion; var $StatDialog_DataUptime; var $StatDialog_DataDownloadTime; var $StatDialog_DataTotalDownloaded; var $StatDialog_DataRemaining; var $StatDialog_DataFree; var $StatDialog_DataAverageSpeed; var $StatDialog_DataCurrentSpeed; var $StatDialog_DataSpeedLimit; var $StatDialog_ArticleCache; var $StatDialog_QueueScripts; var $StatDialog_ChartBlock; var $StatDialog_Server; var $StatRangeDialog; var $StatRangeDialog_PeriodInput; var $StatDialog_Tooltip; var $StatDialog_TodaySize; var $StatDialog_MonthSize; var $StatDialog_AllTimeSize; var $StatDialog_CustomSize; var $StatDialog_Custom; // State var visible = false; var lastPage; var lastTab = null; var lastFullscreen; var servervolumes = null; var prevServervolumes = null; var curRange = 'MIN'; var redrawLock = 0; var needChartUpdate = false; var curServer = 0; var monthListInitialized = false; var curMonth = null; var monYear = false; var monStartIndex = 0; var monEndIndex = 0; var monStartDate; var chartData = null; var mouseOverIndex = -1; var clockOK = false; var volumeMode = false; var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; this.init = function() { $StatDialog = $('#StatDialog'); $StatDialog_DataVersion = $('#StatDialog_DataVersion'); $StatDialog_DataUptime = $('#StatDialog_DataUptime'); $StatDialog_DataDownloadTime = $('#StatDialog_DataDownloadTime'); $StatDialog_DataTotalDownloaded = $('#StatDialog_DataTotalDownloaded'); $StatDialog_DataRemaining = $('#StatDialog_DataRemaining'); $StatDialog_DataFree = $('#StatDialog_DataFree'); $StatDialog_DataAverageSpeed = $('#StatDialog_DataAverageSpeed'); $StatDialog_DataCurrentSpeed = $('#StatDialog_DataCurrentSpeed'); $StatDialog_DataSpeedLimit = $('#StatDialog_DataSpeedLimit'); $StatDialog_ArticleCache = $('#StatDialog_ArticleCache'); $StatDialog_QueueScripts = $('#StatDialog_QueueScripts'); $StatDialog_ChartBlock = $('#StatDialog_ChartBlock'); $StatDialog_Server = $('#StatDialog_Server'); $StatRangeDialog = $('#StatRangeDialog'); $StatRangeDialog_PeriodInput = $('#StatRangeDialog_PeriodInput'); $StatDialog_Tooltip = $('#StatDialog_Tooltip'); $StatDialog_TodaySize = $('#StatDialog_TodaySize'); $StatDialog_MonthSize = $('#StatDialog_MonthSize'); $StatDialog_AllTimeSize = $('#StatDialog_AllTimeSize'); $StatDialog_CustomSize = $('#StatDialog_CustomSize'); $StatDialog_Custom = $('#StatDialog_Custom'); $('#StatDialog_ServerMenuAll').click(chooseServer); $('#StatDialog_Volumes').click(tabClick); $('#StatDialog_Back').click(backClick); $StatDialog.on('hidden', function() { // cleanup lastTab = null; servervolumes = null; prevServervolumes = null; $StatDialog_ChartBlock.empty(); visible = false; }); if (UISettings.setFocus) { $StatRangeDialog.on('shown', function() { $StatRangeDialog_PeriodInput.focus(); }); } $StatRangeDialog.on('hidden', StatRangeDialogHidden); TabDialog.extend($StatDialog); } this.update = function() { if (visible) { RPC.call('servervolumes', [], servervolumes_loaded); } else { RPC.next(); } } function servervolumes_loaded(volumes) { prevServervolumes = servervolumes; servervolumes = volumes; RPC.next(); } function firstLoadStatisticsData() { RPC.call('servervolumes', [], function (volumes) { prevServervolumes = servervolumes; servervolumes = volumes; updateMonthList(); StatDialog.redraw(); }); } this.showModal = function(serverId) { volumeMode = serverId !== undefined; curServer = serverId !== undefined ? serverId : 0; $('#StatDialog_GeneralTab').show(); $('#StatDialog_VolumesTab').hide(); $('#StatDialog_Back').hide(); $('#StatDialog_BackSpace').show(); $('#StatDialog_Title').text('Statistics and Status'); Util.show('#StatDialog_ArticleCache_Row', Options.option('ArticleCache') !== '0'); Util.show('#StatDialog_QueueScripts_Row', Status.status.QueueScriptCount > 0); $StatDialog.removeClass('modal-large').addClass('modal-mini'); if (Options.option('QuotaStartDay') != '1') { $('#StatDialog_MonthTitle').text('Billing month:'); } $('#StatDialog_Volume_MONTH, #StatDialog_Volume_MONTH2').text(monthNames[(new Date()).getMonth()] + ' ' + (new Date()).getFullYear()); monthListInitialized = false; updateServerList(); lastTab = null; $StatDialog.restoreTab(); visible = true; redrawStatistics(); $StatDialog.modal(); firstLoadStatisticsData(); if (volumeMode) { $('#StatDialog_Volumes').click(); } } this.redraw = function() { if (visible) { redrawStatistics(); if (servervolumes !== null && lastTab === '#StatDialog_VolumesTab') { if (redrawLock > 0) { needChartUpdate = true; } else { if (!monthListInitialized) { updateMonthList(); } redrawChart(); } } } } function redrawStatistics() { var status = Status.status; $StatDialog_DataVersion.text(Options.option('Version')); $StatDialog_DataUptime.text(Util.formatTimeHMS(status.UpTimeSec)); $StatDialog_DataDownloadTime.text(Util.formatTimeHMS(status.DownloadTimeSec)); $StatDialog_DataTotalDownloaded.html(Util.formatSizeMB(status.DownloadedSizeMB)); $StatDialog_DataRemaining.html(Util.formatSizeMB(status.RemainingSizeMB)); $StatDialog_DataFree.html(Util.formatSizeMB(status.FreeDiskSpaceMB)); $StatDialog_DataAverageSpeed.html(Util.formatSpeed(status.AverageDownloadRate)); $StatDialog_DataCurrentSpeed.html(Util.formatSpeed(status.DownloadRate)); $StatDialog_DataSpeedLimit.html(Util.formatSpeed(status.DownloadLimit)); $StatDialog_ArticleCache.html(Util.formatSizeMB(status.ArticleCacheMB, status.ArticleCacheLo)); $StatDialog_QueueScripts.html(status.QueueScriptCount); var content = ''; content += 'Download' + (status.DownloadPaused ? 'paused' : 'active') + ''; content += 'Post-processing' + (Options.option('PostProcess') === '' ? 'disabled' : (status.PostPaused ? 'paused' : 'active')) + ''; content += 'NZB-Directory scan' + (Options.option('NzbDirInterval') === '0' ? 'disabled' : (status.ScanPaused ? 'paused' : 'active')) + ''; if (status.QuotaReached) { content += 'Download quotareached'; } if (status.ResumeTime > 0) { content += 'Autoresume' + Util.formatTimeHMS(status.ResumeTime - status.ServerTime) + ''; } $('#StatusTable tbody').html(content); } function tabClick(e) { e.preventDefault(); if (!volumeMode) { $('#StatDialog_Back').fadeIn(500); $('#StatDialog_BackSpace').hide(); } lastTab = '#' + $(this).attr('data-tab'); lastPage = $(lastTab); lastFullscreen = ($(this).attr('data-fullscreen') === 'true') && !UISettings.miniTheme; redrawLock++; $StatDialog.switchTab($('#StatDialog_GeneralTab'), lastPage, e.shiftKey || !UISettings.slideAnimation || volumeMode ? 0 : 500, { fullscreen: lastFullscreen, toggleClass: 'modal-mini modal-large', mini: UISettings.miniTheme, complete: tabSwitchCompleted}); if (lastTab === '#StatDialog_VolumesTab') { if (servervolumes) { redrawChart(); } $('#StatDialog_Title').text('Downloaded volumes'); } } function backClick(e) { e.preventDefault(); $('#StatDialog_Back').fadeOut(500, function() { $('#StatDialog_BackSpace').show(); }); $StatDialog.switchTab(lastPage, $('#StatDialog_GeneralTab'), e.shiftKey || !UISettings.slideAnimation ? 0 : 500, { fullscreen: lastFullscreen, toggleClass: 'modal-mini modal-large', mini: UISettings.miniTheme, back: true}); lastTab = null; $('#StatDialog_Title').text('Statistics and Status'); } function tabSwitchCompleted() { redrawLock--; if (needChartUpdate) { needChartUpdate = false; if (!monthListInitialized) { updateMonthList(); } redrawChart(); } Frontend.alignPopupMenu('#StatDialog_MonthMenu', true); } function size64(size) { return size.SizeMB < 2000 ? size.SizeLo / 1024.0 / 1024.0 : size.SizeMB; } function redrawChart() { var serverNo = curServer; var lineLabels = []; var dataLabels = []; var chartDataTB = []; var chartDataGB = []; var chartDataMB = []; var chartDataKB = []; var chartDataB = []; var curPoint = null; var sumMB = 0; var sumLo = 0; var maxSizeMB = 0; var maxSizeLo = 0; function addData(bytes, dataLab, lineLab) { dataLabels.push(dataLab); lineLabels.push(lineLab); if (bytes === null) { chartDataTB.push(null); chartDataGB.push(null); chartDataMB.push(null); chartDataKB.push(null); chartDataB.push(null); return; } chartDataTB.push(bytes.SizeMB / 1024.0 / 1024.0); chartDataGB.push(bytes.SizeMB / 1024.0); chartDataMB.push(size64(bytes)); chartDataKB.push(bytes.SizeLo / 1024.0); chartDataB.push(bytes.SizeLo); if (bytes.SizeMB > maxSizeMB) { maxSizeMB = bytes.SizeMB; } if (bytes.SizeLo > maxSizeLo) { maxSizeLo = bytes.SizeLo; } sumMB += bytes.SizeMB; sumLo += bytes.SizeLo; } function drawMinuteGraph() { // the current slot may be not fully filled yet, // to make the chart smoother for current slot we use the data from the previous reading // and we show the previous slot as current. curPoint = servervolumes[serverNo].SecSlot; for (var i = 0; i < 60; i++) { addData((i == curPoint && prevServervolumes !== null ? prevServervolumes : servervolumes)[serverNo].BytesPerSeconds[i], i + 's', i % 10 == 0 || i == 59 ? i : ''); } if (prevServervolumes !== null) { curPoint = curPoint > 0 ? curPoint-1 : 59; } } function drawHourGraph() { for (var i = 0; i < 60; i++) { addData(servervolumes[serverNo].BytesPerMinutes[i], i + 'm', i % 10 == 0 || i == 59 ? i : ''); } curPoint = servervolumes[serverNo].MinSlot; } function drawDayGraph() { for (var i = 0; i < 24; i++) { addData(servervolumes[serverNo].BytesPerHours[i], i + 'h', i % 3 == 0 || i == 23 ? i : ''); } curPoint = servervolumes[serverNo].HourSlot; } function drawMonthGraph() { var len = servervolumes[serverNo].BytesPerDays.length; var daySlot = servervolumes[serverNo].DaySlot; var slotDelta = servervolumes[0].FirstDay - servervolumes[serverNo].FirstDay; var dt = new Date(monStartDate.getTime()); var day = 1; for (var i = monStartIndex; i <= monEndIndex; i++, day++) { dt.setDate(day); var slot = i + slotDelta; addData((slot >= 0 && slot < len ? servervolumes[serverNo].BytesPerDays[slot] : null), dt.toDateString(), (day == 1 || day % 5 == 0 || (day < 30 && i === monEndIndex) ? day : '')); if (slot === daySlot) { curPoint = day-1; } } // ensure the line has always the same length (looks nicer) for (; day < 32; day++) { addData(null, null, null); } } function drawYearGraph() { var firstMon = -1; var lastMon = -1; var monDataMB = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; var monDataLo = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // aggregate daily volumes into months var len = servervolumes[serverNo].BytesPerDays.length; var daySlot = servervolumes[serverNo].DaySlot; var slotDelta = servervolumes[0].FirstDay - servervolumes[serverNo].FirstDay; var startDate = new Date(monStartDate.getTime()); var day = 0; for (var i = monStartIndex; i <= monEndIndex; i++, day++) { var dt = new Date(monStartDate.getTime() + day*24*60*60*1000); var slot = i + slotDelta; var bytes = servervolumes[serverNo].BytesPerDays[slot]; if (bytes) { var mon = dt.getMonth(); monDataMB[mon] += bytes.SizeMB; monDataLo[mon] += bytes.SizeLo; if (firstMon === -1) { firstMon = mon; } if (mon > lastMon) { lastMon = mon; } if (slot === daySlot) { curPoint = mon; } } } for (var i = 0; i < 12; i++) { addData(firstMon > -1 && i >= firstMon && i <= lastMon ? {SizeMB: monDataMB[i], SizeLo: monDataLo[i]} : null, monthNames[i] + ' ' + curMonth, monthNames[i].substr(0, 3)); } } if (curRange === 'MIN') { drawMinuteGraph(); } else if (curRange === 'HOUR') { drawHourGraph(); } else if (curRange === 'DAY') { drawDayGraph(); } else if (curRange === 'MONTH' && !monYear) { drawMonthGraph(); } else if (curRange === 'MONTH' && monYear) { drawYearGraph(); } var serieData = maxSizeMB > 1024*1024 ? chartDataTB : maxSizeMB > 1024 ? chartDataGB : maxSizeMB > 1 || maxSizeLo == 0 ? chartDataMB : maxSizeLo > 1024 ? chartDataKB : chartDataB; var units = maxSizeMB > 1024*1024 ? ' TB' : maxSizeMB > 1024 ? ' GB' : maxSizeMB > 1 || maxSizeLo == 0 ? ' MB' : maxSizeLo > 1024 ? ' KB' : ' B'; var curPointData = []; for (var i = 0; i < serieData.length; i++) { curPointData.push(i===curPoint ? serieData[i] : null); } chartData = { serieData: serieData, serieDataMB: chartDataMB, serieDataLo: chartDataB, sumMB: sumMB, sumLo: sumLo, dataLabels: dataLabels }; $StatDialog_ChartBlock.empty(); $StatDialog_ChartBlock.html('
'); $('#StatDialog_Chart').chart({ values: { serie1 : serieData, serie2: curPointData }, labels: lineLabels, type: 'line', margins: [10, 15, 20, 60], defaultSeries: { rounded: 0.5, fill: true, plotProps: { 'stroke-width': 3.0 }, dot: true, dotProps: { stroke: '#FFF', size: 3.0, 'stroke-width': 1.0, fill: '#5AF' }, highlight: { scaleSpeed: 0, scaleEasing: '>', scale: 2.0 }, tooltip: { active: false, }, color: '#5AF' }, series: { serie2: { dotProps: { stroke: '#F21860', fill: '#F21860', size: 3.5, 'stroke-width': 2.5 }, highlight: { scale: 1.5 }, } }, defaultAxis: { labels: true, labelsProps: { 'font-size': 13 }, labelsDistance: 12 }, axis: { l: { labels: true, suffix: units } }, features: { grid: { draw: [true, false], forceBorder: true, props: { stroke: '#e0e0e0', 'stroke-width': 1 }, ticks: { active: [true, false, false], size: [6, 0], props: { stroke: '#e0e0e0' } } }, mousearea: { type: 'axis', onMouseOver: chartMouseOver, onMouseExit: chartMouseExit, onMouseOut: chartMouseExit }, } }); simulateMouseEvent(); updateCounters(); } function chartMouseOver(env, serie, index, mouseAreaData) { if (mouseOverIndex > -1) { var env = $('#StatDialog_Chart').data('elycharts_env'); $.elycharts.mousemanager.onMouseOutArea(env, false, mouseOverIndex, env.mouseAreas[mouseOverIndex]); } mouseOverIndex = index; $StatDialog_Tooltip.html(chartData.dataLabels[index] + ': ' + Util.formatSizeMB(chartData.serieDataMB[index], chartData.serieDataLo[index]) + ''); } function chartMouseExit(env, serie, index, mouseAreaData) { mouseOverIndex = -1; var title = curRange === 'MIN' ? '60 seconds' : curRange === 'HOUR' ? '60 minutes' : curRange === 'DAY' ? '24 hours' : curRange === 'MONTH' ? $('#StatDialog_Volume_MONTH').text() : 'Sum'; $StatDialog_Tooltip.html(title + ': ' + Util.formatSizeMB(chartData.sumMB, chartData.sumLo) + ''); } function simulateMouseEvent() { if (mouseOverIndex > -1) { var env = $('#StatDialog_Chart').data('elycharts_env'); $.elycharts.mousemanager.onMouseOverArea(env, false, mouseOverIndex, env.mouseAreas[mouseOverIndex]); } else { chartMouseExit() } } this.chooseRange = function(range) { curRange = range; updateRangeButtons(); mouseOverIndex = -1; redrawChart(); } function updateRangeButtons() { $('#StatDialog_Toolbar .volume-range').removeClass('btn-inverse'); $('#StatDialog_Volume_' + curRange + ',#StatDialog_Volume_' + curRange + '2,#StatDialog_Volume_' + curRange + '3').addClass('btn-inverse'); } function updateServerList() { var menu = $('#StatDialog_ServerMenu'); var menuItemTemplate = $('.volume-server-template', menu); var insertPos = $('#StatDialog_ServerMenuDivider', menu); $('.volume-server', menu).remove(); for (var i=0; i < Status.status.NewsServers.length; i++) { var server = Status.status.NewsServers[i]; var name = server.ID + '. ' + Status.serverName(server); var item = menuItemTemplate.clone().removeClass('volume-server-template hide').addClass('volume-server'); var a = $('a', item); a.html('' + Util.textToHtml(name)); a.attr('data-id', server.ID); a.click(chooseServer); insertPos.before(item); } $('#StatDialog_ServerCap').text(curServer > 0 ? Status.serverName(Status.status.NewsServers[curServer-1]) : 'All news servers'); $('#StatDialog_ServerMenuAll i').toggleClass('icon-ok', curServer === 0).toggleClass('icon-empty', curServer !== 0); } function chooseServer(server) { curServer = parseInt($(this).attr('data-id')); updateServerList(); redrawChart(); } function dayToDate(epochDay) { var dt = new Date(epochDay * 24*60*60 * 1000); dt = new Date(dt.getTime() + dt.getTimezoneOffset() * 60*1000); return dt; } function dateToDay(date) { var epochDay = Math.ceil((date.getTime() - date.getTimezoneOffset() * 60*1000) / (1000*24*60*60)); return epochDay; } function updateMonthList() { monthListInitialized = true; var firstDay = servervolumes[0].FirstDay; var lastDay = firstDay + servervolumes[0].BytesPerDays.length - 1; var curDay = firstDay + servervolumes[0].DaySlot; var firstDt = dayToDate(firstDay); var lastDt = dayToDate(lastDay); var curDt = dayToDate(curDay); var menu = $('#StatDialog_MonthMenu'); var menuItemTemplate = $('.volume-month-template', menu); var insertPos = $('#StatDialog_MonthMenuYears', menu); $('.volume-month', menu).remove(); // does computer running NZBGet has correct date (after 1-Jan-2013)? clockOK = firstDay > 0 && servervolumes[0].DaySlot > -1; if (!clockOK) { updatePeriod(); return; } // show last three months in the menu firstDt.setDate(1); var monDt = new Date(curDt.getTime()); monDt.setDate(1); for (var i=0; i<3; i++) { if (monDt < firstDt) { break; } var name = monthNames[monDt.getMonth()] + ' ' + monDt.getFullYear(); var monId = '' + monDt.getFullYear() + '-' + monDt.getMonth(); if (curMonth === null) { curMonth = monId; } var item = menuItemTemplate.clone().removeClass('volume-month-template hide').addClass('volume-month'); var a = $('a', item); a.html('' + name); a.attr('data-id', monId); a.click(chooseMonth); insertPos.before(item); monDt.setMonth(monDt.getMonth() - 1); } // show last two years in the menu var insertPos = $('#StatDialog_MonthMenuDivider', menu); firstDt.setMonth(0); monDt = new Date(curDt.getTime()); monDt.setDate(1); monDt.setMonth(0); for (var i=0; i<2; i++) { if (monDt < firstDt) { break; } var name = monDt.getFullYear(); var monId = '' + monDt.getFullYear(); var item = menuItemTemplate.clone().removeClass('volume-month-template hide').addClass('volume-month'); var a = $('a', item); a.html('' + name); a.attr('data-id', monId); a.click(chooseMonth); insertPos.before(item); monDt.setFullYear(monDt.getFullYear() - 1); } updatePeriod(); } function updatePeriod() { if (!clockOK) { monStartDate = new Date(2000, 1); monStartIndex = -1; monEndIndex = -1; return; } var cap; var monStart; var monEnd; monYear = curMonth.indexOf('-') === -1; if (monYear) { cap = curMonth; var year = parseInt(curMonth); monStart = new Date(year, 0); monEnd = new Date(year, 11, 31); } else { var month = parseInt(curMonth.substr(5, 2)); var year = parseInt(curMonth.substring(0, 4)); cap = monthNames[month] + ' ' + year; monStart = new Date(year, month); monEnd = new Date(year, month + 1); monEnd.setDate(0); } $('#StatDialog_Volume_MONTH, #StatDialog_Volume_MONTH2').text(cap); monStartDate = monStart; var firstDay = servervolumes[0].FirstDay; monStart = dateToDay(monStart); monEnd = dateToDay(monEnd); monStartIndex = monStart - firstDay; monEndIndex = monEnd - firstDay; } function updateCounters() { $StatDialog_TodaySize.html(Util.formatSizeMB(Status.status.DaySizeMB, Status.status.DaySizeLo)); $StatDialog_MonthSize.html(Util.formatSizeMB(Status.status.MonthSizeMB, Status.status.MonthSizeLo)); $StatDialog_AllTimeSize.html(Util.formatSizeMB(servervolumes[curServer].TotalSizeMB, servervolumes[curServer].TotalSizeLo)); $StatDialog_CustomSize.html(Util.formatSizeMB(servervolumes[curServer].CustomSizeMB, servervolumes[curServer].CustomSizeLo)); $StatDialog_Custom.attr('title', 'reset on ' + Util.formatDateTime(servervolumes[curServer].CustomTime)); } function chooseMonth() { setMonth($(this).attr('data-id')); } function setMonth(month) { curRange = 'MONTH'; curMonth = month; updateRangeButtons(); updateMonthList(); redrawChart(); } this.chooseOtherMonth = function() { $StatRangeDialog_PeriodInput.val(''); redrawLock++; $StatRangeDialog.modal({backdrop: false}); } function StatRangeDialogHidden() { redrawLock--; StatDialog.redraw(); } this.setPeriod = function() { var period = $StatRangeDialog_PeriodInput.val(); if (period.indexOf('-') === -1) { var year = parseInt(period); if (year < 2013 || year > 2050) { PopupNotification.show('#Notif_StatRangeError'); return; } period = '' + year; } else { var month = parseInt(period.substr(5, 2)); var year = parseInt(period.substring(0, 4)); if (year < 2013 || year > 2050 || month < 1 || month > 12) { PopupNotification.show('#Notif_StatRangeError'); return; } period = year + '-' + (month-1); } $StatRangeDialog.modal('hide'); setMonth(period); } this.resetCounter = function() { $('#StatDialogResetConfirmDialog_Server').text(curServer === 0 ? 'all news servers' : $('#StatDialog_ServerCap').text()); $('#StatDialogResetConfirmDialog_Time').text(Util.formatDateTime(servervolumes[curServer].CustomTime)); ConfirmDialog.showModal('StatDialogResetConfirmDialog', doResetCounter); } function doResetCounter() { RPC.call('resetservervolume', [curServer === 0 ? -1 : curServer, 'CUSTOM'], function() { PopupNotification.show('#Notif_StatReset'); Refresher.update(); }); } }(jQuery)); /*** LIMIT DIALOG *******************************************************/ var LimitDialog = (new function($) { 'use strict' // Controls var $LimitDialog; var $ServerTable; var $LimitDialog_SpeedInput; // State var changed; this.init = function() { $LimitDialog = $('#LimitDialog'); $LimitDialog_SpeedInput = $('#LimitDialog_SpeedInput'); $('#LimitDialog_Save').click(save); $ServerTable = $('#LimitDialog_ServerTable'); $ServerTable.fasttable( { pagerContainer: '#LimitDialog_ServerTable_pager', rowSelect: UISettings.rowSelect, pageSize: 100 }); if (UISettings.setFocus) { $LimitDialog.on('shown', function() { $('#LimitDialog_SpeedInput').focus(); }); } $LimitDialog.on('hidden', function() { // cleanup $ServerTable.fasttable('update', []); }); } this.clicked = function(e) { if (e.metaKey || e.ctrlKey) { toggleLimit(); } else { showModal(); } } function showModal() { var rate = Util.round0(Status.status.DownloadLimit / 1024); $LimitDialog_SpeedInput.val(rate > 0 ? rate : ''); updateTable(); $LimitDialog.modal({backdrop: 'static'}); } function updateTable() { var data = []; for (var i=0; i < Status.status.NewsServers.length; i++) { var server = Status.status.NewsServers[i]; var name = Status.serverName(server); var fields = ['
', server.ID + '. ' + name]; var item = { id: server.ID, fields: fields, }; data.push(item); $ServerTable.fasttable('checkRow', server.ID, server.Active); } $ServerTable.fasttable('update', data); Util.show('#LimitDialog_ServerBlock', data.length > 0); } function save(e) { var val = $LimitDialog_SpeedInput.val(); var rate = 0; if (val == '') { rate = 0; } else { rate = parseInt(val); if (isNaN(rate)) { return; } } var checkedRows = $ServerTable.fasttable('checkedRows'); var servers = []; for (var i=0; i < Status.status.NewsServers.length; i++) { var server = Status.status.NewsServers[i]; var selected = checkedRows[server.ID] !== undefined; if (server.Active != selected) { servers.push([server.ID, selected]); } } saveLimit(rate, servers); } function saveLimit(rate, servers) { function saveServers() { if (servers.length > 0) { changed = true; RPC.call('editserver', servers, function() { completed(); }); } else { completed(); } } changed = false; var oldRate = Util.round0(Status.status.DownloadLimit / 1024); if (rate != oldRate) { changed = true; RPC.call('rate', [rate], function() { saveServers(); }); } else { saveServers(); } } function completed() { $LimitDialog.modal('hide'); if (changed) { PopupNotification.show('#Notif_SetSpeedLimit'); } Refresher.update(); } function toggleLimit() { var limited = Status.status.DownloadLimit > 0; for (var i=0; i < Status.status.NewsServers.length; i++) { var server = Status.status.NewsServers[i]; limited = limited || !server.Active; } var defRate = Options.option('DownloadRate'); var rate = limited ? 0 : parseInt(defRate === '' ? 0 : defRate); var servers = []; for (var i=0; i < Status.status.NewsServers.length; i++) { var server = Status.status.NewsServers[i]; var defActive = Options.option('Server' + (i + 1) + '.Active') === 'yes'; var newActive = limited ? true : defActive; if (server.Active != newActive) { servers.push([server.ID, newActive]); } } saveLimit(rate, servers); } }(jQuery)); /*** FILTER MENU *********************************************************/ var FilterMenu = (new function($) { 'use strict'; var $SaveFilterDialog; var $SaveFilterInput; var $Table_filter; var ignoreClick = false; var $Table_filter; var tabName; var items; this.init = function() { $SaveFilterDialog = $('#SaveFilterDialog'); $SaveFilterInput = $('#SaveFilterInput'); if (UISettings.setFocus) { $SaveFilterDialog.on('shown', function () { $SaveFilterInput.focus(); }); } } this.setTab = function(tabname) { tabName = tabname; $Table_filter = $('#' + tabName + 'Table_filter'); load(); } this.redraw = function() { var menu = $('#FilterMenu'); var menuItemTemplate = $('.filter-menu-template', menu); var insertPos = $('#FilterMenu_Divider', menu); $('.filter-menu', menu).remove(); for (var i = 0; i < items.length; i++) { var name = items[i].name; var item = menuItemTemplate.clone().removeClass('filter-menu-template').removeClass('hide').addClass('filter-menu'); var t = $('span', item); t.text(name); var a = $('a', item); a.click(applyFilter); a.attr('data-id', i); var im = $('button', item); im.click(deleteFilter); im.attr('data-id', i); insertPos.before(item); } Util.show('#FilterMenu_Empty', items.length === 0); if (UISettings.miniTheme) { Frontend.alignPopupMenu('#FilterMenu'); } } function applyFilter() { if (ignoreClick) { ignoreClick = false; return; } var id = parseInt($(this).attr('data-id')); $Table_filter.val(items[id].filter); $('#' + tabName +'Table').fasttable('applyFilter', $Table_filter.val()); } function deleteFilter() { ignoreClick = true; var id = parseInt($(this).attr('data-id')); items.splice(id, 1); save(); } this.saveDialogClick = function() { if ($Table_filter.val() === '') { PopupNotification.show('#Notif_SaveFilterEmpty'); return; } var filter = $Table_filter.val(); var name = filter; // reuse the name if the filter already exists for (var i = 0; i < items.length; i++) { if (items[i].filter === filter) { name = items[i].name; break; } } $SaveFilterInput.val(name); $SaveFilterDialog.modal(); } this.saveClick = function() { $SaveFilterDialog.modal('hide'); var name = $SaveFilterInput.val(); var filter = $Table_filter.val(); // rename if already exists for (var i = 0; i < items.length; i++) { if (items[i].filter === filter) { items[i].name = name; save(); return; } } // doesn't exist - add new items.push({name: name, filter: filter}); save(); } function load() { items = JSON.parse(UISettings.read('Filter_' + tabName, '[]')); } function save() { UISettings.write('Filter_' + tabName, JSON.stringify(items)); } }(jQuery)); nzbget-19.1/webui/style.css0000644000175000017500000013561313130203062015535 0ustar andreasandreas/*! * This file is part of nzbget. See . * * Copyright (C) 2012-2017 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, see . */ body { padding-left: 0; padding-right: 0; } /* NAVBAR */ .navbar-fixed-top { margin-bottom: 18px; margin-left: 0px; margin-right: 0px; position: static; } .navbar-fixed-top .navbar-inner { padding: 0; min-height: 0; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; color: #bbb; } /* fixed navbar (applied dynamically if screen is wide enough) */ body.navfixed { margin-top: 57px; } body.navfixed .navbar-fixed-top { position: fixed; } body.navfixed.scrolled .navbar-fixed-top .navbar-inner { -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); } /* end of fixed navbar */ #Logo { float: left; margin-top: 1px; margin-right: 5px; width: 88px; height: 36px; cursor: pointer; opacity: 1; } #Logo i, #Logo i:hover { opacity: 1; filter: alpha(opacity=100); } .img-logo { background-position: -8px -139px; width: 88px; height: 38px; display: inline-block; } #PlayBlock { width: 70px; height: 38px; display: block; float: left; position: relative; z-index: 2; } #PlayButton, #PauseButton, #PlayPauseBg { width: 50px; height: 50px; position: absolute; padding: 0; z-index: 3; } .img-download-orange { background-position: -176px -80px; } .img-download-green { background-position: -112px -80px; } .img-download-bg { background-position: -240px -80px; width: 50px; height: 50px; } .PlayBlockInner { width: 38px; height: 38px; margin-left: 0px; margin-top: 0px; cursor: pointer; top: 6px; left: 6px; position: absolute; -webkit-border-radius: 18px; -moz-border-radius: 18px; border-radius: 18px; } .img-download-btn { background-position: -16px -80px; width: 21px; height: 21px; margin-left: 9px; margin-top: 8px; } .PlayBlockInner:hover .img-download-btn, .PlayBlockInner:hover .img-download-btn { /* white */ background-position: -16px -112px; opacity: 0.9; } #PlayCaretBlock { left: 37px; top: 1px; position: absolute; } #PlayCaretButton { border: 0; background: none; width: 24px; height: 22px; padding: 3px; line-height: 10px; vertical-align: top; } #PlayCaret { margin-top: 3px; margin-left: 9px; border-top-color: #ffffff; border-bottom-color: #ffffff; opacity: 0.75; filter: alpha(opacity=75); } #PlayCaretButton:hover #PlayCaret { opacity: 1; filter: alpha(opacity=100); } @-webkit-keyframes play-rotate { 0% { -webkit-transform: rotate(0); } 100% { -webkit-transform: rotate(360deg); } } @-moz-keyframes play-rotate { 0% { -moz-transform: rotate(0); } 100% { -moz-transform: rotate(360deg); } } @-ms-keyframes play-rotate { 0% { -ms-transform: rotate(0); } 100% { -ms-transform: rotate(360deg); } } #PlayAnimation { position: absolute; z-index: 4; left: -5px; top: -5px; -webkit-background-size: 70px 70px; -moz-background-size: 70px 70px; background-size: 70px 70px; background-position: center; width: 60px; height: 60px; cursor: pointer; pointer-events: none; -webkit-animation: play-rotate 1s linear infinite; -moz-animation: play-rotate 1s linear infinite; -ms-animation: play-rotate 1s linear infinite; } #PlayAnimation.play { background-image: url("./img/download-anim-green-2x.png"); } #PlayAnimation.pause { background-image: url("./img/download-anim-orange-2x.png"); } #InfoBlock { float: left; margin-right: 10px; padding-top: 2px; cursor: pointer; width: 86px; } #InfoBlock div { margin-top: 1px; font-size: 12px; font-weight: bold; margin: 0; padding: 0; } #InfoBlock div:hover { color: #fff; } #InfoBlock i { margin-right: 1px; } .navbar-inner i { opacity: 0.8; filter: alpha(opacity=80); } #InfoBlock div:hover i { opacity: 1; filter: alpha(opacity=100); } #StatusTime.orange { color: #F08929; } #StatusTime.orange:hover { color: #FFA15A; } .navbar-inner .btn:hover i { opacity: 1; filter: alpha(opacity=100); } #NavLinks { margin-right: 0; } .navbar-container { padding-left: 10px; padding-right: 10px; } .navbar .btn-group { padding: 0; } /* needed for Safari 4 */ .btn-toolbar .btn { height: 28px; } .navbar .nav { margin-right: 0; } .navbar .nav > li > a { color: inherit; } .navbar .nav .active > a, .navbar .nav .active > a:hover { outline: 0; color: #000; text-shadow: none; background: rgb(255,255,255); /* Old browsers */ background: -moz-linear-gradient(top, rgb(255,255,255) 0%, rgb(238,238,238) 45%, rgb(231,231,231) 55%, rgb(255,255,255) 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgb(255,255,255)), color-stop(45%,rgb(238,238,238)), color-stop(55%,rgb(231,231,231)), color-stop(100%,rgb(255,255,255))); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, rgb(255,255,255) 0%,rgb(238,238,238) 45%,rgb(231,231,231) 55%,rgb(255,255,255) 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, rgb(255,255,255) 0%,rgb(238,238,238) 45%,rgb(231,231,231) 55%,rgb(255,255,255) 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, rgb(255,255,255) 0%,rgb(238,238,238) 45%,rgb(231,231,231) 55%,rgb(255,255,255) 100%); /* IE10+ */ background: linear-gradient(to bottom, rgb(255,255,255) 0%,rgb(238,238,238) 45%,rgb(231,231,231) 55%,rgb(255,255,255) 100%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#ffffff',GradientType=0 ); /* IE6-9 */ } #NavLinks .badge { min-width: 14px; display: inline-block; text-align: center; padding: 1px 4px; } #NavLinks .badge.badge2 { min-width: 18px; } #NavLinks .badge.badge3 { min-width: 28px; } #NavLinks .badge-empty { background: none; } /* headers in navbar menu */ .menu-header { display: block; padding: 3px 15px; font-size: 11px; font-weight: bold; line-height: 18px; color: #999999; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-transform: uppercase; } /* for checkboxes in dropdown menu */ .menu-refresh td:first-child , .menu-check td:first-child { width: 18px; } /* for checkboxes in dropdown menu "Refresh" */ .menu-refresh td:nth-child(2) { width: 20px; text-align: right; padding-right: 5px; } /* Search box */ .navbar-search { margin-top: 0px; margin-right: 10px; } .navbar-search .search-query, .navbar-search .search-query:focus, .navbar-search .search-query.focused { width: 130px; padding: 3px 28px 3px 25px; -webkit-border-radius: 12px; -moz-border-radius: 12px; border-radius: 12px; } .navbar-search .search-query:focus, .navbar-search .search-query.focused { margin-top: 1px; } .search-clear { position: absolute; top: 6px; right: 9px; cursor: pointer; opacity: 0.65; filter: alpha(opacity=65); } .search-caret-block { left: -2px; top: 2px; position: absolute; } .search-caret-button { border: 0; background: none; width: 24px; height: 22px; padding: 3px; line-height: 10px; vertical-align: top; } .search-caret-button .caret { margin-top: 3px; margin-left: 9px; border-top-color: #ffffff; border-bottom-color: #ffffff; opacity: 0.75; filter: alpha(opacity=75); } .focused .search-caret-button .caret { margin-left: 5px; border-top-color: #000; border-bottom-color: #000; } .search-caret-button:hover .caret { opacity: 1; filter: alpha(opacity=100); } /* MENUS */ #RefreshMenu { min-width: 160px; } #SettingsMenu { min-width: 190px; } #PlayMenu { min-width: 190px; } #ToolbarOptMenu { min-width: 215px; } #RssMenu { max-width: 300px; overflow: hidden; } .menu-with-button a { padding-right: 70px; overflow: hidden; } .menu-with-button a .btn, .phone .menu-with-button a .btn { visibility: hidden; height: 22px; line-height: 12px; font-size: 8pt; padding: 2px 7px; margin-right: 0px; margin-top: -2px; margin-bottom: -1px; right: 15px; position: absolute; } .menu-with-button a:hover .btn { visibility: visible; } .menu-with-button .menu-no-items { padding-left: 15px; } #FilterMenu { left: -3px; right: auto; margin-top: 3px; min-width: 170px; max-width: 270px; } #FilterMenu::after { left: 13px; right: auto; } #FilterMenu::before { left: 12px; right: auto; } #DownloadsEdit_ActionsMenu, #HistoryEdit_ActionsMenu { min-width: 120px; } ul.dropdown-menu > li > a { line-height: 20px; } ul.dropdown-menu > li > a.has-table { line-height: 18px; } ul.dropdown-menu > li > a > i { margin-right: 5px; } /* BEGIN: Icons */ [class^="icon-"], [class*=" icon-"], [class^="img-"], [class*=" img-"] { background-image: url("./img/icons.png"); } /* HiDPI screens */ @media only screen and (-webkit-min-device-pixel-ratio: 2) { [class^="icon-"], [class*=" icon-"], [class^="img-"], [class*=" img-"] { background-image: url("./img/icons-2x.png"); -webkit-background-size: 700px 300px; -moz-background-size: 700px 300px; background-size: 700px 300px; } } [class^="icon-"], [class*=" icon-"] { display: inline-block; vertical-align: text-top; width: 16px; height: 16px; line-height: 16px; } .icon-empty { background-position: -1000px -1000px; } .icon-plus { background-position: -16px -16px; } .icon-minus { background-position: -368px -112px; } .icon-remove-white { background-position: -48px -16px; } .icon-ok { background-position: -80px -16px; } .icon-time { background-position: -112px -16px; } .icon-file { background-position: -144px -16px; } .icon-messages { background-position: -176px -16px; } .icon-play { background-position: -208px -16px; } .icon-pause { background-position: -240px -16px; } .icon-down { background-position: -272px -16px; } .icon-up { background-position: -304px -16px; } .icon-bottom { background-position: -336px -16px; } .icon-top { background-position: -368px -16px; } .icon-back { background-position: -304px -80px; } .icon-forward { background-position: -336px -80px; } .icon-nextpage { background-position: -368px -80px; } .icon-refresh { background-position: -16px -48px; } .icon-edit { background-position: -48px -48px; } .icon-trash { background-position: -80px -48px; } .icon-settings { background-position: -112px -48px; } .icon-downloads { background-position: -144px -48px; } .icon-plane { background-position: -176px -48px; } .icon-truck { background-position: -208px -48px; } .icon-history { background-position: -240px -48px; } .icon-remove, .icon-close { background-position: -272px -48px; } .icon-merge { background-position: -304px -48px; } .icon-save { background-position: -464px -80px; } .icon-rss { background-position: -304px -112px; } .icon-trash-white { background-position: -336px -48px; } .icon-downloads-white { background-position: -368px -48px; } .icon-history-white { background-position: -400px -48px; } .icon-settings-white { background-position: -432px -48px; } .icon-messages-white { background-position: -464px -48px; } .icon-time-orange { background-position: -400px -80px; } .icon-split { background-position: -432px -80px; } .img-checkmark { background-position: -432px -16px; } .img-checkminus { background-position: -400px -16px; } .icon-postcard { background-position: -432px -112px; } .icon-link { background-position: -400px -112px; } .icon-alert { background-position: -336px -112px; } .icon-process { background-position: -304px -144px; } .icon-process-auto { background-position: -336px -144px; } .icon-duplicates { background-position: -368px -144px; } .icon-mask { background-position: -400px -144px; } .icon-mask-white { background-position: -432px -144px; } .icon-ring-red { background-position: -528px -16px; } .icon-ring-fill-red { background-position: -560px -16px; } .icon-circle-red { background-position: -496px -16px; } .icon-ring-blue { background-position: -528px -48px; } .icon-ring-fill-blue { background-position: -560px -48px; } .icon-ring-ltgrey { background-position: -496px -48px; } /* END: Icons */ .btn-toolbar { margin-top: 6px; margin-bottom: 0px; } .section-toolbar, .modal-toolbar { margin-top: 0; margin-bottom: 7px; } .section-title { margin-right: 10px; } .label-status { text-transform: uppercase; } .label-inline { display: inline-block; margin-bottom: -4px; overflow-x: hidden; text-overflow: ellipsis; } .controls .label-status { line-height: 22px; } .invisible { visibility: hidden; } /* links in black color */ .table a { color: #000000; } /* links in black color */ .table a:hover { color: #000000; } .table a.badge-link:hover { text-decoration: none; } table.datatable > tbody > tr > td { word-wrap: break-word; } #MainTabContent { margin-top: 0; overflow: visible; /* fix problem with dropdown menus */ } /* top toolbox (length-combo and pager) for tables */ .toolbox-top { margin-bottom: 8px; } div.btn-group + div.toolbox-length { margin-left: 10px; } /* combobox with page length for tables */ div.toolbox-length select { width: 75px; height: 28px; } .toolbox-info { margin-top: 10px; } .pagination { height: 32px; margin: 0; } .pagination a { padding: 0 10px; line-height: 26px; } .modal-tab .pagination { margin-bottom: 10px; } .padded-tab { padding-left: 20px; padding-right: 20px; } h1 { font-size: 24px; line-height: 36px; margin-bottom: 8px; margin-right: 20px; } h2 { font-size: 20px; line-height: 26px; margin-bottom: 8px; margin-right: 20px; } .alert-heading { margin-bottom: 10px; } /* remove focus border */ .nav-tabs > .active > a, .nav-tabs > .active > a:hover, .nav-pills > .active > a, .nav-pills > .active > a:hover, .nav-list > .active > a, .nav-list > .active > a:hover, .btn, .btn-group .btn, .pagination a, .btn-toolbar .btn , .control-group .btn, .modal-footer .btn, .form-search .btn, .btn:focus { outline: 0; } form { margin-bottom: 0px; } #DownloadsEdit_PostParamData, #HistoryEdit_PostParamData { padding-bottom: 1px; } #DownloadsEdit_FileTable_filter, #DownloadsEdit_LogTable_filter #HistoryEdit_LogTable_filter, #FeedDialog_ItemTable_filter { width: 180px; } #DownloadsLogRecordsPerPageBlock, #HistoryLogRecordsPerPageBlock { margin-bottom: 12px; } #DownloadsEdit_LogTable_filterBlock, #HistoryEdit_LogTable_filterBlock { float: left; margin-left: 20px; margin-right: 20px; margin-bottom: 12px; } .phone .modal-toolbox { margin-bottom: 20px; } .loading-block { position: absolute; left: 0; top: 0; right: 0; bottom: 0; text-align: center; } .loading-block img { position: absolute; top: 50%; left: 50%; } .modal-body { max-height: 360px; } .modal.no-footer .modal-body { max-height: 420px; } .modal-footer .btn-primary { min-width: 40px; } .modal-body .alert { margin-bottom: 0px; padding: 6px; } .modal-max { margin: 15px; left: 0; top: 0; bottom: 0; right: 0; width: auto; height: auto; } .modal-max .modal-body { position: absolute; left: 0; right: 0; max-height: inherit; /* top: 46px; // must be calculated at runtime */ /* bottom: 58px; // must be calculated at runtime */ } .modal-max .modal-inner-scroll { top: 54px; } .modal-max .modal-footer { position: absolute; left: 0; right: 0; bottom: 0; } .modal-inner-scroll { bottom: 0; left: 15px; overflow-y: auto; position: absolute; right: 0; padding-right: 15px; } .modal-inner-scroll .toolbox-info { margin-bottom: 10px; } .modal-2 { margin-top: -200px; z-index: 1060; } .modal-backdrop.modal-2 { z-index: 1055; opacity: 0.6; } .badge-active { background-color: #FFFFFF; color: #000000; text-shadow: none; } /* BEGIN: Tables */ .table { margin-bottom: 0px; } .table-bordered { border-left: 1px solid #dddddd; } .table-bordered th, .table-bordered td { border-left: none; } .text-right { text-align: right; } .text-center, table td.text-center, table th.text-center { text-align: center; } .table-striped tbody tr:nth-child(odd) { background-color: #f9f9f9; } .table tbody tr:hover { background-color: #f5f5f5; } .table th, .table td { padding: 5px; } .table-condensed th, .table-condensed td { padding: 2px; } .table-nonbordered, .table-nonbordered td { border: none; } table > thead > tr > th.table-selector { text-align: center; background: repeating-linear-gradient(-45deg, #FFFFD8, #FFFFD8 6px, #E7E8D1 6px, #E7E8D1 12px); padding: 2px; font-size: 12px; line-height: 14px; color: #966C38; text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.75), 0 1px 0 rgba(255, 255, 255, 0.75), -1px 0 0 rgba(255, 255, 255, 0.75); } /* END: Tables */ /* BEGIN: Checkmarks in the table */ table.table-check > thead > tr > th:first-child, table.table-check > tbody > tr > td:first-child { width: 14px; height: 14px; padding-left: 6px; } div.check { width: 12px; height: 12px; border: 1px solid #DDDDDD; margin-top: 2px; margin-bottom: 3px; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; } div.check:hover { border: 1px solid #0088cc; } th > div.check { margin-bottom: 2px; } table.table-cancheck tr div.img-check { background-position: 10px 10px; } table.table-cancheck tr.checked div.img-check { background-position: -434px -18px; } table.table-cancheck tr.checkremove div.img-check { background-position: -402px -18px; } tr.checked, tr.checked td, tr.checked:nth-child(odd) .progress { background-color: #FFFFE8; } .table-striped tbody tr.checked:nth-child(odd) td, .checked .progress { background-color: #FFFFD8; } .table tbody tr.checked:hover, .table tbody tr.checked:hover td { background-color: #FFFFC0; } .check-simple tr.checked, .check-simple tr.checked td, .table-striped.check-simple tbody tr.checked:nth-child(odd) td { background-color: inherit; } .table.check-simple tbody tr.checked:hover, .table.check-simple tbody tr.checked:hover td { background-color: #f5f5f5; } table.table-hidecheck thead > tr > th:first-child, table.table-hidecheck tbody > tr > td:first-child { display: none; } .checked .progress { background-color: #FFFFE1; } /* END: Checkmarks in the table */ /* BEGIN: Progress bars */ .progress-block { position: relative; width: 120px; } .progress { margin-bottom: 0px; background: #f0f0f0; } /* style for queued downloads, based on ".progress-success.progress-striped .bar" from bootstrap.css */ .progress-none.progress-striped .bar { background-color: #c0c0c0; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#d0d0d0', endColorstr='#c0c0c0', GradientType=0); } /* text on left side of progress bar */ .bar-text-left { position: absolute; top: 0; left: 5px; text-align: left; } /* text on right side of progress bar */ .bar-text-right { position: absolute; top: 0; right: 5px; text-align: right; } /* text on left side of progress bar */ .bar-text-center { position: absolute; top: 0; left: 5px; width: 100%; text-align: center; } /* END: Progress bars */ /* DROP-DOWN CONTEXT MENUS */ td.dropdown-cell { padding: 0; } th.dropafter-cell, td.dropafter-cell { padding-left: 0; } td.dropdown-cell > div { padding: 5px 12px 6px 4px; margin: 0px 0; display: inline-block; position: relative; } td.dropdown-cell > div:hover { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } td.dropdown-cell > div:not(.dropdown-disabled):hover { cursor: pointer; } td.dropdown-cell > div:not(.dropdown-disabled):hover::after { border-top: 4px solid #888; border-right: 4px solid transparent; border-left: 4px solid transparent; content: ""; position: absolute; margin-bottom: 8px; margin-left: 2px; } .dropdown-menu { z-index: 3000; } .dropdown-menu li:not(:hover) .empty-item { color: #bbb; } .dropdown-warning { text-align: center; background: repeating-linear-gradient(-45deg, #FFFFD8, #FFFFD8 6px, #E7E8D1 6px, #E7E8D1 12px); padding: 2px; font-size: 12px; line-height: 14px; color: #966C38; text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.75), 0 1px 0 rgba(255, 255, 255, 0.75), -1px 0 0 rgba(255, 255, 255, 0.75); border-top: solid 1px #ccc; border-bottom: solid 1px #ccc; margin-bottom: 1px; } table th.priority-cell { padding-left: 8px; } table td.priority-cell { width: 16px; } td.dropdown-cell.priority-cell > div { padding-right: 7px; } td.dropdown-cell.priority-cell > div:not(.dropdown-disabled):hover::after { margin-left: -1px; } #DownloadsTable td:first-child { padding-right: 6px; } td span.none-category { color: transparent; } td:hover span.none-category { color: #aaa; } /* controls */ input[readonly], select[readonly], textarea[readonly], .uneditable-input { cursor: inherit; } .input-prepend .add-on-small, .input-append .add-on-small { font-size: 11px; } .toolbtn { min-width: 56px; } /* BEGIN: override bootstrap styles for modals */ .modal-header h3 { text-align: center; } .modal-header .close { margin-top: 6px; margin-left: 10px; opacity: 0.6; filter: alpha(opacity=60); outline: 0; } .modal-header .close:hover { opacity: 0.9; filter: alpha(opacity=90); } .modal-header .back { float: left; margin-top: 6px; margin-right: 10px; font-size: 20px; font-weight: bold; line-height: 18px; opacity: 0.6; filter: alpha(opacity=60); outline: 0; } .modal-header .back:hover { cursor: pointer; opacity: 0.9; filter: alpha(opacity=90); } .modal-header .back-hidden:hover { cursor: inherit; } .form-horizontal .control-group { margin-bottom: 12px; } .modal .form-horizontal .control-group:last-child { margin-bottom: 0; } .modal .form-horizontal .retain-margin .control-group:last-child { margin-bottom: 15px; } .form-horizontal .control-group-last { margin-bottom: 0; } .form-horizontal .help-block { margin-top: 4px; margin-bottom: -2px; line-height: 18px; } .form-horizontal .help-block-uneditable { margin-top: 3px; } .modal .input-medium { width: 200px; } .modal .input-xlarge { width: 350px; } .modal .input-xblarge { width: 335px; } .modal.modal-padded .input-xxlarge { width: 470px; } /* END: override bootstrap styles for modals */ .modal-bottom-toolbar .btn { margin-top: 12px; } /* based on uneditable-input */ .uneditable-mulitline-input { display: inline-block; width: 340px; overflow: hidden; cursor: not-allowed; background-color: #ffffff; border-color: #eee; border: 1px solid #eee; padding: 4px; padding-right: 20px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); } .input-medium.uneditable-input, .uneditable-mulitline-input { margin-bottom: -4px; } .modal-mini { width: 420px; margin-left:-210px; } .modal-small { width: 480px; margin-left:-240px; } .modal-large { width: 700px; margin-left:-350px; } .modal-padded-small .modal-body { padding-left: 25px; padding-right: 25px; } .modal-padded .modal-body { padding-left: 40px; padding-right: 40px; } .modal-tab-padded { padding-left: 25px; padding-right: 25px; } .modal-tab-padded-small { padding-left: 10px; padding-right: 10px; } .dragover, .dragover .table-striped tbody tr:nth-child(odd) td, .dragover .table-striped tbody tr:nth-child(odd) th { background-color: #dff0d8; } ul.help > li { margin-bottom: 10px; } /* Make "select files" native control invisible */ .hidden-file-input { position: absolute; left: 0; top: 0; width: 0; height: 0; opacity: 0; filter: alpha(opacity=0); } /* BEGIN: PopupNotification alerts */ .alert-inverse { color: #ffffff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #414141; border-color: #222222; } .alert-center { position: fixed; padding: 20px; top: 50%; left: 50%; z-index: 2000; overflow: auto; text-align: center; opacity: 0.9; filter: alpha(opacity=90); } .alert-center-small { width: 200px; margin: -80px 0 0 -100px; } .alert-center-medium { width: 400px; margin: -80px 0 0 -200px; } .alert-error.alert-center { border-color: #B94A48; } .alert-success.alert-center { border-color: #468847; } .alert-info.alert-center { border-color: #3a87ad; } /* END: PopupNotification alerts */ .confirm-help-block { color: #555555; font-size: 13px; line-height: 16px; margin-bottom: 0; } .table .btn-success, .table .btn-success:hover { color: #ffffff; } .btn-group { margin-right: 9px; } .btn-toolbar .btn-group { margin-right: 4px; } .btn-group + .btn-group { margin-left: 0; } .input-prepend .add-on:first-child { margin-left: 0; } /* important for group of buttons with different colors like toggle switch */ .btn-group > .btn:hover, .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active { z-index: inherit; } #ErrorAlert, #FirstUpdateInfo, #ConfigReloadInfo, .unsupported-browser { margin-left: 20px; margin-right: 20px; } #ErrorAlert, #FirstUpdateInfo, #ConfigReloadInfo, .unsupported-browser { margin-top: 20px; } .confirm-menu { text-align: left; min-width: 10px; right: 0; left: auto; } .data-statistics { width: 300px; } .data-statistics-full { width: 364px; } .modal-center { margin-top: 0; } /*** STATISTICS DIALOG */ #StatisticsTab table { width: 350px; } #StatDialog_VolumesBlock { text-align: center; } #StatDialog_ChartBlock { height: 300px; width: 100%; margin-top: 15px; margin-bottom: 15px; } #StatDialog_Chart { height: 100%; *width: 670px; width: 100%; } #StatDialog_Tooltip { float: right; margin-top: -3px; margin-bottom: -6px; padding-right: 15px; font-style: italic; } .stat-size { font-weight: bold; } #StatDialog_TooltipSum { font-weight: bold; } #StatDialog_CountersBlock { padding-right: 20px; } #StatDialog_Counters { margin-top: 8px; text-align: center; } #StatDialog_Counters .span3 { min-height: 0; } #StatDialog hr { margin: 5px 10px; } #StatDialog_Custom a, #StatDialog_Custom a:hover, #AddDialog_Files a, #AddDialog_Files a:hover { color: #000000; cursor: pointer; } /*** CONFIG PAGE */ #ConfigNav.nav-list a { color: #000; text-decoration: none; padding-top: 5px; padding-bottom: 5px; font-size: 12px; } #ConfigNav.nav-list.long-list a { padding-top: 3px; padding-bottom: 3px; } #ConfigNav.nav-list > .active > a, #ConfigNav.nav-list > .active > a:hover { color: #ffffff; *background-color: #505050; } #ConfigNav.nav .nav-header { font-size: 12px; } #ConfigNav { -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; border: 1px solid #eeeeee; padding: 8px 15px; background-color: #F7F7F9; margin-bottom: 15px; } #ConfigContent .config-header { padding: 7px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; margin-bottom: 20px; padding-right: 0; padding-top: 0; border-bottom: 1px solid #eeeeee; } #ConfigTitle { margin-top: 15px; margin-right: 15px; font-size: 16px; font-weight: bold; } .config-header .btn-group { margin-right: 0; } .config-header .btn { margin-top: 7px; margin-right: 0; background-color: #ffffff; background-image: none; border: none; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } span.help-option-title { color: #8D1212; } .failure-message { color: #8D1212; } #ConfigContent p.help-block { margin-top: 6px; line-height: 16px; } #ConfigContent.hide-help-block p.help-block { display: none; } #ConfigContent .control-label { font-weight: bold; } #ConfigContent select { width: inherit; } #ConfigContent .editnumeric { width: 70px; } #ConfigContent .editlarge { width: 95%; } #ConfigContent .editsmall { width: 150px; } #ConfigContent table.editor { width: 97%; } #ConfigContent table.editor td:first-child { width: 100%; padding-right:15px; } #ConfigContent table.editor input { width: 100%; } .ConfigFooter hr { margin: 6px 0 15px; } div.ConfigFooter { padding-bottom: 15px; } #ConfigContent hr { margin: 15px 0; } .config-settitle { font-size: 14px; font-weight: bold; background-color: #505050; color: #ffffff; padding: 7px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; margin-bottom: 20px; border-bottom: 1px solid #eeeeee; } #ConfigContent.hide-help-block .config-settitle { margin-bottom: 15px; } .config-multicaption { color: #c0c0c0; font-weight: normal; } #DownloadsEdit_ParamTab div.control-group.wants-divider, #HistoryEdit_ParamTab div.control-group.wants-divider, #ConfigContent div.control-group, #ConfigContent.search div.control-group.multiset { border-bottom: 1px solid #eeeeee; margin-bottom: 15px; padding-bottom: 12px; } #ConfigContent.hide-help-block div.control-group, #ConfigContent.hide-help-block div.control-group.multiset { border-bottom: none; margin-bottom: 0px; padding-bottom: 12px; } div.control-group.last-group { margin-bottom: 0; } #ConfigContent div.control-group.last-group, #ConfigContent.search div.control-group.last-group.multiset { border-bottom: none; } #ConfigContent div.control-group.multiset { border-bottom: none; margin-bottom: 12px; padding-bottom: 8px; } #ConfigContent .control-label { width: 170px; } #ConfigContent .form-horizontal .controls { margin-left: 180px; } .btn-switch input:focus { border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } .btn-switch .btn { text-transform: capitalize; } .option { font-weight: bold; font-style:italic; color: inherit; } .option-name, .option-name:focus, .option-name:hover { color: inherit; outline: 0; cursor: inherit; text-decoration: none; } .search .option-name, .search .option-name:focus { cursor: pointer; } .search .option-name:hover { cursor: pointer; text-decoration: underline; color: #005580; } div.multiset-toolbar button { margin-right: 15px; } #ScriptListDialog_ScriptTable td:nth-child(2) { padding-right: 100px; } #ScriptListDialog_ScriptTable .btn-row-order-block { float: right; width: 100px; margin-right: -115px; display: block; } #ScriptListDialog_ScriptTable .btn-row-order { float: none; width: 20px; display: none; } #ScriptListDialog_ScriptTable tr:hover .btn-row-order { display: inline-block; cursor: pointer; } #ScriptListDialog_ScriptTable tbody > tr:first-child div.btn-row-order:first-child, #ScriptListDialog_ScriptTable tbody > tr:last-child div.btn-row-order:last-child, #ScriptListDialog_ScriptTable tbody > tr:first-child div.btn-row-order:nth-child(2), #ScriptListDialog_ScriptTable tbody > tr:last-child div.btn-row-order:nth-child(3) { opacity: 0.4; } /* UPDATE DIALOG */ .table .update-release-notes { color: #005580; font-size: 11px; height: 10px; outline: none; } #UpdateDialog_InstallStable, #UpdateDialog_InstallTesting, #UpdateDialog_InstallDevel { margin-top: 5px; } .table .update-row-name { font-weight: bold; padding-top: 14px; } #UpdateDialog_Versions #UpdateDialog_AvailRow td:hover { background-color: #f5f5f5; } #UpdateDialog_Versions #UpdateDialog_AvailRow td:first-child:hover, #UpdateDialog_Versions tr:hover td { background-color: #ffffff; } .log-dialog { width: 640px; margin-left: -320px; } .log-dialog .modal-body { min-height: 280px; position: relative; } .log-dialog .modal-body pre { min-height: 270px; background-color: #222222; color: #cccccc; padding: 3px 6px; margin-bottom: 0px; position: absolute; top: 15px; bottom: 15px; left: 15px; right: 15px; overflow-y: auto; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } .update-log-error, .script-log-error { color: #dd0000; } .script-log-success { color: #00dd00; } /* FEED FILTER DIALOG */ #FeedFilterDialog_FilterBlock { position: absolute; left: 15px; width: 300px; bottom: 0; height: auto; padding-top: 0; margin-bottom: 12px; padding: 0; font-size: 12px; line-height: 18px; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.15); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } #FeedFilterDialog_FilterHeader { font-size: 13px; font-weight: bold; margin-top: 5px; padding-left: 6px; height: 23px; border-bottom: 1px solid #ccc; border-bottom: 1px solid rgba(0, 0, 0, 0.15); } #FeedFilterDialog_FilterLines { position: absolute; left: 0; top: 29px; bottom: 0px; width: 32px; height: auto; overflow: hidden; background-color: #ffffff; border-right: 1px solid #ccc; border-right: 1px solid rgba(0, 0, 0, 0.15); } #FeedFilterDialog_FilterNumbers { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } #FeedFilterDialog_FilterNumbers .lineno { color: #3A87AD; padding-right: 5px; padding-top: 0; text-align: right; font-weight: bold; white-space: nowrap; } #FeedFilterDialog_FilterClient { position: absolute; left: 33px; right: 0px; top: 29px; bottom: 0px; width: auto; padding-left: 3px; height: auto; background-color: #ffffff; } #FeedFilterDialog_FilterInput { width: 100%; height: 100%; margin: 0; padding: 0; border: 0; resize: none; outline: none; border: none; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } #FeedFilterDialog_PreviewBlock { position: absolute; left: 325px; right: 0; bottom: 0; height: auto; width: auto; padding-right: 15px; margin-bottom: 12px; } #FeedFilterDialog_Splitter { position: absolute; left: 319px; width: 5px; padding: 0; right: 5px; bottom: 0; height: auto; cursor: col-resize; } .phone #FeedFilterDialog_FilterBlock, .phone #FeedFilterDialog_PreviewBlock, .phone #FeedFilterDialog_FilterClient { position: static; top: inherit; left: 0; width: inherit; padding-right: 0; } .phone #FeedFilterDialog_FilterInput { height: 380px; } .filter-rule { cursor: pointer; } /* DRAG-N-DROP */ #TableDragBox { position: absolute; left: 100px; top: 100px; text-align: center; border: 1px solid #ccc; background-color: #f8f8f8; display: none; cursor: grabbing; cursor: -webkit-grabbing; z-index: 5000; } #TableDragBox .badge { position: absolute; left: 0px; top: -12px; text-align: center; padding: 1px 6px; color: #fff; background-color: #D70015; } .phone #TableDragBox .badge { font-size: 22px; top: -20px; padding: 6px 10px; border-radius: 12px; } #TableDragBox .table-bordered { border: none; } /* drag grip */ table.table-drag > tbody > tr:hover > td:first-child, #TableDragBox table.table-drag > tbody > tr > td:first-child { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAECAYAAACk7+45AAAAFUlEQVQI12O8e/fufwYGBgYmBpwAAHhsA5rVjhOVAAAAAElFTkSuQmCC); background-position: 1px 1px; background-repeat: repeat-y; } .phone table.table-drag > tbody > tr:hover > td:first-child, .phone #TableDragBox table.table-drag > tbody > tr > td:first-child { background: none; cursor: default; } table.table-drag > tbody > tr > td:first-child { cursor: move; cursor: -moz-grab; cursor: -webkit-grab; } table.table-drag > tbody > tr > td:first-child > div.check { cursor: default; } #TableDragBox table.table-drag > tbody > tr > td:first-child, #TableDragBox table.table-drag > tbody > tr > td:first-child > div.check { cursor: move; cursor: -moz-grabbing; cursor: -webkit-grabbing; } tr.drag-source > td { background-color: #fff !important; color: #fff !important; visibility: visible; } tr.drag-source > td > * { visibility: hidden !important; } .table-striped tbody tr.drag-finish td, .table-striped tbody tr.drag-finish .progress, .table-striped tbody tr.drag-finish td a { background-color: #E6FAE4 !important; } /****************************************************************************/ /* SMARTPHONE THEME */ body.phone { margin-top: 0; } .phone .navbar-fixed-top { position: static; } .phone #PlayBlock { width: 75px; } .phone #PlayCaretButton { width: 25px; margin-left: 10px; } .phone #InfoBlock { width: 190px; height: 42px; margin-right: 0; } .phone #InfoBlock div { display: inline-block; margin-left: 5px; margin-top: 5px; font-size: 14px; line-height: 32px; height: inherit; } /* GENERAL CLASSES */ .phone-only, .btn-group.phone-only { display: none; } .phone .phone-hide, .phone .btn-group.phone-hide { display: none; } .phone .phone-only { display: block; } .phone .phone-only.inline { display: inline-block; } /* FONTS */ body.phone, .phone p, .phone .form-horizontal .help-block , .phone h4 { font-size: 18px; line-height: 22px; } .phone table td { line-height: 22px; } .phone select, .phone input, .phone textarea, .phone label, .phone button, .phone .btn, .phone .btn-toolbar .btn, .phone .uneditable-input { font-size: 18px; line-height: 24px; height: inherit; } .phone .controls .label-status { line-height: 28px; } .phone .menu-header { font-size: 18px; line-height: 24px; } /* SECTION MARGINGS */ .phone .section-toolbar, .phone .toolbox-top, .phone .toolbox-info, .phone #ConfigTabData { padding-left: 5px; padding-right: 5px; } .phone #ErrorAlert, .phone #FirstUpdateInfo, .phone #ConfigReloadInfo, .phone #DownloadQueueEmpty { margin-left: 5px; margin-right: 5px; } .phone #FirstUpdateInfo, .phone #ConfigReloadInfo { margin-top: 5px; } .phone #MainContent { padding-left: 0px; padding-right: 0px; } .phone .section-toolbar{ margin-top: 8px; margin-bottom: 0; } .phone .toolbox-top { margin-top: 0; margin-bottom: 8px; } /* NAVBAR */ .phone .navbar-fixed-top { margin-bottom: 8px; } .phone .navbar-container { padding-left: 5px; padding-right: 5px; } .phone .navbar-inner .btn-toolbar { margin: 6px 0 0; } .phone ul.nav > li { text-align: center; min-width: 52px; } .phone .menu-header { text-align: left; } .phone .navbar .nav > li > a { padding: 4px 4px 6px; } .phone .navbar .nav > li.active > #DownloadsTabLink > i { /* icon-downloads (black) */ background-position: -144px -48px; } .phone .navbar .nav > li.active > #HistoryTabLink > i { /* icon-history (black) */ background-position: -240px -48px; } .phone .navbar .nav > li.active > #MessagesTabLink > i { /* icon-messages (black) */ background-position: -176px -16px; } .phone .navbar .nav > li.active > #ConfigTabLink > i { /* icon-settings (black) */ background-position: -112px -48px; } .phone .navbar .btn-toolbar .btn { padding: 3px; min-width: 40px; } .phone #RefreshBlockPhone { padding-left: 5px; } .phone .navbar-search .search-query, .phone .navbar-search .search-query:focus, .phone .navbar-search .search-query.focused { width: 140px; padding: 4px 28px 4px 28px; font-size: 16px; -webkit-border-radius: 16px; -moz-border-radius: 16px; border-radius: 16px; margin-bottom: 5px; margin-top: 1px; border: 0; } .phone .search-clear { top: 9px; } /* DATATABLE */ .phone table.datatable , .phone table.datatable > tbody, .phone table.datatable > tbody > tr, .phone table.datatable > tbody > tr > td { display: block; } .phone table.datatable > thead { display: none; } .phone table.datatable > tbody > tr > td { width: inherit; height: inherit; } .phone .datatable td { border: 0; } .phone table.datatable > tbody > tr > td:first-child { border-top: 1px solid #DDDDDD; } .phone table.datatable > tbody > tr:last-child > td:last-child { border-bottom: 1px solid #DDDDDD; } .phone table.datatable > tbody > tr > td:first-child { padding-top: 10px; } .phone table.datatable > tbody > tr > td:last-child { padding-bottom: 10px; } .phone table.table-check > tbody > tr > td { padding-left: 60px; } /* CHECKMARKS IN DATATABLE */ .phone div.check { margin-top: -2px; margin-left: -48px; width: 30px; height: 30px; display: block; position: absolute; border-width: 2px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } .phone table.table-cancheck tr.checked div.img-check { /* icon-OK */ background-position: -74px -10px; } /* SPECIAL TABLE STYLES */ .phone .row-title { font-weight: bold; } .phone .progress-block { margin-top: -2px; } .phone .downloads-progresslabel { margin-top: -8px; margin-bottom: 10px; } .phone .label-inline { display: inline; } /* CONTROLS AROUND DATATABLE */ .phone .records-label { display: none; } /* PAGER */ .phone .toolbox-info div { float: none; width: 100%; text-align: center; display: block; margin-top: 5px; } .phone div.toolbox-length { margin-top: 10px; } .phone .modal-toolbox div.toolbox-length { margin-top: 0; } .phone .pagination { height: auto; width: 100%; text-align: center; margin-top: 10px; } .phone .pagination a { padding: 0 10px; line-height: 34px; } /* STATUS LABELS */ .phone .label { font-size: 14px; line-height: 18px; vertical-align: middle; } .phone .datatable .label { line-height: 21px; } /* PROGRESS */ .phone .progress-block { font-size: 16px; width: 100%; } .phone .progress, .phone .progress .bar { height: 24px; } .phone .bar-text-left, .phone .bar-text-center, .phone .bar-text-right { padding-top: 1px; margin-top: 0px; } /* STATISTICS TABLE */ .phone #StatisticsTab table { width: 100%; } .phone #StatisticsTab td:first-child { font-weight: bold; } .phone #StatisticsTab td { text-align: left; } /* MENUS */ .phone .dropdown-menu a { padding: 7px 15px; } .phone .dropdown-toggle { position: static; } .phone #FilterMenu { left: inherit; right: inherit; margin-top: inherit; width: inherit; } /* hide arrow */ .phone .navbar .dropdown-menu:before, .phone .navbar .dropdown-menu:after { display: none; } .phone #RefreshMenu { min-width: 200px; } .phone #SettingsMenu { min-width: 230px; } .phone #PlayMenu { min-width: 250px; } .phone #ToolbarOptMenu { min-width: 270px; } .phone .dropdown-context:hover { cursor: inherit; } .phone .dropdown-context:hover::after { display: none; } /* TOOLBAR AND INPUTS */ .phone .btn-toolbar .btn, .phone .btn-toolbar input { padding: 6px; min-width: 45px; } .phone .btn-toolbar .btn-group { margin-right: 0; } .phone .btn-toolbar .input-prepend .add-on, .phone .btn-toolbar .input-append .add-on { padding: 6px; min-width: 45px; } .phone .btn { min-width: 40px; } .phone input, .phone textarea, .phone .uneditable-input { height: 24px; } .phone input.btn { height: inherit; } .phone .input-prepend .add-on, .phone .input-append .add-on { height: 24px; line-height: 22px; min-width: 16px; } .phone select { height: auto; } .phone [class^="icon-"] { line-height: 24px; vertical-align: baseline; } .phone .caret { line-height: 24px; margin-top: -2px; vertical-align: middle; } /*** MODALS */ .phone .modal-footer > .btn, .phone .modal-footer > .btn-group { display: block; float: none; width: 100%; margin: 10px auto; } .phone .modal-footer .btn { padding: 7px 0; } .phone .modal-footer > .btn-group > .btn { width: 100%; margin: 0; } .phone .modal-footer { padding-top: 5px; padding-bottom: 5px; } .phone .modal-footer .confirm-menu { text-align: center; right: inherit; left: inherit; float: none; width: 100%; } .phone .modal-footer .confirm-menu .menu-header { text-align: center; } .phone .modal-padded .modal-body, .phone .modal-padded-small .modal-body { padding-left: 15px; padding-right: 15px; } .phone .modal-tab-padded, .phone .modal-tab-padded-small { padding-left: 0; padding-right: 0; } .phone .modal-max .modal-body { position: static; } .phone .modal-max .modal-footer { position: static; } .phone .modal-max .modal-inner-scroll { position: static; top: inherit; left: 0; padding-right: 0; } .phone .data-statistics, .phone .data-statistics-wide, .phone .data-statistics-full { width: 100%; } .phone .btn-caption { display: none; } .phone div.toolbox-length select { height: 36px; } .phone .navbar .btn-toolbar { position: relative; } .phone #ConfigNav.nav-list a { font-size: 18px; } .phone #ConfigNav.nav .nav-header { font-size: 20px; } .phone #ConfigContent .config-header { font-size: 20px; } .phone .config-settitle { font-size: 18px; } .phone #TableDragTip { font-size: 18px; } /*** STATISTICS DIALOG */ .phone #StatDialog_Tooltip { margin-top: 3px; margin-bottom: 3px; } .phone #StatDialog hr { margin-top: 30px; } /* MEDIA SMALL SCREENS */ @media (max-width: 700px) { #ConfigContent [class*="span"] { display: block; float: none; width: auto; margin-left: 0; } .modal-large { width: 600px; margin-left:-300px; } } @media (max-width: 568px) { input[type="checkbox"], input[type="radio"] { border: 1px solid #ccc; } [class*="span"], .row-fluid [class*="span"] { display: block; float: none; width: auto; margin-left: 0; } .form-horizontal .control-group > label { float: none; width: auto; padding-top: 0; text-align: left; } .form-horizontal .controls, #ConfigContent .form-horizontal .controls { margin-left: 0; } .form-horizontal .control-list { padding-top: 0; } .form-horizontal .form-actions { padding-right: 10px; padding-left: 10px; } .modal { position: absolute; top: 0px; right: 0px; left: 0px; width: auto; margin: 0; } .modal.fade.in { top: auto; } .modal .input-xlarge , .modal .input-xxlarge, .modal.modal-padded .input-xxlarge, .uneditable-mulitline-input { width: 95%; } .modal-body, .modal.no-footer .modal-body { max-height: none; } .modal-center { right: 20px; left: 20px; } .alert-center-small, .alert-center-medium { right: 20px; left: 20px; width: auto; margin: -10% 0 0; } } @media (max-width: 479px) { #SearchBlock { display: none; } } @media (max-width: 549px) { #Logo { display: none; } } @media (max-width: 480px) { .dialog-transmit { display: none; } } /* END: MEDIA SMALL SCREENS */ nzbget-19.1/webui/downloads.js0000644000175000017500000010302713130203062016205 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2017 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, see . */ /* * In this module: * 1) Download tab; * 2) Functions for html generation for downloads, also used from other modules (edit and add dialogs); * 3) Popup menus in downloads list. */ /*** DOWNLOADS TAB ***********************************************************/ var Downloads = (new function($) { 'use strict'; // Controls var $DownloadsTable; var $DownloadsTabBadge; var $DownloadsTabBadgeEmpty; var $DownloadQueueEmpty; var $DownloadsRecordsPerPage; var $DownloadsTable_Name; var $PriorityMenu; var $CategoryMenu; // State var notification = null; var updateTabInfo; var groups; var urls; var nameColumnWidth = null; var statusData = { 'QUEUED': { Text: 'QUEUED', PostProcess: false }, 'FETCHING': { Text: 'FETCHING', PostProcess: false }, 'DOWNLOADING': { Text: 'DOWNLOADING', PostProcess: false }, 'QS_QUEUED': { Text: 'QS-QUEUED', PostProcess: true }, 'QS_EXECUTING': { Text: 'QUEUE-SCRIPT', PostProcess: true }, '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) { updateTabInfo = options.updateTabInfo; $DownloadsTable = $('#DownloadsTable'); $DownloadsTabBadge = $('#DownloadsTabBadge'); $DownloadsTabBadgeEmpty = $('#DownloadsTabBadgeEmpty'); $DownloadQueueEmpty = $('#DownloadQueueEmpty'); $DownloadsRecordsPerPage = $('#DownloadsRecordsPerPage'); $DownloadsTable_Name = $('#DownloadsTable_Name'); $PriorityMenu = $('#PriorityMenu'); $CategoryMenu = $('#DownloadsCategoryMenu'); var recordsPerPage = UISettings.read('DownloadsRecordsPerPage', 10); $DownloadsRecordsPerPage.val(recordsPerPage); $('#DownloadsTable_filter').val(''); $DownloadsTable.fasttable( { filterInput: '#DownloadsTable_filter', filterClearButton: '#DownloadsTable_clearfilter', pagerContainer: '#DownloadsTable_pager', infoContainer: '#DownloadsTable_info', infoEmpty: ' ', // this is to disable default message "No records" pageSize: recordsPerPage, maxPages: UISettings.miniTheme ? 1 : 5, pageDots: !UISettings.miniTheme, rowSelect: UISettings.rowSelect, shortcuts: true, fillFieldsCallback: fillFieldsCallback, renderCellCallback: renderCellCallback, updateInfoCallback: updateInfo, dragStartCallback: Refresher.pause, dragEndCallback: dragEndCallback, dragBlink: 'update' }); $DownloadsTable.on('click', 'a', itemClick); $DownloadsTable.on('click', 'td:nth-child(2).dropdown-cell > div:not(.dropdown-disabled)', priorityClick); $DownloadsTable.on('click', 'td:nth-child(3).dropdown-cell > div', statusClick); $DownloadsTable.on('click', 'td:nth-child(5).dropdown-cell > div:not(.dropdown-disabled)', categoryClick); $PriorityMenu.on('click', 'a', priorityMenuClick); $CategoryMenu.on('click', 'a', categoryMenuClick); DownloadsActionsMenu.init(); } this.applyTheme = function() { $DownloadsTable.fasttable('setPageSize', UISettings.read('DownloadsRecordsPerPage', 10), UISettings.miniTheme ? 1 : 5, !UISettings.miniTheme); } this.update = function() { if (!groups) { $('#DownloadsTable_Category').css('width', DownloadsUI.calcCategoryColumnWidth()); } RPC.call('listgroups', [], groups_loaded); } function groups_loaded(_groups) { if (!Refresher.isPaused()) { groups = _groups; Downloads.groups = groups; prepare(); } RPC.next(); } function prepare() { for (var j=0, jl=groups.length; j < jl; j++) { var group = groups[j]; group.postprocess = statusData[group.Status].PostProcess; } } this.redraw = function() { if (!Refresher.isPaused()) { redraw_table(); } Util.show($DownloadsTabBadge, groups.length > 0); Util.show($DownloadsTabBadgeEmpty, groups.length === 0 && UISettings.miniTheme); Util.show($DownloadQueueEmpty, groups.length === 0); } this.resize = function() { calcProgressLabels(); } /*** TABLE *************************************************************************/ var SEARCH_FIELDS = ['name', 'status', 'priority', 'category', 'estimated', 'age', 'size', 'remaining']; function redraw_table() { var data = []; for (var i=0; i < groups.length; i++) { var group = groups[i]; group.name = group.NZBName; group.status = DownloadsUI.buildStatusText(group); group.priority = DownloadsUI.buildPriorityText(group.MaxPriority); group.category = group.Category; group.estimated = DownloadsUI.buildEstimated(group); group.size = Util.formatSizeMB(group.FileSizeMB, group.FileSizeLo); group.sizemb = group.FileSizeMB; group.sizegb = group.FileSizeMB / 1024; group.left = Util.formatSizeMB(group.RemainingSizeMB-group.PausedSizeMB, group.RemainingSizeLo-group.PausedSizeLo); group.leftmb = group.RemainingSizeMB-group.PausedSizeMB; group.leftgb = group.leftmb / 1024; group.dupe = DownloadsUI.buildDupeText(group.DupeKey, group.DupeScore, group.DupeMode); var age_sec = new Date().getTime() / 1000 - (group.MinPostTime + UISettings.timeZoneCorrection*60*60); group.age = Util.formatAge(group.MinPostTime + UISettings.timeZoneCorrection*60*60); group.agem = Util.round0(age_sec / 60); group.ageh = Util.round0(age_sec / (60*60)); group.aged = Util.round0(age_sec / (60*60*24)); group._search = SEARCH_FIELDS; var item = { id: group.NZBID, data: group }; data.push(item); } $DownloadsTable.fasttable('update', data); } function fillFieldsCallback(item) { var group = item.data; var status = DownloadsUI.buildStatus(group); var priority = DownloadsUI.buildPriority(group); var progresslabel = DownloadsUI.buildProgressLabel(group, nameColumnWidth); var progress = DownloadsUI.buildProgress(group, item.data.size, item.data.left, 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)) + ''; name += DownloadsUI.buildEncryptedLabel(group.Parameters); 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 category = group.Category !== '' ? Util.textToHtml(group.Category) : 'None'; var backup = DownloadsUI.buildBackupLabel(group); if (!UISettings.miniTheme) { priority = ''; status = '
' + status + '
'; category = ''; var info = name + ' ' + url + dupe + health + backup + propagation + progresslabel; item.fields = ['
', priority, status, info, category, item.data.age, progress, item.data.estimated]; } else { var info = '
' + name + '' + url + ' ' + (group.MaxPriority == 0 ? '' : priority) + ' ' + (group.Status === 'QUEUED' ? '' : status) + dupe + health + backup + propagation; if (group.Category !== '') { info += ' ' + category + ''; } if (progresslabel) { progress = '
' + progresslabel + '
' + progress; } item.fields = [info, progress]; } } function renderCellCallback(cell, index, item) { if (index === 1) { cell.className = 'priority-cell' + (!UISettings.miniTheme ? ' dropdown-cell' : ''); } else if (index === 2 || index === 4) { cell.className = !UISettings.miniTheme ? 'dropdown-cell dropafter-cell' : ''; } else if (index === 3) { cell.className = !UISettings.miniTheme ? 'dropafter-cell' : ''; } else if (index === 5) { cell.className = 'text-right' + (!UISettings.miniTheme ? ' dropafter-cell' : ''); } else if (6 <= index && index <= 8) { cell.className = 'text-right'; } } this.recordsPerPageChange = function() { var val = $DownloadsRecordsPerPage.val(); UISettings.write('DownloadsRecordsPerPage', val); $DownloadsTable.fasttable('setPageSize', val); } function updateInfo(stat) { 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(); } this.processShortcut = function(key) { switch (key) { case 'A': Upload.addClick(); return true; case 'D': case 'Delete': case 'Meta+Backspace': Downloads.deleteClick(); return true; case 'E': case 'Enter': Downloads.editClick(); return true; case 'U': Downloads.moveClick('up'); return true; case 'N': Downloads.moveClick('down'); return true; case 'T': Downloads.moveClick('top'); return true; case 'B': Downloads.moveClick('bottom'); return true; case 'P': Downloads.pauseClick(); return true; case 'R': Downloads.resumeClick(); return true; case 'M': Downloads.mergeClick(); return true; } return $DownloadsTable.fasttable('processShortcut', key); } /*** EDIT ******************************************************/ function findGroup(nzbid) { for (var i=0; i 0) { RPC.call('editqueue', ['PostDelete', '', postprocessIDs], editCompleted); } else { editCompleted(); } }; var deleteGroups = function(command) { if (downloadIDs.length > 0) { RPC.call('editqueue', [command, '', downloadIDs], deletePosts); } else { deletePosts(); } }; DownloadsUI.deleteConfirm(deleteGroups, true, hasNzb, hasUrl, downloadIDs.length); } this.moveClick = function(action) { var checkedEditIDs = checkBuildEditIDList(true, true); if (!checkedEditIDs) { return; } var EditAction = ''; var EditOffset = 0; switch (action) { case 'top': EditAction = 'GroupMoveTop'; checkedEditIDs.reverse(); break; case 'bottom': EditAction = 'GroupMoveBottom'; break; case 'up': EditAction = 'GroupMoveOffset'; EditOffset = -1; break; case 'down': EditAction = 'GroupMoveOffset'; EditOffset = 1; checkedEditIDs.reverse(); break; } notification = ''; RPC.call('editqueue', [EditAction, '' + EditOffset, checkedEditIDs], editCompleted); } this.sort = function(order, e) { e.preventDefault(); e.stopPropagation(); var checkedEditIDs = checkBuildEditIDList(true, true, true); notification = '#Notif_Downloads_Sorted'; RPC.call('editqueue', ['GroupSort', order, checkedEditIDs], editCompleted); } function dragEndCallback(info) { if (info) { RPC.call('editqueue', [info.direction === 'after' ? 'GroupMoveAfter' : 'GroupMoveBefore', '' + info.position, info.ids], function(){ Refresher.resume(true); }); } else { Refresher.resume(); } } function priorityClick(e) { e.preventDefault(); e.stopPropagation(); var group = findGroup($(this).attr('data-nzbid')); var editIds = buildContextIdList(group); $PriorityMenu.data('nzbids', editIds); DownloadsUI.updateContextWarning($PriorityMenu, editIds); $('i', $PriorityMenu).removeClass('icon-ok').addClass('icon-empty'); $('li[data=' + group.MaxPriority + '] i', $PriorityMenu).addClass('icon-ok'); Frontend.showPopupMenu($PriorityMenu, 'left', { left: $(this).offset().left - 30, top: $(this).offset().top, width: $(this).width() + 30, height: $(this).outerHeight() - 2}); } function priorityMenuClick(e) { e.preventDefault(); var priority = $(this).parent().attr('data'); var nzbids = $PriorityMenu.data('nzbids'); notification = '#Notif_Downloads_Changed'; RPC.call('editqueue', ['GroupSetPriority', '' + priority, nzbids], editCompleted); } function statusClick(e) { e.preventDefault(); e.stopPropagation(); var group = findGroup($(this).attr('data-nzbid')); DownloadsActionsMenu.showPopupMenu(group, 'left', { left: $(this).offset().left - 30, top: $(this).offset().top, width: $(this).width() + 30, height: $(this).outerHeight() - 2 }, function(_notification) { notification = _notification; }, editCompleted); } function categoryClick(e) { e.preventDefault(); e.stopPropagation(); DownloadsUI.fillCategoryMenu($CategoryMenu); var group = findGroup($(this).attr('data-nzbid')); if (group.postprocess) { return; } var editIds = buildContextIdList(group); $CategoryMenu.data('nzbids', editIds); DownloadsUI.updateContextWarning($CategoryMenu, editIds); $('i', $CategoryMenu).removeClass('icon-ok').addClass('icon-empty'); $('li[data="' + group.Category + '"] i', $CategoryMenu).addClass('icon-ok'); Frontend.showPopupMenu($CategoryMenu, 'left', { left: $(this).offset().left - 30, top: $(this).offset().top, width: $(this).width() + 30, height: $(this).outerHeight() - 2 }); } function categoryMenuClick(e) { e.preventDefault(); var category = $(this).parent().attr('data'); var nzbids = $CategoryMenu.data('nzbids'); notification = '#Notif_Downloads_Changed'; RPC.call('editqueue', ['GroupApplyCategory', category, nzbids], editCompleted); } }(jQuery)); /*** FUNCTIONS FOR HTML GENERATION (also used from other modules) *****************************/ var DownloadsUI = (new function($) { 'use strict'; // State var categoryColumnWidth = null; var dupeCheck = null; var minLevel = null; this.fillPriorityCombo = function(combo) { combo.empty(); combo.append(''); combo.append(''); combo.append(''); combo.append(''); combo.append(''); combo.append(''); } this.fillCategoryCombo = function(combo) { combo.empty(); combo.append(''); for (var i=0; i < Options.categories.length; i++) { combo.append($('').text(Options.categories[i])); } } this.fillCategoryMenu = function(menu) { if (menu.data('initialized')) { return; } var templ = $('li:last-child', menu); for (var i=0; i < Options.categories.length; i++) { var item = templ.clone().show(); $('td:last-child', item).text(Options.categories[i]); item.attr('data', Options.categories[i]); menu.append(item); } menu.data('initialized', true); } 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) { var statusText = Downloads.statusData[group.Status].Text; var badgeClass = ''; if (group.postprocess && group.Status !== 'PP_QUEUED' && group.Status !== 'QS_QUEUED') { badgeClass = Status.status.PostPaused && group.MinPriority < 900 ? 'label-warning' : 'label-success'; } else if (group.Status === 'DOWNLOADING' || group.Status === 'FETCHING' || group.Status === 'QS_EXECUTING') { 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 && group.MinPriority < 900))) { var kind = 'progress-success'; } else if (group.Status === 'PAUSED' || (group.postprocess && !(Status.status.PostPaused && group.MinPriority < 900))) { var kind = 'progress-warning'; } else { var kind = 'progress-none'; } var totalMB = group.FileSizeMB-group.PausedSizeMB; var remainingMB = group.RemainingSizeMB-group.PausedSizeMB; var percent = Math.round((totalMB - remainingMB) / totalMB * 100); var progress = ''; if (group.postprocess) { totalsize = ''; remaining = ''; percent = Math.round(group.PostStageProgress / 10); } if (group.Kind === 'URL') { totalsize = ''; remaining = ''; } if (!UISettings.miniTheme) { progress = '
'+ '
'+ '
'+ '
'+ '
' + totalsize + '
'+ '
' + remaining + '
'+ '
'; } else { progress = '
'+ '
'+ '
'+ '
'+ '
' + (totalsize !== '' ? 'total ' : '') + totalsize + '
'+ '
' + (estimated !== '' ? '[' + estimated + ']': '') + '
'+ '
' + remaining + (remaining !== '' ? ' left' : '') + '
'+ '
'; } return progress; } this.buildEstimated = function(group) { if (group.postprocess) { if (group.PostStageProgress > 0) { return Util.formatTimeLeft(group.PostStageTimeSec / group.PostStageProgress * (1000 - group.PostStageProgress)); } } else if (group.Status !== 'PAUSED' && Status.status.DownloadRate > 0) { return Util.formatTimeLeft((group.RemainingSizeMB-group.PausedSizeMB)*1024/(Status.status.DownloadRate/1024)); } return ''; } this.buildProgressLabel = function(group, maxWidth) { var text = ''; if (group.postprocess && !(Status.status.PostPaused && group.MinPriority < 900)) { switch (group.Status) { case "LOADING_PARS": case "VERIFYING_SOURCES": case "VERIFYING_REPAIRED": case "UNPACKING": case "RENAMING": case "EXECUTING_SCRIPT": text = group.PostInfoText; break; } } return text !== '' ? ' ' + text + '' : ''; } this.buildPriorityText = function(priority) { 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'; case -100: return 'very low priority'; default: return 'priority: ' + priority; } } this.buildPriority = function(group) { var priority = group.MaxPriority var text; if (priority >= 900) text = '
'; else if (priority > 50) text = '
'; else if (priority > 0) text = '
'; else if (priority == 0) text = '
'; else if (priority >= -50) text = '
'; else text = '
'; if ([900, 100, 50, 0, -50, -100].indexOf(priority) == -1) { text = text.replace('priority', 'priority (' + priority + ')'); } return text; } this.buildEncryptedLabel = function(parameters) { var encryptedPassword = ''; for (var i = 0; i < parameters.length; i++) { if (parameters[i]['Name'].toLowerCase() === '*unpack:password') { encryptedPassword = parameters[i]['Value']; break; } } return encryptedPassword != '' ? ' encrypted' : ''; } this.buildBackupLabel = function(group) { var backup = ''; var backupPercent = calcBackupPercent(group); if (backupPercent > 0) { backup = ' backup: ' + (backupPercent < 10 ? Util.round1(backupPercent) : Util.round0(backupPercent)) + '% '; } return backup; } function calcBackupPercent(group) { var downloadedArticles = group.SuccessArticles + group.FailedArticles; if (downloadedArticles === 0) { return 0; } if (minLevel === null) { 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; } } } var backupArticles = 0; for (var j=0; j < group.ServerStats.length; j++) { var stat = group.ServerStats[j]; var level = parseInt(Options.option('Server' + stat.ServerID + '.Level')); if (level > minLevel && stat.SuccessArticles > 0) { backupArticles += stat.SuccessArticles; } } var backupPercent = 0; if (backupArticles > 0) { backupPercent = backupArticles * 100.0 / downloadedArticles; } return backupPercent; } function formatDupeText(dupeKey, dupeScore, dupeMode) { dupeKey = dupeKey.replace('rageid=', ''); dupeKey = dupeKey.replace('tvdbid=', ''); dupeKey = dupeKey.replace('tvmazeid=', ''); dupeKey = dupeKey.replace('imdb=', ''); dupeKey = dupeKey.replace('series=', ''); dupeKey = dupeKey.replace('nzb=', '#'); 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 = (categoryColumnWidth + 8) + 'px'; } return categoryColumnWidth; } 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); } this.deleteConfirm = function(actionCallback, multi, hasNzb, hasUrl, selCount) { var dupeCheck = Options.option('DupeCheck') === '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_DeletePark', dialog).prop('checked', true); $('#DownloadsDeleteConfirmDialog_DeleteDirect', dialog).prop('checked', false); $('#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('#ConfirmDialog_Help', history && dupeCheck && hasNzb); }; function action() { var deletePark = $('#DownloadsDeleteConfirmDialog_DeletePark', dialog).is(':checked'); var deleteDirect = $('#DownloadsDeleteConfirmDialog_DeleteDirect', dialog).is(':checked'); var deleteDupe = $('#DownloadsDeleteConfirmDialog_DeleteDupe', dialog).is(':checked'); var deleteFinal = $('#DownloadsDeleteConfirmDialog_DeleteFinal', dialog).is(':checked'); var command = deletePark ? "GroupParkDelete" : (deleteDirect ? 'GroupDelete' : (deleteDupe ? 'GroupDupeDelete' : 'GroupFinalDelete')); actionCallback(command); } ConfirmDialog.showModal('DownloadsDeleteConfirmDialog', action, init, selCount); } this.updateContextWarning = function(menu, editIds) { var warning = $('.dropdown-warning', $(menu)); Util.show(warning, editIds.length > 1); warning.text(editIds.length + ' records selected'); } }(jQuery)); /*** DOWNLOADS ACTION MENU *************************************************************************/ var DownloadsActionsMenu = (new function() { 'use strict' var $ActionsMenu; var curGroup; var beforeCallback; var completedCallback; var editIds; this.init = function() { $ActionsMenu = $('#DownloadsActionsMenu'); $('#DownloadsActions_Pause').click(itemPause); $('#DownloadsActions_Resume').click(itemResume); $('#DownloadsActions_Delete').click(itemDelete); $('#DownloadsActions_CancelPP').click(itemCancelPP); } this.showPopupMenu = function(group, anchor, rect, before, completed) { curGroup = group; beforeCallback = before; completedCallback = completed; editIds = Downloads.buildContextIdList(group); // setup menu items Util.show('#DownloadsActions_CancelPP', group.postprocess); Util.show('#DownloadsActions_Delete', !group.postprocess); Util.show('#DownloadsActions_Pause', group.Kind === 'NZB' && !group.postprocess); Util.show('#DownloadsActions_Resume', false); DownloadsUI.updateContextWarning($ActionsMenu, editIds); if (!group.postprocess && (group.RemainingSizeHi == group.PausedSizeHi && group.RemainingSizeLo == group.PausedSizeLo && group.Kind === 'NZB')) { $('#DownloadsActions_Resume').show(); $('#DownloadsActions_Pause').hide(); } DownloadsUI.buildDNZBLinks(group.Parameters, 'DownloadsActions_DNZB'); Frontend.showPopupMenu($ActionsMenu, anchor, rect); } function itemPause(e) { e.preventDefault(); beforeCallback('#Notif_Downloads_Paused'); RPC.call('editqueue', ['GroupPause', '', editIds], completedCallback); } function itemResume(e) { e.preventDefault(); beforeCallback('#Notif_Downloads_Resumed'); RPC.call('editqueue', ['GroupResume', '', editIds], function() { if (Options.option('ParCheck') === 'force') { completedCallback(); } else { RPC.call('editqueue', ['GroupPauseExtraPars', '', editIds], completedCallback); } }); } function itemDelete(e) { e.preventDefault(); DownloadsUI.deleteConfirm(doItemDelete, false, curGroup.Kind === 'NZB', curGroup.Kind === 'URL'); } function doItemDelete(command) { beforeCallback('#Notif_Downloads_Deleted'); RPC.call('editqueue', [command, '', editIds], completedCallback); } function itemCancelPP(e) { e.preventDefault(); beforeCallback('#Notif_Downloads_PostCanceled'); RPC.call('editqueue', ['PostDelete', '', editIds], completedCallback); } }(jQuery)); nzbget-19.1/webui/fasttable.js0000644000175000017500000010365313130203062016165 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2016 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, see . */ /* * Some code was borrowed from: * 1. Greg Weber's uiTableFilter jQuery plugin (http://gregweber.info/projects/uitablefilter) * 2. Denny Ferrassoli & Charles Christolini's TypeWatch jQuery plugin (http://github.com/dennyferra/TypeWatch) * 3. Justin Britten's tablesorterFilter jQuery plugin (http://www.justinbritten.com/work/2008/08/tablesorter-filter-results-based-on-search-string/) * 4. Allan Jardine's Bootstrap Pagination jQuery plugin for DataTables (http://datatables.net/) */ /* * In this module: * HTML tables with: * 1) very fast content updates; * 2) automatic pagination; * 3) search/filtering; * 4) drag and drop. * * What makes it unique and fast? * The tables are designed to be updated very often (up to 10 times per second). This has two challenges: * 1) updating of whole content is slow because the DOM updates are slow. * 2) if the DOM is updated during user interaction the user input is not processed correctly. * For example if the table is updated after the user pressed mouse key but before he/she released * the key, the click is not processed because the element, on which the click was performed, * doesn't exist after the update of DOM anymore. * * How Fasttable solves these problems? The solutions is to update only rows and cells, * which were changed by keeping the unchanged DOM-elements. * * Important: the UI of table must be designed in a way, that the cells which are frequently changed * (like remaining download size) should not be clickable, whereas the cells which are rarely changed * (e. g. Download name) can be clickable. */ (function($) { 'use strict'; $.fn.fasttable = function(method) { if (methods[method]) { return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); } else if ( typeof method === 'object' || ! method ) { return methods.init.apply( this, arguments ); } else { $.error( 'Method ' + method + ' does not exist on jQuery.fasttable' ); } }; var methods = { defaults : function() { return defaults; }, init : function(options) { return this.each(function() { var $this = $(this); var data = $this.data('fasttable'); // If the plugin hasn't been initialized yet if (!data) { /* Do more setup stuff here */ var config = {}; config = $.extend(config, defaults, options); config.filterInput = $(config.filterInput); config.filterClearButton = $(config.filterClearButton); config.pagerContainer = $(config.pagerContainer); config.infoContainer = $(config.infoContainer); config.dragBox = $(config.dragBox); config.dragContent = $(config.dragContent); config.dragBadge = $(config.dragBadge); config.selector = $('th.table-selector', $this); var searcher = new FastSearcher(); // 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 is the query has not changed. // Immediately perform the filter if the ENTER key is pressed. var timer; config.filterInput.keyup(function() { var timerWait = 500; var overrideBool = false; var inputBox = this; // Was ENTER pushed? if (inputBox.keyCode == 13) { timerWait = 1; overrideBool = true; } var timerCallback = function() { var value = inputBox.value.trim(); var data = $this.data('fasttable'); if ((value != data.lastFilter) || overrideBool) { applyFilter(data, value); } }; // Reset the timer clearTimeout(timer); timer = setTimeout(timerCallback, timerWait); return false; }); config.filterClearButton.click(function() { var data = $this.data('fasttable'); data.config.filterInput.val(''); applyFilter(data, ''); }); config.pagerContainer.on('click', 'li', function (e) { e.preventDefault(); var data = $this.data('fasttable'); var pageNum = $(this).text(); if (pageNum.indexOf('Prev') > -1) { data.curPage--; } else if (pageNum.indexOf('Next') > -1) { data.curPage++; } else if (isNaN(parseInt(pageNum))) { return; } else { data.curPage = parseInt(pageNum); } refresh(data); }); var data = { target: $this, config: config, pageSize: parseInt(config.pageSize), maxPages: parseInt(config.maxPages), pageDots: Util.parseBool(config.pageDots), curPage: 1, checkedRows: {}, checkedCount: 0, lastClickedRowID: null, searcher: searcher }; initDragDrop(data); $this.on('click', 'thead > tr', function(e) { titleCheckClick(data, e); }); $this.on('click', 'tbody > tr', function(e) { itemCheckClick(data, e); }); $this.data('fasttable', data); } }); }, destroy : function() { return this.each(function() { var $this = $(this); var data = $this.data('fasttable'); // Namespacing FTW $(window).unbind('.fasttable'); $this.removeData('fasttable'); }); }, update : updateContent, setPageSize : setPageSize, setCurPage : setCurPage, applyFilter : function(filter) { applyFilter($(this).data('fasttable'), filter); }, filteredContent : function() { return $(this).data('fasttable').filteredContent; }, availableContent : function() { return $(this).data('fasttable').availableContent; }, checkedRows : function() { return $(this).data('fasttable').checkedRows; }, checkedCount : function() { return $(this).data('fasttable').checkedCount; }, pageCheckedCount : function() { return $(this).data('fasttable').pageCheckedCount; }, checkRow : function(id, checked) { checkRow($(this).data('fasttable'), id, checked); }, processShortcut : function(key) { return processShortcut($(this).data('fasttable'), key); }, }; function updateContent(content) { var data = $(this).data('fasttable'); if (content) { data.content = content; } refresh(data); blinkMovedRecords(data); } function applyFilter(data, filter) { data.lastFilter = filter; if (data.content) { data.curPage = 1; data.hasFilter = filter !== ''; data.searcher.compile(filter); refresh(data); } if (filter !== '' && data.config.filterInputCallback) { data.config.filterInputCallback(filter); } if (filter === '' && data.config.filterClearCallback) { data.config.filterClearCallback(); } } function refresh(data) { refilter(data); validateChecks(data); updatePager(data); updateInfo(data); updateSelector(data); updateTable(data); } function refilter(data) { data.availableContent = []; data.filteredContent = []; for (var i = 0; i < data.content.length; i++) { var item = data.content[i]; if (data.hasFilter && item.search === undefined && data.config.fillSearchCallback) { data.config.fillSearchCallback(item); } if (!data.hasFilter || data.searcher.exec(item.data)) { data.availableContent.push(item); if (!data.config.filterCallback || data.config.filterCallback(item)) { data.filteredContent.push(item); } } } } function updateTable(data) { var oldTable = data.target[0]; var newTable = buildTBody(data); updateTBody(data, oldTable, newTable); } function buildTBody(data) { var table = $('
')[0]; for (var i=0; i < data.pageContent.length; i++) { var item = data.pageContent[i]; var row = table.insertRow(table.rows.length); row.fasttableID = item.id; if (data.checkedRows[item.id]) { row.className = 'checked'; } if (data.config.renderRowCallback) { data.config.renderRowCallback(row, item); } if (!item.fields) { if (data.config.fillFieldsCallback) { data.config.fillFieldsCallback(item); } else { item.fields = []; } } for (var j=0; j < item.fields.length; j++) { var cell = row.insertCell(row.cells.length); cell.innerHTML = item.fields[j]; if (data.config.renderCellCallback) { data.config.renderCellCallback(cell, j, item); } } } titleCheckRedraw(data); if (data.config.renderTableCallback) { data.config.renderTableCallback(table); } return table; } function updateTBody(data, oldTable, newTable) { var headerRows = $('thead > tr', oldTable).length; var oldTRs = oldTable.rows; var newTRs = newTable.rows; var oldTBody = $('tbody', oldTable)[0]; var oldTRsLength = oldTRs.length - headerRows; // evlt. skip header row var newTRsLength = newTRs.length; for (var i=0; i < newTRs.length; ) { var newTR = newTRs[i]; if (i < oldTRsLength) { // update existing row var oldTR = oldTRs[i + headerRows]; // evlt. skip header row var oldTDs = oldTR.cells; var newTDs = newTR.cells; oldTR.className = newTR.className; oldTR.fasttableID = newTR.fasttableID; for (var j=0, n = 0; j < oldTDs.length; j++, n++) { var oldTD = oldTDs[j]; var newTD = newTDs[n]; var oldHtml = oldTD.outerHTML; var newHtml = newTD.outerHTML; if (oldHtml !== newHtml) { oldTR.replaceChild(newTD, oldTD); n--; } } i++; } else { // add new row oldTBody.appendChild(newTR); } } var maxTRs = newTRsLength + headerRows; // evlt. skip header row; while (oldTRs.length > maxTRs) { oldTable.deleteRow(oldTRs.length - 1); } } function updatePager(data) { data.pageCount = Math.ceil(data.filteredContent.length / data.pageSize); if (data.curPage < 1) { data.curPage = 1; } if (data.curPage > data.pageCount) { data.curPage = data.pageCount; } var startIndex = (data.curPage - 1) * data.pageSize; data.pageContent = data.filteredContent.slice(startIndex, startIndex + data.pageSize); var pagerObj = data.config.pagerContainer; var pagerHtml = buildPagerHtml(data); var oldPager = pagerObj[0]; var newPager = $(pagerHtml)[0]; updatePagerContent(data, oldPager, newPager); } function buildPagerHtml(data) { var iListLength = data.maxPages; var iStart, iEnd, iHalf = Math.floor(iListLength/2); if (data.pageCount < iListLength) { iStart = 1; iEnd = data.pageCount; } else if (data.curPage -1 <= iHalf) { iStart = 1; iEnd = iListLength; } else if (data.curPage - 1 >= (data.pageCount-iHalf)) { iStart = data.pageCount - iListLength + 1; iEnd = data.pageCount; } else { iStart = data.curPage - 1 - iHalf + 1; iEnd = iStart + iListLength - 1; } var pager = '
    '; pager += '← Prev'; if (iStart > 1) { pager += '
  • 1
  • '; if (iStart > 2 && data.pageDots) { pager += '
  • '; } } for (var j=iStart; j<=iEnd; j++) { pager += '' + j + ''; } if (iEnd != data.pageCount) { if (iEnd < data.pageCount - 1 && data.pageDots) { pager += '
  • '; } pager += '
  • ' + data.pageCount + '
  • '; } pager += 'Next →'; pager += '
'; return pager; } function updatePagerContent(data, oldPager, newPager) { var oldLIs = oldPager.getElementsByTagName('li'); var newLIs = newPager.getElementsByTagName('li'); var oldLIsLength = oldLIs.length; var newLIsLength = newLIs.length; for (var i=0, n=0; i < newLIs.length; i++, n++) { var newLI = newLIs[i]; if (n < oldLIsLength) { // update existing LI var oldLI = oldLIs[n]; var oldHtml = oldLI.outerHTML; var newHtml = newLI.outerHTML; if (oldHtml !== newHtml) { oldPager.replaceChild(newLI, oldLI); i--; } } else { // add new LI oldPager.appendChild(newLI); i--; } } while (oldLIs.length > newLIsLength) { oldPager.removeChild(oldPager.lastChild); } } function updateInfo(data) { if (data.content.length === 0) { var infoText = data.config.infoEmpty; } else if (data.curPage === 0) { var infoText = 'No matching records found (total ' + data.content.length + ')'; } else { 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.length != data.content.length) { infoText += ' filtered (total ' + data.content.length + ')'; } } data.config.infoContainer.html(infoText); if (data.config.updateInfoCallback) { data.config.updateInfoCallback({ total: data.content.length, available: data.availableContent.length, filtered: data.filteredContent.length, firstRecord: firstRecord, lastRecord: lastRecord }); } } function updateSelector(data) { data.pageCheckedCount = 0; if (data.checkedCount > 0 && data.filteredContent.length > 0) { for (var i = (data.curPage - 1) * data.pageSize; i < Math.min(data.curPage * data.pageSize, data.filteredContent.length); i++) { data.pageCheckedCount += data.checkedRows[data.filteredContent[i].id] ? 1 : 0; } } data.config.selector.css('display', data.pageCheckedCount === data.checkedCount ? 'none' : ''); if (data.checkedCount !== data.pageCheckedCount) { data.config.selector.text('' + (data.checkedCount - data.pageCheckedCount) + (data.checkedCount - data.pageCheckedCount > 1 ? ' records' : ' record') + ' selected on other pages'); } } function setPageSize(pageSize, maxPages, pageDots) { var data = $(this).data('fasttable'); data.pageSize = parseInt(pageSize); data.curPage = 1; if (maxPages !== undefined) { data.maxPages = maxPages; } if (pageDots !== undefined) { data.pageDots = pageDots; } refresh(data); } function setCurPage(page) { var data = $(this).data('fasttable'); data.curPage = parseInt(page); refresh(data); } function checkedIds(data) { var checkedRows = data.checkedRows; var checkedIds = []; for (var i = 0; i < data.content.length; i++) { var id = data.content[i].id; if (checkedRows[id]) { checkedIds.push(id); } } return checkedIds; } function titleCheckRedraw(data) { var filteredContent = data.filteredContent; var checkedRows = data.checkedRows; var hasSelectedItems = false; var hasUnselectedItems = false; for (var i = 0; i < filteredContent.length; i++) { if (checkedRows[filteredContent[i].id]) { hasSelectedItems = true; } else { hasUnselectedItems = true; } } var headerRow = $('thead > tr', data.target); if (hasSelectedItems && hasUnselectedItems) { headerRow.removeClass('checked').addClass('checkremove'); } else if (hasSelectedItems) { headerRow.removeClass('checkremove').addClass('checked'); } else { headerRow.removeClass('checked').removeClass('checkremove'); } } function itemCheckClick(data, event) { var checkmark = $(event.target).hasClass('check'); if (data.dragging || (!checkmark && !data.config.rowSelect)) { return; } var row = $(event.target).closest('tr', data.target)[0]; var id = row.fasttableID; var doToggle = true; var checkedRows = data.checkedRows; if (event.shiftKey && data.lastClickedRowID != null) { var checked = checkedRows[id]; doToggle = !checkRange(data, id, data.lastClickedRowID, !checked); } if (doToggle) { toggleCheck(data, id); } data.lastClickedRowID = id; refresh(data); } function titleCheckClick(data, event) { var checkmark = $(event.target).hasClass('check'); if (data.dragging || (!checkmark && !data.config.rowSelect)) { return; } var filteredContent = data.filteredContent; var checkedRows = data.checkedRows; var hasSelectedItems = false; for (var i = 0; i < filteredContent.length; i++) { if (checkedRows[filteredContent[i].id]) { hasSelectedItems = true; break; } } data.lastClickedRowID = null; checkAll(data, !hasSelectedItems); } function toggleCheck(data, id) { var checkedRows = data.checkedRows; var index = checkedRows[id]; if (checkedRows[id]) { checkedRows[id] = undefined; data.checkedCount--; } else { checkedRows[id] = true; data.checkedCount++; } } function checkAll(data, checked) { var filteredContent = data.filteredContent; for (var i = 0; i < filteredContent.length; i++) { checkRow(data, filteredContent[i].id, checked); } refresh(data); } function checkRange(data, from, to, checked) { var filteredContent = data.filteredContent; var indexFrom = indexOfID(filteredContent, from); var indexTo = indexOfID(filteredContent, to); if (indexFrom === -1 || indexTo === -1) { return false; } if (indexTo < indexFrom) { var tmp = indexTo; indexTo = indexFrom; indexFrom = tmp; } for (var i = indexFrom; i <= indexTo; i++) { checkRow(data, filteredContent[i].id, checked); } return true; } function checkRow(data, id, checked) { if (checked) { if (!data.checkedRows[id]) { data.checkedCount++; } data.checkedRows[id] = true; } else { if (data.checkedRows[id]) { data.checkedCount--; } data.checkedRows[id] = undefined; } } function indexOfID(content, id) { for (var i = 0; i < content.length; i++) { if (id === content[i].id) { return i; } } return -1; } function validateChecks(data) { var filteredContent = data.filteredContent; var checkedRows = data.checkedRows; data.checkedRows = {} data.checkedCount = 0; for (var i = 0; i < data.content.length; i++) { if (checkedRows[data.content[i].id]) { data.checkedRows[data.content[i].id] = true; data.checkedCount++; } } } //*************** DRAG-N-DROP function initDragDrop(data) { data.target[0].addEventListener('mousedown', function(e) { mouseDown(data, e); }, true); data.target[0].addEventListener('touchstart', function(e) { mouseDown(data, e); }, true); data.moveIds = []; data.dropAfter = false; data.dropId = null; data.dragging = false; data.dragRow = $(''); data.cancelDrag = false; data.downPos = null; data.blinkIds = []; data.blinkState = null; data.wantBlink = false; } function touchToMouse(e) { if (e.type === 'touchstart' || e.type === 'touchmove' || e.type === 'touchend') { e.clientX = e.changedTouches[0].clientX; e.clientY = e.changedTouches[0].clientY; } } function mouseDown(data, e) { data.dragging = false; data.dropId = null; data.dragRow = $(e.target).closest('tr', data.target); var checkmark = $(e.target).hasClass('check') || ($(e.target).find('.check').length > 0 && !$('body').hasClass('phone')); var head = $(e.target).closest('tr', data.target).parent().is('thead'); if (head || !(checkmark || (data.config.rowSelect && e.type === 'mousedown')) || data.dragRow.length != 1 || e.ctrlKey || e.altKey || e.metaKey) { return; } touchToMouse(e); if (e.type === 'mousedown') { e.preventDefault(); } if (!data.config.dragEndCallback) { return; } data.downPos = { x: e.clientX, y: e.clientY }; data.mouseMove = function(e) { mouseMove(data, e); }; data.mouseUp = function(e) { mouseUp(data, e); }; data.keyDown = function(e) { keyDown(data, e); }; document.addEventListener('mousemove', data.mouseMove, true); document.addEventListener('touchmove', data.mouseMove, true); document.addEventListener('mouseup', data.mouseUp, true); document.addEventListener('touchend', data.mouseUp, true); document.addEventListener('touchcancel', data.mouseUp, true); document.addEventListener('keydown', data.keyDown, true); } function mouseMove(data, e) { touchToMouse(e); e.preventDefault(); if (e.touches && e.touches.length > 1) { data.cancelDrag = true; mouseUp(data, e); return; } if (!data.dragging) { if (Math.abs(data.downPos.x - e.clientX) < 5 && Math.abs(data.downPos.y - e.clientY) < 5) { return; } startDrag(data, e); if (data.dragCancel) { mouseUp(data, e); return; } } updateDrag(data, e.clientX, e.clientY); autoScroll(data, e.clientX, e.clientY); } function startDrag(data, e) { if (data.config.dragStartCallback) { data.config.dragStartCallback(); } var offsetX = $(document).scrollLeft(); var offsetY = $(document).scrollTop(); var rf = data.dragRow.offset(); data.dragOffset = { x: data.downPos.x - rf.left + offsetX, y: Math.min(Math.max(data.downPos.y - rf.top + offsetY, 0), data.dragRow.height()) }; var checkedRows = data.checkedRows; var chkIds = checkedIds(data); var id = data.dragRow[0].fasttableID; data.moveIds = checkedRows[id] ? chkIds : [id]; data.dragging = true; data.cancelDrag = false; buildDragBox(data); data.config.dragBox.css('display', 'block'); data.dragRow.addClass('drag-source'); $('html').addClass('drag-progress'); data.oldOverflowX = $('body').css('overflow-x'); $('body').css('overflow-x', 'hidden'); } function buildDragBox(data) { var tr = data.dragRow.clone(); var table = data.target.clone(); $('tr', table).remove(); $('thead', table).remove(); $('tbody', table).append(tr); table.css('margin', 0); data.config.dragContent.html(table); data.config.dragBadge.text(data.moveIds.length); data.config.dragBadge.css('display', data.moveIds.length > 1 ? 'block' : 'none'); data.config.dragBox.css({left: data.target.offset().left, width: data.dragRow.width()}); var tds = $('td', tr); $('td', data.dragRow).each(function(ind, el) { $(tds[ind]).css('width', $(el).width()); }); } function updateDrag(data, x, y) { var offsetX = $(document).scrollLeft(); var offsetY = $(document).scrollTop(); var posX = x + offsetX; var posY = y + offsetY; data.config.dragBox.css({ left: posX - data.dragOffset.x, top: Math.max(Math.min(posY - data.dragOffset.y, offsetY + $(window).height() - data.config.dragBox.height() - 2), offsetY + 2)}); var dt = data.config.dragBox.offset().top; var dh = data.config.dragBox.height(); var rows = $('tbody > tr', data.target); for (var i = 0; i < rows.length; i++) { var row = $(rows[i]); var rt = row.offset().top; var rh = row.height(); if (row[0] !== data.dragRow[0]) { if ((dt >= rt && dt <= rt + rh / 2) || (dt < rt && i == 0)) { data.dropAfter = false; row.before(data.dragRow); data.dropId = row[0].fasttableID; break; } if ((dt + dh >= rt + rh / 2 && dt + dh <= rt + rh) || (dt + dh > rt + rh && i === rows.length - 1)) { data.dropAfter = true; row.after(data.dragRow); data.dropId = row[0].fasttableID; break; } } } if (data.dropId === null) { data.dropId = data.dragRow[0].fasttableID; data.dropAfter = true; } } function autoScroll(data, x, y) { // works properly only if the table lays directly on the page (not in another scrollable div) data.scrollStep = (y > $(window).height() - 20 ? 1 : y < 20 ? -1 : 0) * 5; if (data.scrollStep !== 0 && !data.scrollTimer) { var scroll = function() { $(document).scrollTop($(document).scrollTop() + data.scrollStep); updateDrag(data, x, y + data.scrollStep); data.scrollTimer = data.scrollStep == 0 ? null : setTimeout(scroll, 10); } data.scrollTimer = setTimeout(scroll, 500); } } function mouseUp(data, e) { document.removeEventListener('mousemove', data.mouseMove, true); document.removeEventListener('touchmove', data.mouseMove, true); document.removeEventListener('mouseup', data.mouseUp, true); document.removeEventListener('touchend', data.mouseUp, true); document.removeEventListener('touchcancel', data.mouseUp, true); document.removeEventListener('keydown', data.keyDown, true); if (!data.dragging) { return; } data.dragging = false; data.cancelDrag = data.cancelDrag || e.type === 'touchcancel'; data.dragRow.removeClass('drag-source'); $('html').removeClass('drag-progress'); $('body').css('overflow-x', data.oldOverflowX); data.config.dragBox.hide(); data.scrollStep = 0; clearTimeout(data.scrollTimer); data.scrollTimer = null; moveRecords(data); } function keyDown(data, e) { if (e.keyCode == 27) // ESC-key { data.cancelDrag = true; e.preventDefault(); mouseUp(data, e); } } function moveRecords(data) { if (data.dropId !== null && !data.cancelDrag && !(data.moveIds.length == 1 && data.dropId == data.moveIds[0])) { data.blinkIds = data.moveIds; data.moveIds = []; data.blinkState = data.config.dragBlink === 'none' ? 0 : 3; data.wantBlink = data.blinkState > 0; moveRows(data); } else { data.dropId = null; } if (data.dropId === null) { data.moveIds = []; } refresh(data); data.config.dragEndCallback(data.dropId !== null ? { ids: data.blinkIds, position: data.dropId, direction: data.dropAfter ? 'after' : 'before' } : null); if (data.config.dragBlink === 'direct') { data.target.fasttable('update'); } } function moveRows(data) { var movedIds = data.blinkIds; var movedRecords = []; for (var i = 0; i < data.content.length; i++) { var item = data.content[i]; if (movedIds.indexOf(item.id) > -1) { movedRecords.push(item); data.content.splice(i, 1); i--; if (item.id === data.dropId) { if (i >= 0) { data.dropId = data.content[i].id; data.dropAfter = true; } else if (i + 1 < data.content.length) { data.dropId = data.content[i + 1].id; data.dropAfter = false; } else { data.dropId = null; } } } } if (data.dropId === null) { // restore content for (var j = 0; j < movedRecords.length; j++) { data.content.push(movedRecords[j]); } return; } for (var i = 0; i < data.content.length; i++) { if (data.content[i].id === data.dropId) { for (var j = movedRecords.length - 1; j >= 0; j--) { data.content.splice(data.dropAfter ? i + 1 : i, 0, movedRecords[j]); } break; } } } function blinkMovedRecords(data) { if (data.blinkIds.length > 0) { blinkProgress(data, data.wantBlink); data.wantBlink = false; } } function blinkProgress(data, recur) { var rows = $('tr', data.target); rows.removeClass('drag-finish'); rows.each(function(ind, el) { var id = el.fasttableID; if (data.blinkIds.indexOf(id) > -1 && (data.blinkState === 1 || data.blinkState === 3 || data.blinkState === 5)) { $(el).addClass('drag-finish'); } }); if (recur && data.blinkState > 0) { setTimeout(function() { data.blinkState -= 1; blinkProgress(data, true); }, 150); } if (data.blinkState === 0) { data.blinkIds = []; } } //*************** KEYBOARD function processShortcut(data, key) { switch (key) { case 'Left': data.curPage = Math.max(data.curPage - 1, 1); refresh(data); return true; case 'Shift+Left': data.curPage = 1; refresh(data); return true; case 'Right': data.curPage = Math.min(data.curPage + 1, data.pageCount); refresh(data); return true; case 'Shift+Right': data.curPage = data.pageCount; refresh(data); return true; case 'Shift+F': data.config.filterInput.focus(); return true; case 'Shift+C': data.config.filterClearButton.click(); return true; } } //*************** CONFIG var defaults = { filterInput: '#TableFilter', filterClearButton: '#TableClear', pagerContainer: '#TablePager', infoContainer: '#TableInfo', dragBox: '#TableDragBox', dragContent: '#TableDragContent', dragBadge: '#TableDragBadge', dragBlink: 'none', // none, direct, update pageSize: 10, maxPages: 5, pageDots: true, rowSelect: false, shortcuts: false, infoEmpty: 'No records', renderRowCallback: undefined, renderCellCallback: undefined, renderTableCallback: undefined, fillFieldsCallback: undefined, updateInfoCallback: undefined, filterInputCallback: undefined, filterClearCallback: undefined, fillSearchCallback: undefined, filterCallback: undefined, dragStartCallback: undefined, dragEndCallback: undefined }; })(jQuery); function FastSearcher() { 'use strict'; this.source; this.len; this.p; this.initLexer = function(source) { this.source = source; this.len = source.length; this.p = 0; } this.nextToken = function() { while (this.p < this.len) { var ch = this.source[this.p++]; switch (ch) { case ' ': case '\t': continue; case '-': case '(': case ')': case '|': return ch; default: this.p--; var token = ''; var quote = false; while (this.p < this.len) { var ch = this.source[this.p++]; if (quote) { if (ch === '"') { quote = false; ch = ''; } } else { if (ch === '"') { quote = true; ch = ''; } else if (' \t()|'.indexOf(ch) > -1) { this.p--; return token; } } token += ch; } return token; } } return null; } this.compile = function(searchstr) { var _this = this; this.initLexer(searchstr); function expression(greedy) { var node = null; while (true) { var token = _this.nextToken(); var node2 = null; switch (token) { case null: case ')': return node; case '-': node2 = expression(false); node2 = node2 ? _this.not(node2) : node2; break; case '(': node2 = expression(true); break; case '|': node2 = expression(false); break; default: node2 = _this.term(token); } if (node && node2) { node = token === '|' ? _this.or(node, node2) : _this.and(node, node2); } else if (node2) { node = node2; } if (!greedy && node) { return node; } } } this.root = expression(true); } this.root = null; this.data = null; this.exec = function(data) { this.data = data; return this.root ? this.root.eval() : true; } this.and = function(L, R) { return { L: L, R: R, eval: function() { return this.L.eval() && this.R.eval(); } }; } this.or = function(L, R) { return { L: L, R: R, eval: function() { return this.L.eval() || this.R.eval(); } }; } this.not = function(M) { return { M: M, eval: function() { return !this.M.eval();} }; } this.term = function(term) { return this.compileTerm(term); } var COMMANDS = [ ':', '>=', '<=', '<>', '>', '<', '=' ]; this.compileTerm = function(term) { var _this = this; var text = term.toLowerCase(); var field; var command; var commandIndex; for (var i = 0; i < COMMANDS.length; i++) { var cmd = COMMANDS[i]; var p = term.indexOf(cmd); if (p > -1 && (p < commandIndex || commandIndex === undefined)) { commandIndex = p; command = cmd; } } if (command !== undefined) { field = term.substring(0, commandIndex); text = text.substring(commandIndex + command.length); } return { command: command, text: text, field: field, eval: function() { return _this.evalTerm(this); } }; } this.evalTerm = function(term) { var text = term.text; var field = term.field; var content = this.fieldValue(this.data, field); if (content === undefined) { return false; } switch (term.command) { case undefined: case ':': return content.toString().toLowerCase().indexOf(text) > -1; case '=': return content.toString().toLowerCase() == text; case '<>': return content.toString().toLowerCase() != text; case '>': return parseInt(content) > parseInt(text); case '>=': return parseInt(content) >= parseInt(text); case '<': return parseInt(content) < parseInt(text); case '<=': return parseInt(content) <= parseInt(text); default: return false; } } this.fieldValue = function(data, field) { var value = ''; if (field !== undefined) { value = data[field]; if (value === undefined) { if (this.nameMap === undefined) { this.buildNameMap(data); } value = data[this.nameMap[field.toLowerCase()]]; } } else { if (data._search === true) { for (var prop in data) { value += ' ' + data[prop]; } } else { for (var i = 0; i < data._search.length; i++) { value += ' ' + data[data._search[i]]; } } } return value; } this.nameMap; this.buildNameMap = function(data) { this.nameMap = {}; for (var prop in data) { this.nameMap[prop.toLowerCase()] = prop; } } } nzbget-19.1/webui/messages.js0000644000175000017500000002106313130203062016021 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2016 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, see . */ /* * In this module: * 1) Messages tab. */ /*** MESSAGES TAB *********************************************************************/ var Messages = (new function($) { 'use strict'; // Controls var $MessagesTable; var $MessagesTabBadge; var $MessagesTabBadgeEmpty; var $MessagesRecordsPerPage; // State var messages; var maxMessages = null; var lastID = 0; var updateTabInfo; var notification = null; var curFilter = 'ALL'; var activeTab = false; this.init = function(options) { updateTabInfo = options.updateTabInfo; $MessagesTable = $('#MessagesTable'); $MessagesTabBadge = $('#MessagesTabBadge'); $MessagesTabBadgeEmpty = $('#MessagesTabBadgeEmpty'); $MessagesRecordsPerPage = $('#MessagesRecordsPerPage'); var recordsPerPage = UISettings.read('MessagesRecordsPerPage', 10); $MessagesRecordsPerPage.val(recordsPerPage); $('#MessagesTable_filter').val(''); $MessagesTable.fasttable( { filterInput: '#MessagesTable_filter', filterClearButton: '#MessagesTable_clearfilter', pagerContainer: '#MessagesTable_pager', infoContainer: '#MessagesTable_info', filterCaseSensitive: false, pageSize: recordsPerPage, maxPages: UISettings.miniTheme ? 1 : 5, pageDots: !UISettings.miniTheme, shortcuts: true, fillFieldsCallback: fillFieldsCallback, fillSearchCallback: fillSearchCallback, filterCallback: filterCallback, renderCellCallback: renderCellCallback, updateInfoCallback: updateInfo }); } this.applyTheme = function() { $MessagesTable.fasttable('setPageSize', UISettings.read('MessagesRecordsPerPage', 10), UISettings.miniTheme ? 1 : 5, !UISettings.miniTheme); } this.show = function() { activeTab = true; this.redraw(); } this.hide = function() { activeTab = false; } this.update = function() { if (maxMessages === null) { maxMessages = parseInt(Options.option('LogBufferSize')); initFilterButtons(); } if (lastID === 0) { RPC.call('log', [0, maxMessages], loaded); } else { RPC.call('log', [lastID+1, 0], loaded); } } function loaded(newMessages) { merge(newMessages); RPC.next(); } function merge(newMessages) { if (lastID === 0) { messages = newMessages; } else { messages = messages.concat(newMessages); messages.splice(0, messages.length-maxMessages); } if (messages.length > 0) { lastID = messages[messages.length-1].ID; } } this.redraw = function() { var data = []; for (var i=0; i < messages.length; i++) { var message = messages[i]; var item = { id: message.ID, data: message }; data.unshift(item); } $MessagesTable.fasttable('update', data); Util.show($MessagesTabBadge, messages.length > 0); Util.show($MessagesTabBadgeEmpty, messages.length === 0 && UISettings.miniTheme); } function fillFieldsCallback(item) { var message = item.data; var kind; switch (message.Kind) { case 'INFO': kind = 'info'; break; case 'DETAIL': kind = 'detail'; break; case 'WARNING': kind = 'warning'; break; case 'ERROR': kind = 'error'; break; case 'DEBUG': kind = 'debug'; break; } var text = Util.textToHtml(message.Text); // replace URLs var exp = /(http:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; text = text.replace(exp, "$1"); if (!item.time) { item.time = Util.formatDateTime(message.Time + UISettings.timeZoneCorrection*60*60); } if (!UISettings.miniTheme) { item.fields = [kind, item.time, text]; } else { var info = kind + ' ' + item.time + ' ' + text; item.fields = [info]; } } var SEARCH_FIELDS = ['kind', 'time', 'text']; function fillSearchCallback(item) { item.data.kind = item.data.Kind; item.data.text = item.data.Text; item.data.time = Util.formatDateTime(item.data.Time + UISettings.timeZoneCorrection*60*60); item.data._search = SEARCH_FIELDS; } function renderCellCallback(cell, index, item) { if (index === 1) { cell.className = 'text-center'; } } function updateInfo(stat) { updateTabInfo($MessagesTabBadge, stat); if (activeTab) { updateFilterButtons(); } } this.recordsPerPageChange = function() { var val = $MessagesRecordsPerPage.val(); UISettings.write('MessagesRecordsPerPage', val); $MessagesTable.fasttable('setPageSize', val); } function filterCallback(item) { return !activeTab || curFilter === 'ALL' || item.data.Kind === curFilter; } function initFilterButtons() { var detail = ['both', 'screen'].indexOf(Options.option('DetailTarget')) > -1 var info = ['both', 'screen'].indexOf(Options.option('InfoTarget')) > -1; var warning = ['both', 'screen'].indexOf(Options.option('WarningTarget')) > -1; var error = ['both', 'screen'].indexOf(Options.option('ErrorTarget')) > -1; Util.show($('#Messages_Badge_DETAIL, #Messages_Badge_DETAIL2').closest('.btn'), detail); Util.show($('#Messages_Badge_INFO, #Messages_Badge_INFO2 ').closest('.btn'), info); Util.show($('#Messages_Badge_WARNING, #Messages_Badge_WARNING2').closest('.btn'), warning); Util.show($('#Messages_Badge_ERROR, #Messages_Badge_ERROR2').closest('.btn'), error); Util.show($('#Messages_Badge_ALL, #Messages_Badge_ALL2').closest('.btn'), detail || info || warning || error); } function updateFilterButtons() { var countDebug = 0; var countDetail = 0; var countInfo = 0; var countWarning = 0; var countError = 0; var data = $MessagesTable.fasttable('availableContent'); for (var i=0; i < data.length; i++) { var message = data[i].data; switch (message.Kind) { case 'INFO': countInfo++; break; case 'DETAIL': countDetail++; break; case 'WARNING': countWarning++; break; case 'ERROR': countError++; break; case 'DEBUG': countDebug++; break; } } $('#Messages_Badge_ALL,#Messages_Badge_ALL2').text(countDebug + countDetail + countInfo + countWarning + countError); $('#Messages_Badge_DETAIL,#Messages_Badge_DETAIL2').text(countDetail); $('#Messages_Badge_INFO,#Messages_Badge_INFO2').text(countInfo); $('#Messages_Badge_WARNING,#Messages_Badge_WARNING2').text(countWarning); $('#Messages_Badge_ERROR,#Messages_Badge_ERROR2').text(countError); $('#MessagesTab_Toolbar .btn').removeClass('btn-inverse'); $('#Messages_Badge_' + curFilter + ',#Messages_Badge_' + curFilter + '2').closest('.btn').addClass('btn-inverse'); $('#MessagesTab_Toolbar .badge').removeClass('badge-active'); $('#Messages_Badge_' + curFilter + ',#Messages_Badge_' + curFilter + '2').addClass('badge-active'); } this.filter = function(type) { curFilter = type; Messages.redraw(); } this.clearClick = function() { ConfirmDialog.showModal('MessagesClearConfirmDialog', messagesClear); } function messagesClear() { Refresher.pause(); RPC.call('clearlog', [], function() { RPC.call('writelog', ['INFO', 'Messages have been deleted'], function() { notification = '#Notif_Messages_Cleared'; lastID = 0; editCompleted(); }); }); } function editCompleted() { Refresher.update(); if (notification) { PopupNotification.show(notification); notification = null; } } this.processShortcut = function(key) { switch (key) { case 'A': Messages.filter('ALL'); return true; case 'T': Messages.filter('DETAIL'); return true; case 'I': Messages.filter('INFO'); return true; case 'W': Messages.filter('WARNING'); return true; case 'E': Messages.filter('ERROR'); return true; case 'D': case 'Delete': case 'Meta+Backspace': Messages.clearClick(); return true; } return $MessagesTable.fasttable('processShortcut', key); } }(jQuery)); nzbget-19.1/webui/index.js0000644000175000017500000006752413130203062015335 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2017 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, see . */ /* * In this module: * 1) Web-interface intialization; * 2) Web-interface settings; * 3) Refresh handling; * 4) Window resize handling including automatic theme switching (desktop/phone); * 5) Confirmation dialog; * 6) Popup notifications. */ /*** WEB-INTERFACE SETTINGS (THIS IS NOT NZBGET CONFIG!) ***********************************/ var UISettings = (new function($) { 'use strict'; /*** Web-interface configuration *************/ // Options having descriptions can be edited directly in web-interface on settings page. this.description = []; this.description['activityAnimation'] = 'Animation on play/pause button (yes, no).'; this.activityAnimation = true; this.description['refreshAnimation'] = 'Animation on refresh button (yes, no).'; this.refreshAnimation = true; this.description['slideAnimation'] = 'Animation of tab changes in tabbed dialogs (yes, no).'; this.slideAnimation = true; this.description['setFocus'] = 'Automatically set focus to the first control in dialogs (yes, no).\n\n' + 'Not recommended for devices without physical keyboard.'; this.setFocus = false; this.description['showNotifications'] = 'Show popup notifications (yes, no).'; this.showNotifications = true; this.description['dupeBadges'] = 'Show badges with duplicate info in downloads and history (yes, no).'; this.dupeBadges = false; this.description['rowSelect'] = 'Select records by clicking on any part of the row, not just on the check mark (yes, no).'; this.rowSelect = false; this.description['windowTitle'] = 'Window-title for browser.\n\n' + 'The following variables can be used within placeholders to insert current data:\n' + ' COUNT - number of items in queue;\n' + ' SPEED - current download speed.\n' + ' TIME - remaining time;\n' + ' PAUSE - "download paused"-indicator.\n\n' + 'To form a placeholder surround variable with percent-characters, for example: %COUNT%.\n\n' + 'To improve formating there is a special syntax. If variable value is empty or null then nothing is inserted:\n' + '%(VARNAME)% - show variable value inside parenthesis;\n' + '%[VARNAME]% - show variable value inside square brackets;\n' + '%VARNAME-% - append hyphen to variable value;\n' + '%(VARNAME-)% - show variable value with hyphen inside parenthesis;\n' + '%[VARNAME-]% - show variable value with hyphen inside square brackets.\n\n' + 'Examples:\n' + ' "%(COUNT-)% NZBGet" - show number of downloads in parenthesis followed by a hyphen; don\'t show "(0) - " if queue is empty;\n' + ' "%PAUSE% %(COUNT-)% NZBGet" - as above but also show pause-indicator if paused;\n' + ' "%[COUNT]% %SPEED-% NZBGet" - show number of downloads and speed if not null (default setting).'; this.windowTitle = '%[COUNT]% %SPEED-% NZBGet'; this.description['refreshRetries'] = 'Number of refresh attempts if a communication error occurs (0-99).\n\n' + 'If all attempts fail, an error is displayed and the automatic refresh stops.' this.refreshRetries = 4; // Time zone correction in hours. // You shouldn't require this unless you can't set the time zone on your computer/device properly. this.timeZoneCorrection = 0; // Default refresh interval. // The choosen interval is saved in web-browser and then restored. // The default value sets the interval on first use only. this.refreshInterval = 1; // URL for communication with NZBGet via JSON-RPC this.rpcUrl = './jsonrpc'; /*** No user configurable settings below this line (do not edit) *************/ // Current state this.miniTheme = false; this.showEditButtons = true; this.connectionError = false; this.load = function() { this.refreshInterval = parseFloat(this.read('RefreshInterval', this.refreshInterval)); this.refreshAnimation = this.read('RefreshAnimation', this.refreshAnimation) === 'true'; this.activityAnimation = this.read('ActivityAnimation', this.activityAnimation) === 'true'; this.slideAnimation = this.read('SlideAnimation', this.slideAnimation) === 'true'; this.setFocus = this.read('SetFocus', this.setFocus) === 'true'; this.showNotifications = this.read('ShowNotifications', this.showNotifications) === 'true'; this.dupeBadges = this.read('DupeBadges', this.dupeBadges) === 'true'; this.rowSelect = this.read('RowSelect', this.rowSelect) === 'true'; this.windowTitle = this.read('WindowTitle', this.windowTitle); this.refreshRetries = parseFloat(this.read('RefreshRetries', this.refreshRetries)); } this.save = function() { this.write('RefreshInterval', this.refreshInterval); this.write('RefreshAnimation', this.refreshAnimation); this.write('ActivityAnimation', this.activityAnimation); this.write('SlideAnimation', this.slideAnimation); this.write('SetFocus', this.setFocus); this.write('ShowNotifications', this.showNotifications); this.write('DupeBadges', this.dupeBadges); this.write('RowSelect', this.rowSelect); this.write('WindowTitle', this.windowTitle); this.write('RefreshRetries', this.refreshRetries); } this.read = function(key, def) { var v = localStorage.getItem(key); if (v === null) { return def.toString(); } else { return v; } } this.write = function(key, value) { localStorage.setItem(key, value); } }(jQuery)); /*** START WEB-APPLICATION ***********************************************************/ $(document).ready(function() { Frontend.init(); }); /*** FRONTEND MAIN PAGE ***********************************************************/ var Frontend = (new function($) { 'use strict'; // State var initialized = false; var firstLoad = true; var mobileSafari = false; var scrollbarWidth = 0; var switchingTheme = false; var activeTab = 'Downloads'; var lastTab = ''; var lastMenu = $(); this.init = function() { window.onerror = error; if (!checkBrowser()) { return; } $('#FirstUpdateInfo').show(); UISettings.load(); Refresher.init(); initControls(); switchTheme(); windowResized(); Options.init(); Status.init(); Downloads.init({ updateTabInfo: updateTabInfo }); 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(); ExecScriptDialog.init(); AlertDialog.init(); ScriptListDialog.init(); RestoreSettingsDialog.init(); LimitDialog.init(); DownloadsEditDialog.init(); DownloadsMultiDialog.init(); DownloadsMergeDialog.init(); DownloadsSplitDialog.init(); HistoryEditDialog.init(); PurgeHistoryDialog.init(); $(window).resize(windowResized); initialized = true; authorize(); } function initControls() { mobileSafari = $.browser.safari && navigator.userAgent.toLowerCase().match(/(iphone|ipod|ipad)/) != null; scrollbarWidth = calcScrollbarWidth(); var FadeMainTabs = !$.browser.opera; if (!FadeMainTabs) { $('#DownloadsTab').removeClass('fade').removeClass('in'); } $('#Navbar a[data-toggle="tab"]').on('show', beforeTabShow); $('#Navbar a[data-toggle="tab"]').on('shown', afterTabShow); setupSearch(); $('li > a:has(table)').addClass('has-table'); $(document).on('keydown', keyDown); $(window).scroll(windowScrolled); } function checkBrowser() { if ($.browser.msie && parseInt($.browser.version, 10) < 9) { $('#FirstUpdateInfo').hide(); $('#UnsupportedBrowserIE8Alert').show(); return false; } return true; } function error(message, source, lineno) { if (source == '') { // ignore false errors without source information (sometimes happen in Safari) return false; } $('#FirstUpdateInfo').hide(); $('#ErrorAlert-title').text('Error in ' + source + ' (line ' + lineno + ')'); $('#ErrorAlert-text').text(message); $('#ErrorAlert').show(); if (Refresher) { Refresher.pause(); } return false; } this.loadCompleted = function() { Downloads.redraw(); Status.redraw(); Messages.redraw(); History.redraw(); if (firstLoad) { Feeds.redraw(); $('#FirstUpdateInfo').hide(); $('#Navbar').show(); $('#MainTabContent').show(); $('#version').text(Options.option('Version')); selectInitialTab(); windowResized(); firstLoad = false; } } function selectInitialTab() { var location = window.location.toString(); var link = null; if (location.indexOf('#downloads') > -1) link = 'DownloadsTabLink'; else if (location.indexOf('#history') > -1) link = 'HistoryTabLink'; else if (location.indexOf('#messages') > -1) link = 'MessagesTabLink'; else if (location.indexOf('#settings') > -1) link = 'ConfigTabLink'; if (link) { $('#DownloadsTab').removeClass('fade'); $('#' + link).click(); $('#DownloadsTab').addClass('fade'); } } function beforeTabShow(e) { 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(); $('#' + activeTab + 'Table_filter, #' + activeTab + 'Table_clearfilter').show(); switch (activeTab) { case 'Config': Config.show(); break; case 'Messages': Messages.show(); break; case 'History': History.show(); break; } FilterMenu.setTab(activeTab); } function afterTabShow(e) { switch (lastTab) { case 'Config': Config.hide(); break; case 'Messages': Messages.hide(); break; case 'History': History.hide(); break; } switch (activeTab) { case 'Config': Config.shown(); break; } } function setupSearch() { $('.navbar-search .search-query').on('focus', function() { $(this).next().removeClass('icon-remove-white').addClass('icon-remove'); $('#SearchBlock_Caret').addClass('focused'); }); $('.navbar-search .search-query').on('blur', function() { $(this).next().removeClass('icon-remove').addClass('icon-remove-white'); $('#SearchBlock_Caret').removeClass('focused'); }); $('.navbar-search').show(); beforeTabShow({target: $('#DownloadsTabLink')}); } function keyDown(e) { var key = Util.keyName(e); var modals = $('.modal:visible'); if (modals.length > 0) { if (key === 'Enter' && !Util.wantsReturn(e.target)) { var primaryButton = $('.btn-primary:visible', modals.last()); if (primaryButton.length === 1) { primaryButton.click(); } return false; } return; } var filterBox = $('#DownloadsTable_filter, #HistoryTable_filter, #MessagesTable_filter, #ConfigTable_filter'); if (filterBox.is(':focus') && (key === 'Escape' || key === 'Enter')) { filterBox.blur(); return false; } if (!(Util.isInputControl(e.target) && $(e.target).is(':visible'))) { switch (activeTab) { case 'Downloads': if (Downloads.processShortcut(key)) return false; case 'History': if (History.processShortcut(key)) return false; case 'Messages': if (Messages.processShortcut(key)) return false; case 'Config': if (Config.processShortcut(key)) return false; } switch (key) { case 'Shift+D': $('#DownloadsTabLink').click(); return false; case 'Shift+H': $('#HistoryTabLink').click(); return false; case 'Shift+M': $('#MessagesTabLink').click(); return false; case 'Shift+S': $('#ConfigTabLink').click(); return false; case 'Shift+L': $('#StatusSpeed').click(); return false; case 'Shift+A': $('#StatusTime').click(); return false; case 'Shift+R': $('#RefreshButton').click(); return false; case 'Shift+P': $('#PlayPauseButton').click(); return false; case 'Shift+T': $('#ScheduledPauseButton').click(); return false; } } } function windowScrolled() { $('body').toggleClass('scrolled', $(window).scrollTop() > 0 && !UISettings.miniTheme); } function calcScrollbarWidth() { var div = $('
'); // Append our div, do our calculation and then remove it $('body').append(div); var w1 = $('div', div).innerWidth(); div.css('overflow-y', 'scroll'); var w2 = $('div', div).innerWidth(); $(div).remove(); return (w1 - w2); } function windowResized() { var oldMiniTheme = UISettings.miniTheme; UISettings.miniTheme = $(window).width() < 560; if (oldMiniTheme !== UISettings.miniTheme) { switchTheme(); } resizeNavbar(); alignPopupMenu('#PlayMenu'); alignPopupMenu('#RefreshMenu'); alignPopupMenu('#RssMenu'); alignPopupMenu('#StatDialog_MonthMenu', true); alignCenterDialogs(); if (initialized) { Downloads.resize(); } } function alignPopupMenu(menu, right) { var center = UISettings.miniTheme; var $elem = $(menu); if (center) { $elem.removeClass('pull-right'); var top = ($(window).height() - $elem.outerHeight())/2; top = top > 0 ? top : 0; var off = $elem.parent().offset(); top -= off.top; var left = ($(window).width() - $elem.outerWidth())/2; left -= off.left; $elem.css({ left: left, top: top, right: 'inherit' }); } else { $elem.css({ left: '', 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'); } } } this.alignPopupMenu = alignPopupMenu; function showPopupMenu(menu, anchor, rect) { var $menu = $(menu); if ($menu.is(':visible')) { $menu.hide(); return; } lastMenu.hide(); lastMenu = $menu; $menu.css({ left: rect.left + (anchor.indexOf('right') > -1 ? rect.width - $menu.outerWidth() : 0), top: rect.top + (anchor.indexOf('top') > -1 ? - $menu.outerHeight() : rect.height) }); $menu.show(); if ($menu.offset().top < $(window).scrollTop()) { $menu.css({ top: rect.top + rect.height }); } if ($menu.offset().left + $menu.outerWidth() > $(window).width()) { $menu.css({ left: $(window).width() - $menu.outerWidth() }); } if ($menu.offset().top + $menu.outerHeight() > $(window).height() + $(window).scrollTop()) { $menu.css({ top: rect.top - $menu.outerHeight() }); } if ($menu.offset().top < $(window).scrollTop()) { $menu.css({ top: $(window).scrollTop() }); } if (UISettings.miniTheme) { alignPopupMenu($menu); } $('html').on('click.PopupMenu', function () { $menu.hide(); $('html').off('click.PopupMenu'); }); } this.showPopupMenu = showPopupMenu; function alignCenterDialogs() { $.each($('.modal-center'), function(index, element) { Util.centerDialog(element, true); }); } function resizeNavbar() { var ScrollDelta = scrollbarWidth; if ($(document).height() > $(window).height()) { // scrollbar is already visible, not need to acount on it ScrollDelta = 0; } if (UISettings.miniTheme) { var w = $('#NavbarContainer').width() - $('#RefreshBlockPhone').outerWidth() - ScrollDelta; var $btns = $('#Navbar ul.nav > li'); var buttonWidth = w / $btns.length; $btns.css('min-width', buttonWidth + 'px'); $('#NavLinks').css('margin-left', 0); $('body').toggleClass('navfixed', false); } else { var InfoBlockMargin = 10; var w = $('#SearchBlock').position().left - $('#InfoBlock').position().left - $('#InfoBlock').width() - InfoBlockMargin * 2 - ScrollDelta; var n = $('#NavLinks').width(); var offset = (w - n) / 2; var fixed = true; if (offset < 0) { w = $('#NavbarContainer').width() - ScrollDelta; offset = (w - n) / 2; fixed = false; } offset = offset > 0 ? offset : 0; $('#NavLinks').css('margin-left', offset); // as of Aug 2012 Mobile Safari does not support "position:fixed" $('body').toggleClass('navfixed', fixed && !mobileSafari); if (switchingTheme) { $('#Navbar ul.nav > li').css('min-width', ''); } } } function updateTabInfo(control, stat) { 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); if (control.lastOuterWidth !== control.outerWidth()) { resizeNavbar(); control.lastOuterWidth = control.outerWidth(); } } function switchTheme() { switchingTheme = true; $('#DownloadsTable tbody').empty(); $('#HistoryTable tbody').empty(); $('#MessagesTable tbody').empty(); $('body').toggleClass('phone', UISettings.miniTheme); $('.datatable').toggleClass('table-bordered', !UISettings.miniTheme); $('#DownloadsTable').toggleClass('table-check', !UISettings.miniTheme || UISettings.showEditButtons); $('#HistoryTable').toggleClass('table-check', !UISettings.miniTheme || UISettings.showEditButtons); alignPopupMenu('#PlayMenu'); alignPopupMenu('#RefreshMenu'); alignPopupMenu('#RssMenu'); alignPopupMenu('#StatDialog_MonthMenu', true); if (UISettings.miniTheme) { $('#RefreshBlock').appendTo($('#RefreshBlockPhone')); $('#DownloadsRecordsPerPageBlock').appendTo($('#DownloadsRecordsPerPageBlockPhone')); $('#HistoryRecordsPerPageBlock').appendTo($('#HistoryRecordsPerPageBlockPhone')); $('#MessagesRecordsPerPageBlock').appendTo($('#MessagesRecordsPerPageBlockPhone')); $('#StatDialog_MonthMenu').appendTo($('#StatDialog_MonthBlockPhone')); } else { $('#RefreshBlock').appendTo($('#RefreshBlockDesktop')); $('#DownloadsRecordsPerPageBlock').appendTo($('#DownloadsTableTopBlock')); $('#HistoryRecordsPerPageBlock').appendTo($('#HistoryTableTopBlock')); $('#MessagesRecordsPerPageBlock').appendTo($('#MessagesTableTopBlock')); $('#StatDialog_MonthMenu').appendTo($('#StatDialog_MonthBlockTop')); } if (initialized && !firstLoad) { Downloads.redraw(); History.redraw(); Messages.redraw(); Downloads.applyTheme(); History.applyTheme(); Messages.applyTheme(); windowResized(); } switchingTheme = false; } function authorize() { var formAuth = document.cookie.indexOf('Auth-Type=form') > -1; if (!formAuth) { Refresher.update(); return; } function sendAuth() { var username = $('#LoginDialog_Username').val(); var password = $('#LoginDialog_Password').val(); var headers = [{name: 'X-Authorization', value: 'Basic ' + window.btoa(username + ':' + password)}]; RPC.call('version', [], function(version) { $('#LoginDialog').modal('hide'); // reloading of page is needed for certain browsers to force save-password-dialog document.location.reload(); }, function(err, result) { $('#LoginDialog_PasswordBlock').removeClass('last-group'); $('#LoginDialog_Error').show(); if (!$('#LoginDialog_Password').is(":focus")) { $('#LoginDialog_Username').focus(); } }, 0, headers); } $('#LoginDialog_Form').submit(function(e) { if ($('#LoginDialog_Error').is(":visible")) { $('#LoginDialog_Error').hide(); $('#LoginDialog_PasswordBlock').addClass('last-group'); setTimeout(sendAuth, 500); } else { setTimeout(sendAuth, 0); } return false; }); // try RPC call, it may work without extra authorization RPC.call('version', [], Refresher.update, function() { $('#LoginDialog').modal({backdrop: 'static'}); $('#LoginDialog_Username').focus(); }, 10000); } }(jQuery)); /*** REFRESH CONTROL *********************************************************/ var Refresher = (new function($) { 'use strict'; // State var loadQueue; var firstLoad = true; var secondsToUpdate = -1; var refreshTimer = 0; var indicatorTimer = 0; var indicatorFrame=0; var refreshPaused = 0; var refreshing = false; var refreshNeeded = false; var refreshErrors = 0; this.init = function() { RPC.rpcUrl = UISettings.rpcUrl; RPC.connectErrorMessage = 'Cannot establish connection to NZBGet.' RPC.defaultFailureCallback = rpcFailure; RPC.next = loadNext; $('#RefreshMenu li a').click(refreshIntervalClick); $('#RefreshButton').click(refreshClick); updateRefreshMenu(); } function refresh() { UISettings.connectionError = false; $('#ErrorAlert').hide(); refreshStarted(); loadQueue = new Array( function() { Options.update(); }, function() { Status.update(); }, function() { Downloads.update(); }, function() { Messages.update(); }, function() { History.update(); }); if (!firstLoad) { // query NZBGet configuration only on first refresh loadQueue.shift(); } loadNext(); } function loadNext() { if (loadQueue.length > 0) { var nextStep = loadQueue[0]; loadQueue.shift(); nextStep(); } else { firstLoad = false; Frontend.loadCompleted(); refreshCompleted(); } } function rpcFailure(res, result) { // If a communication error occurs during status refresh we retry: // first attempt is made immediately, other attempts are made after defined refresh interval if (refreshing && !(result && result.error)) { refreshErrors = refreshErrors + 1; if (refreshErrors === 1 && refreshErrors <= UISettings.refreshRetries) { refresh(); return; } else if (refreshErrors <= UISettings.refreshRetries) { $('#RefreshError').show(); scheduleNextRefresh(); return; } } Refresher.pause(); UISettings.connectionError = true; $('#FirstUpdateInfo').hide(); $('#ErrorAlert-text').html(res); $('#ErrorAlert').show(); $('#RefreshError').hide(); if (Status.status) { // stop animations Status.redraw(); } $('html, body').animate({scrollTop: 0 }, 400); }; function refreshStarted() { clearTimeout(refreshTimer); refreshPaused = 0; refreshing = true; refreshNeeded = false; refreshAnimationShow(); } function refreshCompleted() { refreshing = false; refreshErrors = 0; $('#RefreshError').hide(); scheduleNextRefresh(); } this.isPaused = function() { return refreshPaused > 0; } this.pause = function() { clearTimeout(refreshTimer); refreshPaused++; } this.resume = function(wantUpdate) { refreshPaused--; if (refreshPaused === 0 && wantUpdate) { this.update(); } else if (refreshPaused === 0 && UISettings.refreshInterval > 0) { countSeconds(); } } this.update = function() { refreshNeeded = true; refreshPaused = 0; if (!refreshing) { scheduleNextRefresh(); } } function refreshClick() { if (indicatorFrame > 10) { // force animation restart indicatorFrame = 0; } refreshErrors = 0; refresh(); } function scheduleNextRefresh() { clearTimeout(refreshTimer); secondsToUpdate = refreshNeeded ? 0 : UISettings.refreshInterval; if (secondsToUpdate > 0 || refreshNeeded) { secondsToUpdate += 0.1; countSeconds(); } } function countSeconds() { if (refreshPaused > 0) { return; } secondsToUpdate -= 0.1; if (secondsToUpdate <= 0) { refresh(); } else { refreshTimer = setTimeout(countSeconds, 100); } } function refreshAnimationShow() { if (UISettings.refreshAnimation && indicatorTimer === 0) { refreshAnimationFrame(); } } function refreshAnimationFrame() { // animate next frame indicatorFrame++; if (indicatorFrame === 20) { indicatorFrame = 0; } var f = indicatorFrame <= 10 ? indicatorFrame : 0; var degree = 360 * f / 10; $('#RefreshAnimation').css({ '-webkit-transform': 'rotate(' + degree + 'deg)', '-moz-transform': 'rotate(' + degree + 'deg)', '-ms-transform': 'rotate(' + degree + 'deg)', '-o-transform': 'rotate(' + degree + 'deg)', 'transform': 'rotate(' + degree + 'deg)' }); if ((!refreshing && indicatorFrame === 0 && (UISettings.refreshInterval === 0 || UISettings.refreshInterval > 1 || !UISettings.refreshAnimation)) || UISettings.connectionError) { indicatorTimer = 0; } else { // schedule next frame update indicatorTimer = setTimeout(refreshAnimationFrame, 100); } } function refreshIntervalClick() { var data = $(this).parent().attr('data'); UISettings.refreshInterval = parseFloat(data); scheduleNextRefresh(); updateRefreshMenu(); UISettings.save(); if (UISettings.refreshInterval === 0) { // stop animation Status.redraw(); } } function updateRefreshMenu() { Util.setMenuMark($('#RefreshMenu'), UISettings.refreshInterval); } }(jQuery)); function TODO(text) { $('#Notif_NotImplemented_Param').html(text === undefined ? '' : ': ' + text); PopupNotification.show('#Notif_NotImplemented'); } /*** CONFIRMATION DIALOG *****************************************************/ var ConfirmDialog = (new function($) { 'use strict'; // Controls var $ConfirmDialog; // State var actionCallback; var confirmed = false; this.init = function() { $ConfirmDialog = $('#ConfirmDialog'); $ConfirmDialog.on('hidden', hidden); $('#ConfirmDialog_OK').click(click); } this.showModal = function(id, _actionCallback, initCallback, selCount) { $('#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); if (selCount > 1) { var html = $('#ConfirmDialog_Text').html(); html = html.replace(/selected/g, selCount + ' selected'); $('#ConfirmDialog_Text').html(html); } actionCallback = _actionCallback; if (initCallback) { initCallback($ConfirmDialog); } Util.centerDialog($ConfirmDialog, true); confirmed = false; $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() { if (confirmed) { actionCallback($ConfirmDialog); } // confirm dialog copies data from other nodes // the copied DOM nodes must be destroyed $('#ConfirmDialog_Title').empty(); $('#ConfirmDialog_Text').empty(); $('#ConfirmDialog_OK').empty(); } function click(event) { event.preventDefault(); // avoid scrolling confirmed = true; $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 PopupNotification = (new function($) { 'use strict'; this.show = function(alert, completeFunc) { if (UISettings.showNotifications || $(alert).hasClass('alert-error')) { $(alert).animate({'opacity':'toggle'}); var duration = $(alert).attr('data-duration'); if (duration == null) { duration = 1000; } window.setTimeout(function() { $(alert).animate({'opacity':'toggle'}, completeFunc); }, duration); } else if (completeFunc) { completeFunc(); } } }(jQuery)); nzbget-19.1/webui/feed.js0000644000175000017500000006116013130203062015117 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2017 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, see . */ /* * 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 = $('span', item); a.text(name !== '' ? name : 'Feed' + i); a.attr('data-id', i); a.click(viewFeed); var im = $('button', item); im.click(fetchFeed); im.attr('data-id', i); insertPos.before(item); } } Util.show('#RssMenuBlock', $('.feed-menu', menu).length > 0); } function viewFeed() { var id = parseInt($(this).attr('data-id')); FeedDialog.showModal(id); } function fetchFeed() { var id = parseInt($(this).attr('data-id')); RPC.call('fetchfeed', [id], function() { PopupNotification.show('#Notif_Feeds_Fetch'); }); } this.fetchAll = function() { RPC.call('fetchfeed', [0], function() { PopupNotification.show('#Notif_Feeds_Fetch'); }); } }(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', rowSelect: UISettings.rowSelect, pageSize: pageSize, renderCellCallback: itemsTableRenderCellCallback }); $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, backlog, pauseNzb, category, priority, interval, feedscript) { Refresher.pause(); enableAllButtons(); $FeedDialog.restoreTab(); $('#FeedDialog_ItemTable_filter').val(''); $('#FeedDialog_ItemTable_pagerBlock').hide(); $ItemTable.fasttable('update', []); $ItemTable.fasttable('applyFilter', ''); 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 (name === undefined) { 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 feedBacklog = backlog === 'yes'; var feedPauseNzb = pauseNzb === 'yes'; var feedCategory = category; var feedPriority = parseInt(priority); var feedInterval = parseInt(interval); var feedScript = feedscript; RPC.call('previewfeed', [id, name, url, filter, feedBacklog, feedPauseNzb, feedCategory, feedPriority, feedInterval, feedScript, false, 0, ''], itemsLoaded, feedFailure); } if (!UISettings.miniTheme) { $('#FeedDialog_TableBlock').removeClass('modal-inner-scroll'); $('#FeedDialog_TableBlock').css('top', ''); $('#FeedDialog_TableBlock').css('top', $('#FeedDialog_TableBlock').position().top); $('#FeedDialog_TableBlock').addClass('modal-inner-scroll'); } } 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; if (item.Category !== '') { info += ' ' + item.Category + ''; } info += ' ' + age + '' + ' ' + size + ''; fields = [info]; } var item = { id: item.URL, item: item, fields: fields, data: { status: item.Status, name: itemName, category: item.Category, age: age, size: size, _search: true } }; 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'); var checkedCount = $ItemTable.fasttable('checkedCount'); if (checkedCount === 0) { PopupNotification.show('#Notif_FeedDialog_Select'); return; } disableAllButtons(); var fetchItems = []; for (var i = 0; i < items.length; i++) { var item = items[i]; if (checkedRows[item.URL]) { 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'); PopupNotification.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 feedId; var feedName; var feedUrl; var feedFilter; var feedBacklog; var feedPauseNzb; var feedCategory; var feedPriority; var feedInterval; var feedScript; 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', headerCheck: '', pageSize: pageSize, 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(id, name, url, filter, backlog, pauseNzb, category, priority, interval, feedscript, _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'); feedId = id; feedName = name; feedUrl = url; feedFilter = filter; feedBacklog = backlog === 'yes'; feedPauseNzb = pauseNzb === 'yes'; feedCategory = category; feedPriority = parseInt(priority); feedInterval = parseInt(interval); feedScript = feedscript; cacheId = '' + Math.random()*10000000; cacheTimeSec = 60*10; // 10 minutes if (url !== '') { RPC.call('previewfeed', [feedId, name, url, filter, feedBacklog, feedPauseNzb, feedCategory, feedPriority, feedInterval, feedScript, 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', [feedId, feedName, feedUrl, filter, feedBacklog, feedPauseNzb, feedCategory, feedPriority, feedInterval, feedScript, 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, data: { match: item.Match, rule: item.Rule, title: itemName, category: item.Category, age: age, size: size, _search: true } }; 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)); nzbget-19.1/webui/img/0000755000175000017500000000000013130203062014426 5ustar andreasandreasnzbget-19.1/webui/img/favicon.ico0000644000175000017500000000030613130203062016546 0ustar andreasandreas°( @@@@àààÿÿÆ{´÷¥7¾cÇ÷ÿÿÿÿK¸J$Y8h¤K¸nzbget-19.1/webui/img/icons-2x.png0000644000175000017500000013116613130203062016606 0ustar andreasandreas‰PNG  IHDRxXØŽbKGDÿÿÿ ½§“ pHYs  šœtIMEà   <%Š IDATxÚìÝ{”Õ÷ÿ÷îÆn@n"x1I M;Þ#9Fƒƒñö(ú“htt™Ç™8ñ£Áu4hÔŒF¸Ìh4úDô§‰Ñ‘`ìÁèe€6C ö$ (7ÛFЦ[º÷k®>œ[wŸÓ]UçóZë,à\ê°«NUíúÖw7ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆÄÑ*é?j @ÍšFzn(pp °?0Ø¿ô ð°ØR³¦qG®å¨ýj¿ˆ$Ÿ¼"""""""ý¤qB rj*€±À—€£€ÏÃþ#»€íÀûÀŸ€¥À€wCË1q ò©ýåÝ~)x¥ÌÎ^žÓꉥJ ]«A¤lÏãã2¼¶¸x&æmœÜL,Á²×WÏë§$=iÁ½éÀiÀ±þ¸W•gmþXøð°¨fMãæôe«ýj¿ˆ$›¼’tS€7C'ÆW¿VÅ­!ÖÚ!Àéþ1¨†;F`µ¿x{Þ³3ÛîràNÜÝêžÚè··.jãïÏÀµè&H9ZGæànà#`$ñ¾ ´]Âå¯ÓOI$ZÒ‚{SÙÀ™Àá=\äZà—Àã5kÒ¿CíWûE$¹âà=øzNÉä5`ZÚsíÀ\àö84ÀZ[… vÞì/`óin2Æ´ÅxÛíÀ°uQ+w3ãx`EµûB`Á­ÍÀ¥¸ ÝyOœÜ“vîïçW–I`&ð0p>î¦e¹ìÿù ZÞÆrí÷‹$RxóCò¿ üðu`@/½—Éy/ð»š5Q ò©ýåÝ~QG/ð>pP– ¼ÃIÐ0^kí4à1ߦ1wð™ó€ú ËŒ1‹Ëô÷}nHg&íþb°-âÛ"ð,09ôôNàe`+î.íx\ÖÏÉt ˆ®Î6ƬÖÅ®.j“p8ô®Æy·–I»?`ï;é7- yOäËplÂM@’d3ý9¡ÊŸãO^/£ý?ÉÇyxEÊHZæætàû¸R-Å´¸£fMã¢ôïTûÕ~Iž¸vôr˜Fà ŽÇÿjÆÚã€W€A¡§o7Æ\—ã3„žÚL7ƼUf¿í‘¸!ÝÙ2^_ÄÕ6ŠòöŸü*Ô†UÀ-Às™2s}¦ïYÀÀ‘¡€ÇƘ×¶Ÿ'ñX7x›ÒÑÝ»IçßC=.°U5ym¿iãß}± ‰éd0ßµøÊ%È«oy÷ûE%ðü¹¾_ ÏójÖ4.Oÿnµ_í‘d©Ð*ˆh/ßÚ)ÀKt î̱Öþ0Ëg.Ä Û ,´ÖÖ”Ù*¼ìÁÝ6Üd#QÞþAænІ[/cžÊVvÁÓfŒy ø¢?þóÏúåI´5áø¥ Z¶øß}ê3§€»ô“‘2’)¸‹ï#¼ÄÞ¥ˆ$~Ö÷Á9F’m6ð€Zñà'ÔšMi“N¾|×گö‹H‚)À]}r©½‚¼¾,Ã#¸™æÓÆ•y(Ó€ r¼þ n˜w$ùLÜgéÌä¼È3×Óžö¾×¬µ¯¥ÞÓnŒ™ \ÚþÏúåÆÕ¸Ì£ðãø„ýn¯~ œT‚ ñõÀ ¸ú¤s²¾¾\¬S…”lÁÝÀäM‚¹”®ôÌ&àj­âD üW‹|;p‰VItùº«˜ŽËÜܧ„_77i×ôÆ 5QÈÞìaû[ý1ò/þ±Õ?§öǬý"R: ðF׸<¯ï òZkÏž sp70¾LÖ[]KT¤ ²$£ì;tÖܽÕóx–÷MËuAï?dòN¾ãíú6nøjøñû„ývçoâê*×R¼‰ÄÇøŽàk¸úÜIñà8.$ÁfÐYs7— È«ý!¾`ï›™éÓèœ? wó.ßgÕ*N´`RMëÿ¤Uy‡úý¹/®ÑÆûïÃöïö}ãßøãØ¿øÇÏüskOÕþص_DJ`€VAd]Eþ¬Û9¾ôÂLrw¡3Зt×¹ÊÌ%Âíµƒ|Àe÷6}pž_'ßµÖÞkŒ‰ã,ãGiÏMôÛ‚ Ä/ÎÅeÎéÅ2¾Ôá‚D%l²ÝÂÕI’“ýï{P7Ž! q5ykõ%RzÀPh›¿®ÕS¶.Ç•çj]üVKäMŽíÃï;Öçúµ¿ø#n²°ÿÖÐYjl0—;Ã÷¡©ý±i¿ˆ”€2x#Êg_þ]o=‹ü™=·cî-ƒÕ67ÁX6oE¼ §ÓYšan¶z»Ýøµã‚ÚøåžÓm›)ƒ·!¡¿ãÑþ‚ýb\ ÷º,£w“è\¹’E$/¸8ˆîÁDâàd\Y†îþ®•É›\ªÃ,{uóüãÁ´ëº§•Z=ÑÕ8¡f(îæû¸>üÚq@ÿî8´ÿS\pó \¶ê"\p²É?Öûç~æß³‚Ü™¬IhÿF\€w§ÿ{,Û/"¥£o”{mÆr»çRº ¬Še¦ÿ³W/µž÷Ë /?n2Õà­Mðo¹ xÀ?îÆ «*tr´&ÿþûý>ñùoÅÝѸr "I¢gÁÝÀp\Àïh­ÊÄPfÉÄÏùë9ëûFßÐ5^ä|¾ûgUþ;ˆIû7øãÚ¯q™«6Ë>ðð.ËõÝ„¶ÿ\È,àÇÀ½Àùþ¹µ1l¿ˆ”HOþ3€?³w¶^ø‘KsŽÏ½í—½ òÞkŒ)—I5.ȳm_¤³>Y”Õù?_-V)¿œWÓ–7å”Áv.›w ® A¾ÉWãJ;,ñŸûvÏ.$Þu¦EÀw_ ÷éÃý1 NAÞ!þ‚¶»ý¾ð…~ú# YŒªÃ,¹~g¤=w ®F³&Rжqµ±ûÚ!þ»£Þþ]¸ŒÔE¸f>ký{ÿè?›´ö®~€áw‘ÿû:ƒ¼qj¿ˆ”H¼‘»†joÔÃ,¯y2Æ\U&¿ãáÀ9^oà Wƒ`¨Îª"/wUÚò%>NÄeâ6₼ ²¼oAà{p¢rs'nh»H ñ} b•NîIG£f§¿h-Vøí¸25qÖÓ:ÌIòÚ^>ÊÁ³ií}øçÐú“èÚߣûãšiÿ´ð?¸ÌU[àñbï+ïHXû+€“€3q“¥ öñþ¹“ý{âÔ~)‘(xG—xù± pù }‰1æ[1ÿmŽîFÇç‡À˜¯ßKþÌÇ(]àCç Ùù{4™-L{[[ÚòãæTòÏ~œš ãt›ß÷§øNÜiÀíiï Ê8 éßßFù©Ä•µ‰£¸®3Ó ê*dxLŠÙ:¨÷DzÞy·ã&[ã߃ê0ËÁ¸OMÚ~ôm¥íûi•ÅÆ```?|ï@`ß´ÿc`3¹ƒ•évøÏìLXû+/ûãAE†cÄ4ÿž8µ_DJDõ™bÂZ{nr!޶Ö~'ÆÍMá†i¾Mþ’GãfΦ‰½ƒaQ¶ÝÿYÈŽ§r¼–>”stÚò㦽Äï²KpC¯â&N™‡›xm¶ÿ}_\+ǰÎI(f'l=¢øg쉔»zܰóžy·ãnòÅ9¸«:̰¼ÀGF7ÜpÃÔn¸ajÌ÷…e¸,½·éÁ%ð9àÒÞûà·:|ˆˆˆ”/xcÀwÃÝ+Ô=ÖÚ8Ößü <¬:1Ë{+q%7r­—¹¸ X\¬ ­‡œŒ1ç›ì¦gX¯áåÇÍBº74saBvÿ«põ“_¡3H?¸x7ÌêQ\íÙ{üë£ý~³¸²Ì—W¯ë¬‘X;µ ÊÆËô,È»Ü]ã¶—sféô;\v^0<ý?èLôJ5…'Vû´Êbår×J-•]þ»£Þþ}qµb»3òpn±! k»ïÛ¾t¤½ï}ÿZ{ÌÚ/"%2 ‚ÿ§õ”¶ŒBœ‚}= îî³ÖbŒ¹?&Í­ÂwG§¬âf }.íý߯ [Ïæ-ࡘí“où6M³Ö*ÆDkÖÚAtά½B‡½Ø¸x¬M/Ar³ÿóÜ„*é5¨ÇಷNFâ²~“îþîï’YCžc{.ÍžÛ„+UФU+/gSx Ú¸² q󤮋:€n¸!}"¢ôuÓ’Ðßÿïü9<Ä} xü ×%½øÐ¿OµwãáúgTÝvÿÝQoÿP\i’ À»ù~×5kiœP3ÁfhÛÿ²?&žIçäl›q#=~[³¦±=f푉bï\`k‰–½ 7”9zÜ Üg­½"&M¾‹Ì5ãªpCÓ/ =7†üA««‰ßõC0çi™ç„.ˆžé±ªÜjð>„ Þ¾LÎòž›udŸ`p²ïøÝí/“ìÕ8Û%¯ËŠxá”íPp7žà‚¼ùjŠ'!s7hG¹×a·Ýæâ²¹ƒkµ`„Ò :ƒ]Øíß îÆÉf\öe_{Ï_G½ý/_Á•)É©qBÍxÿÞÉ@uÛ¿—øqnäÞÏü~;°%†í‘‰bïãþ‘‹ÍÓ)Š=kíYÜ]BþáwXkÛ1QÎn«®ÈóúøÀî£þ﹆ ¼ˆ«ã7/úïAÀÖÚ§Œ1m½øUá†óã—ûbLw‰¶¿?JžÇ•fxº€};ßh‡qYñçâ2¹.Hàyl­o_9N*—TËp3F¿Bïfoà ó_ Uk üvü{×—‡Îà®Ê³$Ð~ðƒ÷˰Ùá² û¯S3\ëÜ7‰Ÿ-ÀŸüyªªûÒoûïŽCûÇ_¶áÊÖ¬MøÌÝñÀ×tÞ Kjûá¯Ú}ænÛ/"%¢¼Ñuùƒ»×cŽÁ MÎ箈··7Ä,Ÿ kñä<˜«â¸Ñ}I†»ý?'Òû¡õóürî.Fɇ~ò Ý«ÁûJLÛ¹Ê_¬= Ì,Ò2O÷Ç“Kp%@ xÃû[ .ð³Iše¸áö=ÍämÃe~*¸› A·-Ã1@Á]I²OpC³o ºŠÀ§þïÿIg°Gb¤fMãž[߇_»Xê¿;íß—Å:ø&n¢ÁCýüãÐÆ 5Óýk³ý{«ÞþCp7}ö‰kûE¤thDV¾l´«1wc®´Ö¶àjqf‡ÈM@#nâ´!½XÎÝÀêoû{q™–S€k¬µ«{’}m­½<ô›xË/W¢À¸w£Ø™¶Wà†¨ŸŠ '%Ûõ"TW:É–à‚¼ é^&oÜ}Q«0qÇÈ &o.¸{ îJòUâÊ6ÕÏøãᛸì>‰¯·€7èLÄ(µ7è¼Ñ—öjqó³| ø\yp“}W–`,ù3¡Õþhµ_DJ@ÞèºÈ_Ôf t^eŒé¬3Æ\k­m®Ïðþ\MÃ8xwGóYàÈ|¾ W(¶Œ1mÖÚ3€¥þ„þkíx`®1&oMakm%.swŽj+pFoJ=DÀ©t/o®~mÜÚx¹oÔôÆõ¸§%äy«¿Ð•dën· W²CÁÝx¹7Òht7>37t=ŸMÀu¸òN"qôý^Ç•ð’dØ€zÿ×Pg¶—ÖúïÚÃöïã_?8øØ?¿/nB±j,Û/"%  eŒYŒ øìL{éÊôànè3séœh!°8Õ§z´ÀQ=¼›KÿÌJ[ìí¿ˆ 2¯ç+­µçùºº{±ÖVYkÏVÒÜݜ旗„‹›R½?*N/ñòg$äù|†c$×>ÌwloÕ8y^«,væÑ½ànwDôËT‰HiœPcjÖ4v‹€_â&Ë+•Ýþ;Õ¬iìhœPcbÚþþ<ñÿMaÁMµ?bí‘ÒQ€7ÂŒ1¯ã2—–áî¼Í6ÆÜŸç3·Wâ{¯Ó1¯Æ°ù-¸áê—ø¿â-à¡mÿ%¸@w0½W\ÿkí³ÖÚŸXkçø?Ÿ>ð¯×ø÷¯Žñˉ»…t¯ó|2Ï‚^¬G&cY…á å%ÛMÏ@Ü}N«*–Æ•xù#µŠE$jjÖ4nÆM.þB ¿æàçþ»Ô~µ_D,®wplÛ$Ù‰+ÙP“ç}'ÀZ|>c÷R\†S!©M¸ìƟƼ,ƒÕ±NÒ~MÀ1Ä»Æv±öÓÍ÷$Åqì]¾¨ 7ÁÈ3eø;Lá7D“|¼OêyÀ–AEʆÏâ´þïÓïSü‘V €;jÖ4.JÿNµ_í‘ä‰kï¦,Ïo×&M¤U¸LÖ's¼çA:ÑŠ1¦Íó pAŒe¡ ùÿïgüë‡cŒypŠ7³p“v¡ÄΧ¼‚»ÙöƒM=xOR¤gò¶“üàn!ÇÃí$gòDé¹AZ"ñP³¦Ñ††Ëÿ¸7”¾eÆÚý²îðËŽ\pOí/ïö‹HiÄõNÿÀÀ˜Ðs«kÑð̤ 2YƒI&6↶ܤ‹ÛÄ9¸Þ ÝÝŠ›yú~­N‰©ó€{BǼfàOuó=I3 W’æ*Êc²½™ÀYއë«c¾¶Pº¼Á::,æ¿uy·áFù´#"±‘–É9øp&=Ÿxí/¸àÞã5k—§‡Ú¯ö‹Hri(—ˆˆˆˆô§¸zK°ìµ¸qŸ|/éA~‘²•ä;˜Ž›lùX¿ÏWåYD›?¼«¹º(¨¹‡àžÚ_Þí‘âQ€WDDDDDD¤Ÿ¤ù p(ð%\™ºÏ‡Ãþ#»påyÞþ,þl¨YÓØ‘¾Lµ_í‘äS€WDDDDDD¤5YÃA¹Æ 5C€ý}ýK›-5kwäZŽÚ¯ö‹Hò)À+"""""""e«qB 5k÷z.ŸLŸ ?'"Òà‘²Ô8¡fO@¶Ð n6áå(È+"}I^);Å îä‘þ ¯ˆˆˆˆˆˆˆ”•Rw òŠH_S€WDDDDDDDÊFx-v`7]ð òŠH©UhˆˆˆˆˆˆˆH9èë€k_’ED”Á+"""""""‰Wʲ Ù¨\ƒˆôeðŠˆˆˆˆˆˆHÙèËŒZeïŠH_P¯ˆˆˆˆˆˆˆ”…þ ¸*{WDJI¼""""""""""1¥¯ˆˆˆˆˆˆˆˆˆHL©Dƒˆˆˆˆˆˆˆ$^×ÃU™)eðŠˆˆˆˆˆˆˆˆˆÄ”¼""""""""""1¥¯ˆˆˆˆˆˆˆˆˆHL)À+"""""""""S ðŠˆˆˆˆˆˆˆˆˆÄ”¼""""""""""1¥¯ˆˆˆˆˆˆˆH7Õ¬iÔJ‘HP€WDDDDDDD¤›'Ôh%ˆH$(À+"""""""R eîŠHÔ­)ý•u« °ˆ”’2xEDDDDDDDDDbJ^‘˜R€WDDDDDDD¯qBM¿”J¨YÓ¨ ÙD¤¤à‘²Ñ—A^ÕÞ‘¾ IÖDDDDDDD¤,„³xKUþzE¤””Á+"""""""e¡¯Ë%(¸+"}A¼"""""""RVJ™É«Ì]ék ðŠˆˆˆˆˆˆHÙ)EWÁ]é ðŠˆˆˆˆˆˆHY*fWÁ]é/ ðŠˆˆˆˆˆˆHÙ »á l¡ÁÞLŸQpWDúš¼"""""""ý¨¾¾Þ¤R)zn(pp °?0Ø¿ô ð°Ø’J¥väZNÔÎZfæ×ÙÐsµ¿a~ÝŽ\ËQûE¤(À[&ª««'3€[[[ÛµF"k<ðàZ ­‡Ë¨.Φ•ÀƒÀUZ½"±r"p§ÿûµÀ«%þœˆH$Xk»ÇŒ1¯–òshïžþŸ1¦­‡ËÈØÿ3ƨÿõõõ&ÈÖ××Wc/GŸ†ýGvÛ÷?K?câä­µÌÉÚYËzÕþÐrL\‚œåÞ~)xË@uuu ð0x´µµõ­•Hï·Óà)àü,c"ðOô:p*°S«X$VÞòßä;÷¥üœˆH$Xk»ÇŒ1—òsýÜÖ.ý?cÌù=XFÖþŸ1Fý¿ˆK îLNŽÆá’7riÖoøßÁ¢T*µ9}ÙQ•Ü,Jûæ×mN_¶Ú/"I˯µö3À1À ø ðN¾»ôÖÚ‡|Çé•(¶«ººº ¸¸ØwòzâùÖÖÖ3BˬÄÝÑ›zÏý­­­Wêç)áÎ}`,°±Ë8X„ ä‡;÷§-1YÅØrÙ< ÜBÏ3¤ã|Á<¸¨–cnÑ®íMÖÃsvO?'"•óU—ã˜1Æ”òsýØÎŒý?cÌÆn,#kÿÏÓ¢_S´¥w§³3Ã{¸ÈµÀ/ÇS©TCúwDMZp³¨ío˜_×þj¿ˆ$Yl.ú¬µÀ¿ƒÓúo„úo¯ÿdŒY”öùû¿·Ö^QQQñ¯Qlcuuõ9½\L;PÝÚÚÚî3wGMÀ+tf4œÛÚÚúLWÃ+@ª”ý(श{ð403ôÜt Z=X‰»ËÞNeïàî Üн—#¸бâvຘ]ìžÜÌ6ƼރÏ׿"”Ýõ ß2 +>7än{jGÍyŽÿÈà ž[‚Ê6H4q%?ÿcNŠhÛûìæfO‡ÿ—¸ý]ŽƘéÚBŽƘ™¼¡ç–±² ÖÚŒý¿n”¤ÈÚÿKîúïšfŒyYGœh¯¾$×¾îÏå½±—Éy/ð»T*ÕÅ oxô% JÖþ†ùuQ r–{ûE¤ø*bÒé¿ h·Öþokíà >l§ÿ°Öþµv„í4àï}G¯*ÂM½´Ëø‘îwóúNßt:³A7Ë"ºÚb¾üžjÎÆÝµ}ØêOÌÇøùëÓ:÷-¸¡=™‚»¿ÂR£¼4_Ħ‚Ç)¡mxDÚkÁã\«¨M{í³tµ.%F¬µ3_#qwæÃ¯±Ö.²ÖÞ™ãógá2ùÃC7WG%¨c‹çâë>¸®ÆZ]3ø7úý7—JÿIÿ\8«ëhÿ]q0 xø3ðÝ?sžÿÌÊn?Eçÿþv³?)ÑòÇøå߇ãŸxwûøg­Ýëøg­ôñÏa÷êÿYk{ÕÿËÜrÿ¯ì¤\¿ |W?y@?—ú}¿l| 927öÓŽ%o¿¤ªý"’h‘ßÉ­µïcCA]ã;D™Þ»ç¯t&§]Üc­µþ‰0ÆÜŶVWW‡×'µ¶¶Ö÷p9GÒ5c· —ù¼x ¸¨µµu­~þ‘6ÅoÃJ\îâ<ïWæ!8W„ž„ §üÄQÜåýŸ'à††]<à/^N óMŠi¸Á2g¸ÈÝÅ㟵ö‡¸@Ü\­Æ˜ÖÚɸá›Ãó1O…>3Ò·w2p1æö´eVâ²¢Òƒc;3Œ1õhwQ³ bœ•œ¾ÎõÛéÿïKpú{ò,ç\ÝÝðçÆû}'Nçÿãü10ÔÉ—yq¨Ýà2¡§oé”"ïënñÇÿFà\cÌŠÐk'ãnÖ·_4Æ4føüÑÀïý?2Ƽzm"ð0n”ÀVcÌlŸÿ¢|ž°ÖvéÿcçyÆþ_Úo¨Kÿ/j¿ß—y›®7#‹m#pDTêg(Ë0Ü+…ç€y©Tjyúw÷— e JÞþ†ùuËÓ¿[푤‰l¯µvßáÛÜ5λƘ¿ö3!ÀŒ1?2Æ· £ëžbúB/‚»“}À'\Ž¡ xHµ¶¶žƒàîÃ>ØQìÇÊýÞ•’h^¢3³%›qiÿ®ô„AÆæ ÿïT ÚÞNg`v]hû=jËÒ,Ûø5ßöJ±<¿0ÔÁª‰¸ÌâŸømþßþØõ߸àîV e­½ÂZ{ºµ¶Î¿o²__?J;†ä/Óƒ»[“¢Ü•œÃÝø,n’´ÑÀ¼>÷¿ß׆>wgÌÚ>Åÿ¶Ó3öæ=ûìBîžvŒªN;Ö¥?Öáêf¢2Î?zkíö4¢~þ[—…s¡sCpn’åóáÑi]öcÌj\vhø{bsüó“¤uûøú\¬Ž>8¿§ÿçƒ÷Ýîÿù²LAp7Òý?cLnn„ö}E pU'›óªÍƺ+•¯ßðß)~B±>i¿ÿ.µ_D-Êw°?²Ö¥k6îƘÿ[Àg߯ûàˆ u þ_cÌ=Qls8ƒ·µµÕôàóS|'.S¾ Ww÷ùü.Wâ& +¶ŠÿÛ IDAT8Î&_‡ Òá;üÙJk¤gp„Û|?î®p8@ð:.Ã5r»>õGƒûxg£züó¥®ïF0'°xXlŒYí‡v>ÍÞÃ}×ã&]iŒP›•Á[üßz¬Ïÿ@]'M—žÉ{î&f¶àæ\ù–(ª¶àJkä PÌÆeâtG•ÿm}‹ÀbKrþ÷¿¨¶ÛâçP0Æ´[k×±wà®'^6Æœ>ÖFñÙ׉q8Oø›¸{úƘe¹®yºÓÿ3Æœ±¶^ÜKfx¶€ãaw¬Ç•ÀX|7}¤Sòåfán\Ž/à#­ÀGÀÇþßûÃpŽç³ÖŸ7ŸN¥RQh¿/Ðçío˜_§ö‹HbE2ƒ×Zû0Ü=´à®w\Ž Øê$nÈ<ÁÝàŒ˜w¡kYbZÃM»ŒÎIá^!{à/Û¶=—õ’þ¹g"Üæáþÿ;³ÈË ÌˆòÆ6ÆÊ³¡ÀÏx9½foø-IÛˆÕÕÕu¸àn¦;ÞÁd[¯WWWOimmC-ÂB&Z€Ëðú,®NÛ·|µ•—Åt/ÃM0¶Ð?Nbïš’Ý Ú­~ñ6/-à=µië¡.Ïç^ˆÑ…Ý`Ÿ4òf\íÄBeºÉó:®ænS™œÛîÁÝ(;7„ÿ$ê®Â(ójp7nòHox{ –<Ì>¶€¾Ì‰> qR̶}·ÎÿƘÖÚ¤œÿ‡û ÞAEîWϤtCߥ´}eÖÚ=ý?kíIáúÊ êÿ Á•›ZŠ«¿|Œ?œÓ‹e> |Ë÷ Ÿ-ö~U$Süñ<—àþ¸÷Àݬ· ¸Zó3ü:Ì5á±þ;׫ýj¿ˆ$S3xï Ô2ÀO³ KÊЉý>¹ƒ»‰“'¸»8Å×ó]4TWW/ŠA³Vpx¶ï¼µÕî'{C`IŒ7õ\Vg%.“7<|yù' lÎu¢ªü3ž¿à øî^ܽjŒ™O÷‡f^Ä•e(—à.ìÜ í',Šôf/ñ#þ®€÷E×Ú£™ÜÜñöÞ º ü À÷ŽÁy§Ähßíöùß“¤óÿRòߌ¬M›ƒâ¨<ïWÓ:ç¼’Ši? KÿÏOÂ\÷t»ÿÅ:´!£ý5ÍÅÆ˜sÉ=©f6í¸z»—°÷¼$‘P__?|Ε­ÿ).¸÷.[sQ*•ZŸJ¥šüc½oßÏü{V;“sPç¿»_ÕÎZÖ£ö7̯[ß0¿®É?zÔ~ÿݱl?n.‘þ±1®í‘Ò‰b€÷›iÿþV!²ÖÞÜ”Ëq!;0)¯ººúh\°od–ŽÜ)­­­¯ûe+NŒAÓò]à]…»£¹¸‚Ρü—“r|>îÀž“_å;À“i¸Ì„ª>¿Œ½³^£ê\vv1M÷8^à-6Æœ |Wk·Pý]‹Nw{Ü©U RX7—»{ èkwû³w0þ(2gçÍÁM˜4 €~Ühx|L¶{Þó¿?ž-®ðÙ©I:ÿç½¹™žÁYh„Ä—1¦KÿÏZ;ÙZÛíþ_†ìß(ª°Ö>à§QxRB“ÿýÖÚû€G \?ýáàóyþp7g~J¥Ö¤R©½Fn¤R)›J¥ÞÁÝÈY¼›gÝ~Þw¬Úß0¿nMÃüº½Úß0¿Î6̯K|ûqs­ŒÆÕìý1îÆõùþ¹µ1l¿ˆ”H¤J4tttk“£- Õ#Ëç Ò“©px»µ¶7lá Ú~÷‘9S­ Ük§?ßÍoã‚ø_ŽËRlÂÝ¥Ï4AËZ\ ®¸[Lg¹†×pƒ2F¶â‚¿Sp™K/㲜ÛbÒε%Xæ« ¸È[̶ÖÞ܈ËNÉe4’nеöcÌ“Z‘÷ ÿó|ö^à꘷n2Ì¥tÍò©ô‹à¹ídÏZÇ¿öÜèú¸Ÿÿ­µ{ÿ1MÖÚ¬çcL\ÎÿGà2×q™ÓýŸ‹tH‰õùq¨\C·ûƘ¶˜5ù ¿?Ÿ‹»Ùõ¹k¯ÆÁ›ü:JE¼}’{âç]¸ŒÌEö‰×ú÷N!{BÓ!þ»×¨ý±jÿhàZàÌÐ羆˾=†í‘‰Z Þ£B¸øNI¡Ÿÿþ3ÓkÖÚ/cŽê1N†«®®žŒª™npRkkëª7o§?Q¥o÷¿‰AÀ¡÷¾ìOpmþb/Û…rR,Æ éÍ4YÄ\¢_oRzo=…Í2]…«SºªÌÖO ¹ëÝl­}&†¼å¨'AÞ‡p#=’`+0Û_¸…k ‡¾Ã XÎ 9wC4’|MÝnÿ}ÝÚDœÿ1k}¿µ˜Ë|µØË”~û},¶Öfíÿc’Öÿ;¸Ïs¾µö(\’N¦Érçc¶[k#úÁ]p£6r»wÿ¼“)s7]*•²õõõkp£>Ž#{€o¸ÿîØ´?Sænº†ùu¶vÖ²D¶7Rç$\pw<#wÆûç–ù}#N푉T‰cÌg‚YþÏ7z»Lkí `µö߀µÖÚ! Ùv—fxn#0=æÁÝ@ú°Ô«¸»ó·§]´Þ‡Ëpúo\vo&K´ßžƒ›x+Ý•(¸›xþ˜öB–‹œL&ZkÇYkï²Ö®LÐ10—|Û‰dEY h¥s‚­áþï™Ã´=ÄÕ\-Ä ,í#¯·a9ƒ€ŸÄõüoŒÑù¿¼Ï})kmk0φµv¸ÍnxB×AÖþ_ƒ»Á¹üQ_wød\ù…ÛÓÞ”q8ÚZ{¤?WÄáæí`r— üØŒ ôj‡ÿL®’}ÕþXµ¿ø2.s7=vs0.c¿2f푉Z Þ{üê`܃EEXælÜ]­uÖÚõÄã®nOÇ´¶¶6&¤}éíø6.+ÜPûL5EÛqY[CÙ{š¤dð^áÏUÚ AËÚLšF šÜ5¶ƒ×ZÃݽ¸Nö¡‡ÙÊÙ܇îû]¢9‹t¹&fÿßË_Qx=Áöm« € |ïÑÀwôóÎh;.ƒWç‰Û¹¯œÁ:ÈÛÿ³Ö޶Ö&©ÿw nDßBài`ž1æ:m×\bŒ¹Ú#úëÆõþõví9""Rn¢–ÁûðŸáõþß½u®FM8ŽýLòÕÕÕ·à&[z˜ÔÚÚzIkkëÆý6Ó³øÎZ.k'SÁ6\ {']'¢jOÈÞ¸aÊ•iÏ/ñÏ=áßóð6™³¼ãbj|?ßÞßÑOŸ9¾Î?âzq7Wk|Z†—_Åe°d ånNø Ú}i\Œþ¯Ãq™—›ÒžßN×›«üqp6…OJue8îårÙ³9ãz<üÇ^.£ 7ÄsqÏÿƘvkmÞó¿?¾%íüßT/Kÿ9x-q77ýͽŽƘíiíÞsüKÚyÎO$³ÿçßóð¶µöÒ4û*ß·y…ÎùæXko4ÆÓ°¼·p¨\æßw3.Ûáàaà☶ù5à÷ž?ÈÎì 2\3A—úG/îFû‹œLê§€SŒ1 ²ßÚ€›€/cÊe¨òú„µ'ÈNÌuî»7!Íet pÅYO‚»ûHFwt–cÞz¿n.Âø‡Óì7iç¼­¸áÍq tõüoŒ‰ÍùßZ;ÍZ›ñæ¦pçúlÆ››ÖÚ:_§¸,ŽIš@ÓZ[i­íQÿÏZ{qŒ›~»ß_Ƥ½v³fc­½¸+íõ1þsOâæ¥ˆªò\› õûþ„úúzSà2'øÏ ÍñžítÖ1Eûkg-+ëöズ/ã’ºÖá‚´»üßü¶a~]{ÌÚ/"%µIÖÎÃe f Âi×oÐ1 ìëu`‹1&öCvZ[[Ï.ƒß抴‹ÓMþnŽÿ3<Ò£¾CûŽïäß™¶¬8µªüEüy^Û œ®º‚Ή¥Æã&ÑiÇyÛÓ.xãîðH‚7ÜwêKÒŽ`­ƒ îfvy¯1&<™Ô½Ú¿ÝsK„›¸€Âë ²,€¿Ãe|™ŸÁN¿m³Õ•œˆ ~­Nоޛànà>Œx0¦ë ÎëÒ³ÍÛqC—Gg6O¶O›p™»Šóùß×Ï{þ·ÖÆýüÿZ–çOÇeižŸ©ë³\³ÿ–Æù@à³qïÍVW×Z»çøgŒIL_ÇôsöÿŒ1-ÖÚ¬ý?km{ ×ÉC~Ÿ^LÎòž›}–r¶‘8“q¯Sqß+"ØÎ͸ìËl_¾‚ 体kaõõõãý{'ãÊ™eó{Š}ûkg-Kzû×ân|,Ã%±\æîËÀ–¶_DJd@LþŸÖõñ À…ƘŸø¹ŸbÿÙûµÉc£É?Fâê†~—‰øœÞà;oà²ÚéZ—õd\& Äwxæ߯lõVgû>¸Ò-À)À³þBoº_Æ#~Ý=£¶™:ÙnäœçÛ{‰ÿ÷H¿½se)Õâ‚E±¹ØµÖŽó9ã3¼|•1æÞ´çžó·p­Ý‘¿x?µË|˜”¶.ò¿›Ò5@”çøßöó iÏYÜ]ÂÞÙœéðǡ"ÜÞï×wc_½Ìe Ò­÷çµqÚøÆ˜&kmÆó¿1æïüþ¼çüïž&´¯'áüŸm?ÂZ;;äÍÜ-I;þÙØ9ûƘ‚úÖÚ6cL\úÏã²µŸ.àØž¯ÌÒ‰¸šÅçânŽ^±¶nÁÝxk#{é±±¸2ƒÛêëë_Ö¦R©½2 |p÷븛åcÉ>OE®„Û–¸µ¿vÖ²€µ óëöj¿n&ºý¸É•×úßôÓÁ± a~]{LÛ/"%‡¯ •›bŒùC£“üÉ=ÈÞÅómòXYEçPû›qß hÔâ Á0—Ѹ»õO¤âæ 2ϰœ‹»k¾ŸÌôüE¸¬ÏpÁ¬'üEÁ31i!ÛìbߦëÈ\s7]¤ëZkO6e|VÒ+.b‚ƒÏdŒ´Ykïæ…ž®ôõ{wâ²ÀÖG­Tƒµ¶ÒÿŸ/ðêÏ×úöœå/Z µÉs°Ïø» 79W%îÇ•¸áß/%ìXy²ÿ3)Á¬{Èܽ7t¿R wíow‚»K€ xß•¸ p\kóg=ÿ‡jñv9ÿûR6q?ÿwëæ¦?¶'îæf9ÿüÍÈœý?cL·ú>“÷™ìóçãFžÍ,Ò2O÷ç“Kp#z¦XkcZú»±©TjG}}ý2¿ý&fyÛ>¸,ÎÙÀþÀ¢úúúÕtÖØ‚–?—­ürÏS±XšJ¥vôwûæ×í¨µ¬Ûí¯µ¬×ío˜_ÛöãFjíií¬e±l¿ˆ”N¼Ap¶Ðú;Xk¿üÖ—f²wçhsÇú¯ ôÌT7è%\ïÊ ¯µ¿ GÆùÎy¶þs¸ ®°3ügæøÿ)þ߯ÇûNÐ/|çù™ýF.%Þ“ÉǬ9¸ÌR¬µ;qî¦Ð5È8ÃójŽÅ=„ …³T¯ÇO iŽZÐcNÚ1:ÈØ»Öÿß»#¨×ùCº˼؟. e&EXö´åyýjànÿ÷+q¯kr¼kÄÛ[hŠþBÏðÞ€}ÊÆð7ñüŸ¡|×KÖÚÄœÿC7ør½íbkmÁ771o°Lÿ¢ÑÈÛÿ3Æô¨ÿçË{Dµÿ·„½™âgÚ^»At*.xÜ¡v¿¼‘c{ƒ›@°—Àò%àpÃûÁMÂõ9ܰü±äŸ„øˆÕþòn¿ˆ”@,2xc­]fŒÉ;I„µöjàŸCÁ]üÙs‡6wìÚY?Îwd3 j“ÅÅdÁvPŽ‹Þ‹2<ßêÔÏóüÓpÁïWp5Ù¼ÏÅd}<ËÀ(Ær ºA½ð±m™kÒ®N3ƬÈs1¿ÕZû8ìS·G°ýgxîr\ïQ=\f¦Àÿ9>ô:ù‡wFÑv2½‘°cÿEþ8˜i&è«põˆÃ®õí™n´ànFU%]k‰gÓâ×K¡çÅC}¨.‡âð-Êù? ™z%’ˆ››Ýá'Ë3žOÄñÏZÛ£þŸ/iRPÿÏgò>Ám{ªµörߨëëq5šO‹XÓ7ànÆý5™Kqöñ¯Œ ÚìŸß7¡ÖÀ¾k­ÿ® j¿Ú/"ÉUÕ~NZ§ÇS­s‡µöK~HoðúDkíw¬µ³wp÷ccÌç´©ciE7Þ›­C§!{Óp¬dkËvܤj;³¼¾ÕwäWᆤ÷ïç;úWã²$~Añ†¿•J|;ƒÎYáÇqÚ#è:c|ð =†îôˆ¶÷\Ъ=ǾpB¾ànȽž{¨5Æ4Ædèm–MKŽ‹É3Œ1µ:ÄFÖb\¶Uú±îÊ,¿mpekÒgLßé—Sá¶¶ã꤯Ïñžõ¸áÖ…fòõëâ’ÆYnçÿLÇë¢-Ç—r¨^ôX[PÿÏO8—éœVpÿÏZÕþ_©ûg3¢ÔØúúz“J¥:pÃî ì.àcq™œŸñÑÜÛí¿cQ*•ꨯ¯7ýÝþÚYËLÃüº>oÃüºŽÚYËÔ~I¬¨x1c̤5>¸‹µöZkí[ÀnÛéÏÖÚ{¬µƒÓ‚»ëŒ1Câ¸aª««gDqY}¬¨¸Ôß;—¹1<Ç{.)`lÅÕbZ…Š6W–¡7aÇeÀ«¸š¦Qü]C©Ÿöë$<Üh8ÐlëiH–‹þá¸Ú…w¥}OTx;1WáîÌ7fRL7ƬïÆòVù‹¹À½>¨¹3¢¿ÿ |®;2Õ\}<Ó ôI¯ãF%,ÃežÌ&ÿD©·â‚À›üqnºÿ3ê¾Fæ›VÆ¿Vh°²#!Û¿œÎÿ{ø!úÁM(ãû²]nnš H»¹z)ê77Ëžµ¶ þ_¾›³>È[PÿÏZ¹þŸ1æ|SZçGqû§R©ÍÀã¸ìÊRyø¹ÿ®Hi˜_×gí÷ߥö‹H¢E2Àëkæþ•/«0γ%Tƒ×â“zƒGèyÿqc€yƘÏÄl{„ƒN/UWWÛb<è:¡ÐÖ­õô¾¼B2x&ûm4Wj!Sæâ-^V!ÈäX›‰¾ 7¬oŠïäŸï/|Ÿ¥sr’¨ø©ÿóhÜ]íÖ`Ÿü:ªÄM@c3<…–õ«Ðó͸Ìå ‹éшû–àjm…kdm2Æ4õ`qáLÇQÞŒ1wã2ŒVùÀÎ\ÿ›ï[p¿~Ÿ¾Câc1.pu8ðdŸ¹7Œq:É™t®Ç»VŒÿïårþOï›=m­=ÑO$èñÍMkípkmdon–;_–!oÿ¯Ð² ¡LÞ¼ý?kíÉÚý'•JÙ “6•J5ø>Û‚|ÕàÞT*µöd÷ûHކùu6È$m˜_Wòö7̯[{2gÕ~I¬Šˆþn0Æ,õ•w1gc–‡²Ò;5ÆÓlŒ¹ ØÇsC ·ÇOûà;ÑúhÇ ­ïIAø6ÜdKq¸À{Øwî}`∠'çî»6ùNþZ\6ì&à[¸z…¿À Ý[‹ òŠÐº¸ ˆÛT¢åoòËŸÙˆŒ«9;tA\¸$M7–³07³|¤cî6ÆL2ÆaŒ¹µ·™¶Æ˜6cÌ\cÌXcÌƘ«#^scpÍ_âGø»$Ùb{ç÷ÿ^ÿ1q ðîus3¢FÚÍM›i77CÏÇâæfpL²%Áã_ÑûƘ‚ûÖÚAH¿ yßwà†Òc¤Q»_Ö~Ù‘ îÂAξhÔ‚›åÞ~)Q:RW"ÖžL1ÆÌÉó¾¯âjÏŒ>Ö¿7ÆÄú¢µººº 7ó{0Û}1mÂe@]×ÚÚÚ¦Ÿ~dTÒYw©—­6øoÿÜZÿ\S—?Æ_øÁe´‡«Ï¶ØÏv`R‘:RÜãá#tN>v¸1fm–q9ðà~cÌ•Z«‘6W‹õ Ï&\¹—Zå"‘9Þ—²ÿ×¥hŒi‹`ûûôøço€öw› êÿõpÖÚ‚ú*[ÔÿÂ×úúú©À7€3É=ñV.Á÷Zæn&áÀcí¬eEm2W˽ý"R\*²-Òÿ> 3»f'C-·ã&Öéíðú1¸É;FúNþ·q³o¯ÂeylÒ&ˆäÿ\]¾»1·÷pƒp3æ.0ÆÌÖZ‰Ìy>gÿ¯«f[~ÎþŸÏö•H òè·×iÀ±¸„¦ª<‹hÕ¶yWsuQPs7ÊÁÝ@Z³(íjÎÆ!¸Yîí‘âQ€W¤ÿ}¸/í¹&:'*†q¸LŽ‘¡NÀ)ô<3Xâs9ØnŒyPkCDD$2çç¬ý¿b•ñ÷íÕÿëif°”NZׇ_Âew8W‡{ ÿÈ.ÜÍ€÷€?K?R©TGú2£.-ÈÙ«ö7̯ëH_¦Ú/"å@^‘h¸ø..Û¶¸ 7<¯˜Æáê±­Çͦ¼]«]DDD¤øRJ]ú=)É”ç;ºôÿŒ1êÿEThâ5zn(pp °?°¯écÜ(ÀÍÀ–T*µ#×râ 4ñ˜ =WPûæ×íȵµ_DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDz¬B«@DDDDDDDDD$¾ŒVˆˆˆˆˆˆˆˆˆHü(ƒWDDDDDDDDDDDDD$.b;|çŽ;î¨9rä0kmá5†¦¦¦¾ÿýï·jÓ‹ôúx‘íøaóü[´ýEDDDDDD¤Ü}å+_Ù8èÖÃNDr3þQzTüc *ËcŸÐû*Ó–,W´ýEDD¤4‚óoú¹]DDDj@\ÿãÍÍÍ­}ù9‘2¾…ƒqáà\úŸé ÁÍ”Ž v¤½~¿hû‹ˆˆìeÛ¼IC€Óýc"P vÀjàyàùQsWî,óÕõpð)°!Ã9ZDDDj€VHYKꙚáǺfd¦ge†u„íþ±;ôhO{ï .bĈÓ{š››1bÄ"ß–lê›››OJû|ð™W›››§1âàÊæææµ7‰ˆH9ˆs€×ôñçD’$=;3ÄÛ¨¦sÈ}5]‡äï gvB×LÍ  ×柭¡·úçÂÁ¿ô,OÑö‘®ê€‡Z­Š=€Ë€ejorl›7i"ð,09ôôNàe`+°ŒNÆ{G÷—o›7éìQsW®.Ó}b .È{ ðVŒþß÷ãüßOÌóÞT†çNLûs<ðp˜“""R.ù"R^û»¡kvf¸ =4hßY³Î;üØcùìá‡~èpà~ûí7rÈ!Ã8¸ªªªª²²² ½½½½­­­m×®]ŸìüxçG~øaÓæÍ›7¯}gí»o¼ñæŸçÏê–––O€–Ðc¿p–gäS°OÛ_¢c_Pá'á Ðìƒ/kU]ð 0\ý·.,°—±¸Lí¿mó&M~EgÖî*àà¹L™¹>Ó÷,àFàHÿtpƨ¹+_/³ýaAÒíÀß¿Ã|Ĉ ¹¹ÙÏ¥¹¹Ùdú|ú2Òß'""’ô ~Iþ¾nèfdiã‚6C€¡—]vÙ¤™3g}ä‘GN:ôÐCÇWWWWXk÷¾Æ =eûšLc uWkë»6¬]µjÕÊ_|qÉÃ?¼ØËÈ »èÌî †÷+£SÛ_Û¿ï ŽŽ¾ˆ«yy8°_ŸÿxWó¿¥À¿Í¥ûŽâ‚»ê¿íu0\0ëÿ;S{cÊgîþw# àVà¦QsW¶ðÙJ\9‡ëýS[ã”É[‡Ëj—åõõ^Û œÔG½q ðŠˆˆôþ¢?–¦Nj†w÷sµµµ;–/_Þ®M/e´‡36¶t§ IDATƒLÍÁ¸€Þ0`ØÅ_m)mðàtàkþïÅ.ã´—qøÜ„HË´Ê ’Üuÿ­ÂÇ…rò&¶½>w)e.5wåãÞ÷À¨¹+OȲœ q“¬ŽŠYM^“á|wðnäDwµç‘®E«¯ˆˆHï;±4uêÔC–/_¾±Ÿµ|ùò´é¥ öípÖf•¿(Œ ê 9räèÛn»mÆ×¾öµSÆŒsø€^(çâ{v¯¿wë?bL–üÃðÞÆ ë~³ð7 ÿñ¯_ÐÔÔ´Õ_´~Dçpþ`ø¾E>mmÿb:¸ø&nvú¾´øðSà}mŠŒ2wcÝ+ôãAÒƒ¼‰mï¶y“®îôÿ¼uÔÜ•s³¼ÏŒš»ÒäXÖ<:3y¯5wåí1^5qµ—‡ôb±Qn¤ȶ4771bÄ›þø—ÍëÍÍÍ'¤}þ5`Zðš¼""RŽA€Xš:uêa Ù{Øpî@Cmmí¾Ë—/ÿD›^¾_½¸ŒÍ!¸!×ûrÈ!Ýu×]ç|õ«_1tèÐáîjÑ ·ö§=béhï`ݺu¬^½šuëÖñÞ{ï±yËf>lú>úˆ]»v0pà@† Æ~#GràpÈ!‡pØa‡1qâD;ì0***ödoúÒýG}ÀÏ`øhÇGÛûÛß.øÞ÷¾÷Ì{ï½· 7Ì;Æ¿‹Î ¹äÓö×öï/׳p%;úÓ§À|àGÀ´iö8x‰½ƒ»±î¿•@¦cÁvàT`±ÚÛæM„«;wógR¶¬Û¼•ÀÛ¸àèVà°QsW¶Äôœþ¹ƒùÄâ÷ÊÀ=¦¹¹yI/—u¾ö°¼""RN€Xš:uêgÖö À;~ùòåѦ—„ª 3¸WMgÆæ~ƒ :àÞ{ï=óŒ3Îø_Æ æ¥øV¯^Í›o¾ICC+W®¤¥¥w×Dƒ bÒ¤IÔÖÖrÌ1Ç0aÂW­Õ„|ÆÿÃGíØþüó¿úÿ¾óïü²¥¥e .Ðdt¶ÒäëÐ&×ö—n©þ øzûøµÿÿ5”ùvÊÜwÓ£;Ù|[Ëp¥1¢êtàa:k¯"×zHj×&±½ÛæM:ø…ÿçù£æ®|*Ç{óx»»Ì g"çÓDçÄtáçN!%q )ËÐ ðŠˆH¹ˆû ¯Û©S§Ž\¾|ù‡Úô’@þÔY‚›iÔW\qÜ5×\sùx(.33rõ6lØÀ‚ xå•WØ´iS×%ïc æ ƒ9À`ö3Ü`ûoØö°Û-öC°[,v“¥cƒuṃ:ˆ“N:‰¯Í˜ÁØ1cÜÁȘ.A?ƒaÓæMïþèG?zè_ÿõ_ÛpK ²9Ûp>ùJ¼ýÜÁÄ6Ì2vh¶ŒdZeÙ×Wiýx7ìh34µ6bذ£‚uV7W°å“ mÿþw8p.c7êç~‹ËèýGÜDmåæ8\Y†!E^îZÿ;ˆªw€ñE^æN\p+IA^›Äön›7é1àB\YžýseÛv#À;øW¢èñQsW^³óù—ý± 2íõF`6™ƒ¶ÍtÞÚ Ì–Ç¡Ñ ðŠˆˆôŽNx"ÉØÃµVƒ¬ÍýG5öç?ü[Çÿ×'™ŠŠ lÖvÍÚ\²d Ï<ó Ë—/ïœTkTYAņŠÃ fxïv»¥ãKÇÛ–öUîò¨¬¬ä¨£Žâì³Ï¦®®®K6gðwkmÇïÿŸ¯\xáE?Ù¶mÛ±ds¦×fÕö/Âö5¨ƒcî îÀv¾0ªýõî?øA üq[%Ë6Wòæûlk©Ðöï;Õ¸@éµtÞŽ‰‹]¸zœ·á2·ËÁ4\æî/¢ªTûðN\fëë1ù Ø2k/ÛæMZ  ,5wå©yÞ[P€×¿÷%\sÕ¨¹+'Å`{o§3@»—öúzà(\ð6“ À» 8 øS\Î ðŠˆˆ$·£/"…íÃá!ù{²6Ï;ï¼£n»í¶ï0zôÁ„&Ðò%K–ðÈ#°zõj·¤*¨œj¨<¶s¸Ùkr¬¢]ÉX‹}ÇÒþFü¡‚ÊöJ*++™8q"³gϦnêTWŸÕÿ§¶¿uëÖ÷¯¿þú~ê©§–Ò5›3e€¶éÔ?Žˆy;Þ¾A †÷R©ƒ»Qï÷•rÿSÐÓ–Y{Ø6oÒÿÛ¿{ÔÜ•Wçyow¼wßvŽš»rhÌ·w;0=ÏvmƉ§ã²öc#à=¡¹¹ùõ^.ëD`(À+""別©S§V444 éfÇÈÔÖÖ~¼|ùòvmzIÈþ÷â&ÑÚ8ð®»î:ë›ßüæeûì3`ŸÛY_ÕZ˺uëøÉCñ‡†·ØÝÑŽÝ·ƒÓ+¨œVÜ·‡û‰…7 ÿ9€-.Ð7yòd¾ùÍ‹8ôÐq¡LNèÛ½{÷§?ûÙÏþÞ÷¾÷°—͹ƒÎ ¸Ê)ÈW”í?¢ªÿõÙO9mÂn†Võmv´ÁKÈ¯× fÇî}´ý‹ëpÙ¯Õ iO+. ùÿ$x›mÆ”q¿¯ÔûîF`l ~¶ÌÚ tmÛGÍ]y]ïÍäåQsWžzï9PX@8âÛûÀyÞs!P»;c!ð677›#F< œÓÃE½ÜÜÜ|ʈ#šá ðŠˆH¹ãÿ{­µvi>·/nh¯Hœ…ƒ{ƒpCòGUVVòôÓO_‘J}e¦µÐÞÞ±'°×ÖÖÆSO=ůýkÚÚÚ0Ca@ª‚Šã+0UýÓ÷5ƒ ¤Àž°›ö%–Ê×*yûOâÆobÆŒœyæ™TUUí©ÉZQY±Ïß^zéãÇ7kÖ¬ÛÛÛ+éœX¬äë‹@Aì·ÿð–ó>ÛÊÌñŸRÝOg‚¡U0ës»8cÂ.¾;˜gÿ2LÛ¿÷â&ªš°vU÷âf’¿ ÔOš«pBUêWtí~ýª½Ñ”&(d’½§€ó²¼–~«rthùå`&.Ã÷‹ÀŸçôâ³'ë'""å(μCvì©ZHc¡¶¶vèòåËwjÓKÌ÷ÛôàÞèaÆ{á…®û¾p´ Å·Ö²víZxà6lØÀn»›öã>¥#Õ±–í‚ê×2pé`˜rÈ!üíßþ-‡v˜Ïæc*0V¬øã~úé·}ôÑGëqµè>¢3È—äLÎ^m:v3sÜNÎûl ûî­UôñnÃsÎÂ÷÷ƒ mÿ<‡êŸd¯gÑ™¡–$çO°wªú}¥ÚgÛp7<ž‰ÉoÀ–Y{Ø6oR0…´ Ü",w!.à÷Ö¨¹+kc¾½ )Ѱw“èop“±Å5ƒ·Wû_†2xED¤¬ThˆÄNxXþ0`ô~ûíwØÂ… o9rÒ‘G···~¼üòBn½õV6mÚ„=´ƒö+Û°3#Üŵ¨õä]ì¸l;»ÙÍ–-[øñÌ,ZäÛÓ±çÏI“&ýÕÂ…¿¹e¿ýö; —¡3̯“tAµýCÛÿsÃ?åÇÝÄßùIä‚»û°|cb3·MÝÀ„¡»´ý»g¬¿èŸVmæÛ:6m{œk+Á²×G¼í¥øÿÅ1Øiò<’ÖÞÀ[Áþ½mÞ¤¢ôPür‚câŠlï*ÿç¹Àcö‰JÜ  |YÎc€ßŸOûž¸:¢¹¹Ùd{µˆˆˆHùx•½+ Øg+qÕ‡£† 6îßÿýßož0aÂäŽÝ.ÖÑÞNkk+ÿöoÿÆSOÍgwÇnÚ¾ÜJë¥-Ø£ŸØØ1ºí³›øø¸ìîØÍ¯~ùKžxâ Z[[»/Ç?|ò‹/¾xó°ÿŸ½;o£¾óþùÍȲc[²â#IHÂB$$Ðr•k i¡-z,-m·Gz¼¶Û>ݶۃ§Mû°¥ÛkwÚBSèv·Û¥íCK ¡¥^À²$¤¤@â@BçpìØ±å‘/3¿ç™‘~lÉv‚Fþ¼y Ë:Fšù&öÇßùþ¢Ñ…°«#ζ ¡:¾ ¿efpâüŸóz°0’©ø•œßÆ—Ï:ˆ·žÔ Ëäø—²É<`é4Zç¥Î:WkÈûnLmÈÛà“¾ÞŸtÞçTI9ÛñÞiò™úú>è|Éž¯ºÑYÜ€mv¾þÀ‡ü ìª]ÕBOÁžDs,m°'»Ðù>ÈgµìŠÅb²ØÀ üõ€ˆˆ(Ø-j_xá…Q¶h iÄîµéº¾àá‡þêYgužzJþ`"»òì޽隌·Å‘YœžÒ7#Çù]ALÑá¥ö@Úžp:Œ%K–ছnBcC£}ʾfOÂõòË/o½úê«¿fšæAاë'`Ÿ–h°¦óø×‰$>~Z'VÌ äJï4ñocµÓ}ü‹i…]ͺÓÓ+°+ôª±]ÃzØ=yýÚ5ÄÊ\Vú6•ùø¸ÏmnØy_ý¼.³ÿüVáú:Õ¶{̰ÀòÖ[v¤&±¼0€NÐ`Ië-;‚øá­¾\âcû4{nðvدU´1Z4,Çã;ÇxÞjyó²°EMGA®àmàðÑ4âž¾W ÀL³þóŸ|ùòåç™–•­jìëëÃ÷ðìÛ¿fsñ÷÷M:Ü•>ÿwiÁã'(¹p]ï8ŒL,â'?ù7ôÇûaZ¹JÎ3Ï<ó¼Ÿÿüç0ÛÙ6õζª–Sõ'4þsf$±ñì wàÌè þáôW0»69Ç¿˜:ØÎÒi|l\êlƒº*\·û`Ÿªír ”y ’É®[ÊÙn÷UѾ0Ö?¢U±¾Nøú=çÛSa›“q«³ø^@Ã]øß¶”øØfŸÛl†=ùZPí§‚÷yQ Þ¡ >Ïâ°SÀ¨“j¹}W[o»í¶õ—¾éM׸§ä[–…D"»ï¾ÇŽCz^ÇþºVÌœào“c„¹š{]ú_4õ1öe2ao¦)ƒCë`tÎ0úûûpÏ=÷`pp0ðY–‰K.¹äšo|ãëaW4VS?Ö ÿiÃøÒ™û1«.ø@k8…ÿµ¸§ÌHLÇñË]˜=wÇs±³-ªÑý®ÇñéÉ[RÎöºŸëHÿŠ\/ÞÏõÞº|ÃDâ<ïsηÛå•„ÝWy2gÎð£oƒñzðžÍCQþÒû³ŸýL[´hQ[4->ëºnnܸ±õÞ{ïÝQîòo¼ñÆy7nì6MS/ö˜D"‹/¾8Í݃*„†\¸0÷ú믿è{ßûÞ7C¡P”,K"Nãgÿù3ìß·Cmƒèzk'ä&ÒÊ `…úUBŠŽ ²ð{ûé¢à1å¶qE,AôX,\ˆo¸555öéúB “ɤ?ûÙÏ~á7¿ùÍ3ŽÀ>uw@ÁýãNÙãríþvñ«kÇ·åžôÙäqüÇ%ei¸»k¦›§Óøów-އOø¿Uºn×Àîˮԟß^GîaÇ ;äúWï­Ë®Èt'û&€[ZoÙa–ð\våz¬i½eÇ*Ø4ëaO°6‘ è¼À³•¼‚c´h([4ÑtTqÿà-]º4ÖÙÙùH*•Ê;åÔÛkWJ©e2™†r{ð†B¡!!Ę¿è‡ÃáWæÍ›wÕ«¯¾ç.BðÕ`÷]˜ÝÒÒrê£>z{KKËÜlßUKâw÷ÿ/½ôF"#8ø¶ý°êÊ˳ ‚]o¨+Ê}òËh™SúFäXÿr¿ew!ß x¢†Ì~Ô½q™ã_ÌjÏ8ûå$\`[•®ß:¿ƒò2°È?<¥`÷}ˆë|½·.?v[7ä}ÀWÜç×—×é·»À×kYÓàí­·ìØ€UÖQ8™Z±cÿo`O´æç€Ï}}Öá¸È€—ˆˆhòáAʼnF£— Ýgšæxm”›Œûx]×Ö†ñ8wªì^¢ °O=_ð«_ýêÞð†7\%¥„eIHiáÏþ3{ì1$CIì»vÒÑò Ðe.iÍv B]©ôØ-áà"•ÃŒêªU½y¹riçð`-–?rÂf-.¹øb¬XqvvÂ-!ž{î¹GÞýîwÿ€ƒ°'_‚=;µUÍã_+Gñwó·£%œœ²!ï_ Qä_‘û"}*¸ ýÉéËÌÀ{Þˆ¤VWíãï§ö)Çgððèk€•°ÃÞj´À¿˜Ë¡Î:ào0=ÂÝi³¾N%ïf+”›<»2w€Å°ûήƒÝkÖõ€k«¤r·\qä&*ìq¶ÍŸƒðÆðM><¨8†a<ÞÖÖvn(2Æyh¹ÿ`ùøP(d´µµËp—*„€]Õv~q™ù|à‚5kÖ\i9“jY–‰®®.<ýôÓ€¾ì`Yána] )$d^ÿ\ h¹Û¥pº-hc\œÇØ·Ÿ›ëÑk?Æ^ŽÌõêJîÏ›jLâµ ÷@h[¶nÅÑž£Îö°`YÖ¬Ysåûßÿþ `O¸ÕèlCÁªz+kü5¼gÎ+SîzÆYø|zî~©Ü—W¿}c’ï­94‚›_„^Ýã_Ì—Àpw,g8Û¨Z=`9‡9ÏrLŸpwÚ¬¯ήð ØU¨pŽçëlp›óõFäÂÝ>çñk¦i¸«êp€ôžÕ1›ªIñzxˆ$"¢é¢b'YëêêÚÛÔÔt™®ëG@q\N­u—«ëúÑX,vYWW×^îTÜxU‡Ýo-RWW7ëcÿØ)¡¹aV*•£>)%zVÅмÒçàðVíJM©ÚU&PsC];œ“ÙÀ×or5éw¿êe—#ó&k“jð‹ÒC^cö:Ï8)%žzê)¤Ói¸ŽABûØÇ>¶¡®®n€ˆ³ ug­²ÇÿŠØœ^?5]eŠøÒç{o¨+=!p±‹[¼=Ñû)µÇpqýžjÿb–ø<ãú¼³­ªU‡xZoi³¾­·ìHµÞ²ã'Þhì^Ø­ÜðoÄùþ^çþ“[oÙq‡_‡iæ€ ìD°Z}¹w2­5ÜçîøM ÿ‹n[[ÛñxüÙt:;^¯QSSŸ9sæGÝÅ]‚*„:±V3€ùÿôOÿô77ÜpÃíSó-H)±eËlß¾MØuåN;<-A~¸ë v½í<­ò*0ǘdÍ=]?û™ß{7¯¯Û¶AyL)í„%°ò¿ÏEÌhÆòåËpÎ9« 9§êkš†_ÿú×?ù¾ðo9¿e­²ÆŽÖ‡µn>‰¿ƒe+v‹pª°…ðÜç\ÈïÀ‘©,OæïRkw2÷*‡)þkèMè–mÕ6þÅüÀ»xx,É/¼››ˆ¦™›ü Àan ""¢éE«ô7ØÓÓ³«­­íìP(ÔL]%¯»œP(Ô5kÖ¬³îRÒa|ÑÙ³gÏY»ví ö©ùöièÇŽÃÎ;è8ï„Â]) «vó¾×ò«x³U¸cTñfŸãTïf[:dïC¶¢7¯mƒÛ²Á½^BщÔ$^9g KìÞ½ñx¿SÅi·0¸êª«n˜={öØ”Õ9ÛÔYûêÿ\Û1©p7»E”‹TZiŸŠ\Y¬2W÷œ_U°œdÛ]H¼yÆŸÒªrü½Vx'‹%{§³Íˆˆ¦“ͰÃ]›‚ˆˆhz Ä?þ‰DÂhiiy8Nß`šf£ÎNøt!„”RŠššš£3g쪫«k7wª võf-ì`jÖm·}ãË–-[mWnÚß3Ï<ƒÁÁAtžz½ Kk1æ­Üõ v5½>­ VxúU~žªO‘}éܧWüß¾æÝOº6³3 bþ‚pãÂp8\7oÞ<ùÐC½ûTÎ왪­jÿóë^ʺΠ¿X^u®O¿Ü¢·9c ͳŸ _(vŸ:1[öq¨ä­×RH!Œ.̪–ñ/æG`ïÝr?GsaW=M£ê?ïDDD4}hAy£===/rÊ)Kkjjº¥”“ª¾rÂÝîSN9eiOOÏKÜ ¨Â¸½Wk4655µ]xáE벓H™&>ŒžÞ˜µ\ZÚ<"Å+w½×•‰Ïò*u=§i°'_ÓœÇ8éÁcõôÍ¿M©ðõVò¢´JÞ}K^C¦6þþ~twuÁt&!3M¼ñ‚uMMMm°'a©EåO¶Uòø7èi\4c⟒PBZO ›7‘šû½(ì·ë­ìõNºæ+á°ÛÇ×]öD~#}CÍNÔkÉj?çx+e{«³íˆˆˆˆˆˆªš¤7»k×®xssó%¡P¨(¿]ƒÒ–¡£¹¹ù’]»vŹ P…q#6·ÿjôïÿþï×ͨ¯o², ¦e"cYhoo‡&48½f9îBKªÜÍ Xs÷É‚ Òda;Ï%[í«É\ì ‘¥'ØLÈ›©ÉàµE{ „Àž={`9“mY–…õõMŸùÌgÖ!wš~¹¨1ÐãaÝ«¨Ó2z¡l do¥®ÚBÃ'¨U¯{Càb­à3ñZv267ðUž[îäkµ"óôÕ0þÅ|Ážîõü<}Ž›ˆˆˆˆˆª´7ÜÝݽ{Á‚ç…Ãá®r+y¥”"w-X°à¼îîn¶e JäÆcaõ¢]tÑZË4íÞ«¦…îîn$ ¤ëÒè\x¸¼%çµS(¬â•J¥m^¯[}ë韛­Ö¼=yá9ß?èÅ8!/Déç옷©Ú†‡‡ÑÛÛ Ó²`Y&,ËÄ…^¸@“³Mèü€wÜñoÔ’8»fßä_ÉÛfÁSÍëVéJOõ®ðôÛÍ¿: Bßìã}ªy¥çõ²ÅeZ.v£AŒ}üýÌ{ïNÆ;mHDDDDDTµ´ ¾é}ûöõÌŸ?EMMÍ`üJ^÷þššš=óçÏ_±oß¾=U°ìªaýúõ+ÚÚfì†U¦i¢cÿ~MÃÁE°ôñ[‰ªÕ¯² Jž@y¹…}wÝ R êdA8(³á¯ûöö•%¾ãUñZº…}'½¡i8|ø°sš¾Ý¯¶­­íäë®»î, ζ }üÏ­ÙaNèò'Òƒo°+”êZ©„½y®PZ,¨¹žÇÏ„jPªváéã,½={KÝhÂÄ Ñ^ ãïõ!5<,NX³ ‰ˆˆˆˆˆª–Ô7¾wïÞÞ¶¶¶ËÃáð)¥(òºª…Ãámmm—ïÝ»·—ÃNÊ{z~ã[Þò–+-+wºùÐÐúûû!Cžt°¼% ¿6 jå.”V©ØUÛ¼ÊN¿þºîò<Õ»~}~¥§Ę!¯c¼wßœ½°t‰D#Ãðœ^¬–eá-oyó•°û°Vêiú%­fâ,ýµ ½H^ß]oÈ«N §¶QðTâúµfȆººÏmž@Yhž– j ¬¶‰åµjX&w!ŒLPÇ¿˜÷óÐÈmHDDDDD4-Èo¾³³óÐÍ7ß|j8~Í/äUÂÝ×n¾ùæS;;;qÈ©‚©ß ‘¥KO?Ï­B4M]]G iºfw"S“w>»ðVÑ"¿µBA^YðX™0ͽ¨š²ðyÞ–>aná{B.~+±UC:”Æ¡–ƒÐ4Í>Mß ÷LÓÂé§/=@ÄÙ¶•ðŽ9þ§iP'R>êÝÖžJ\áÓCWkY¤×®ô´tð |¥O˜ë šE™£S‹–ȽA?«œÊCã¤êlK"""""¢ª¤}î¾ûîtkkë›ÂáðN5äUÂÝ­­­oºûî»Ón ö)Å3Ö¯¿~yCCcs®‚ÓDoo4MÃ9ÊZh.蕹ŠXßÖ ~-›,Mí£«Aiç ºyU¾2¿…ûåW+•›yát U¼m{!„@<W*8M4444¯¿nýrØ_³­9þg`b-ÄÕ1-VÅ+”>˲H‹5ü•E&QC±‰Ù|ÚAøîsžq/Õ©æ® ¿×ÛxHä¶$"""""V +ÑÙÙÙù¾÷½oemmíkîÄkRJQ[[ûÚûÞ÷¾•jªpnÔ¥¨0ãâ‹/:Ïr«7- ƒƒCȤÓ­E_䨏 T«ws•°È…©pBÔl%­7Ü•ù_=“®I¥’W½/êÚϳ¼pÑ`W¹é3ÉZiU¼½ =©…išvNÓw/^táy°¾Zd§«ˆ*Î’Ç¿Q c6ŽNøU„'<ÊmÐ<•´ÞV n¿\Ï„jj›}¬îù^ Ï&c¼°¹ŒUl³ºÐ€á 1WóÐÈmIDDDDD4­ZVä®»îJ/Y²dU8ÞápxÛ’%KVÝu×]¬Ü¥ p£­0€úE'Ÿ|–i™ÙpÊ0 iºgv•I)Á­û*Þ · j²Ødln]á;1—Ì«ê IŸJao \0©V^°WØ‹w‡À_ù IDAT¼­x$zš¦ahh¦eÁ2í Ê-Zt€zgë¨Ì cŽÿByhBoZz&3ƒO¥®w4¡Œ«·_®ô ¡¶pÈ›tO©îU'XS¿÷¾¾ç½Ê26â|sÇß+¶˜J«mJDDDDDTu´jZ™öööÄܹs׆ÃáïÎ;wm{{{‚CLáFY!áÚÚÚ†¶Y³gO17- AÓŽ6u»°l%ͪù¯Xp[a¥­T«/Ý`XóTñ*}yÕª^™7™›ô„ÇJ[ïëûV »íÆŽûŽDì^µ£££v¸ç„¤mmm‹kkkð‰ ÿ"ü+oóZ5ø­…tzKÙÍœ±•"·Ñ½ý˜¥OíîÙýN–¶® f_ÐÆ?ïí˜ÉCâ”›élÛ!n """""ª&lÑ@T”n¦ÕÕÕE¥RÁ)¥„®é¬,c‘ÒóÕSq dTßÎýês ½Ú ×¾Iæ=1ÜÙ)­]…)óÛ+Œ>#¿­„»%Æq‰š4Í>Ä™fn[ÖÕÕF‘뿪ù¼jÅŽƒeLlÉEZHQü~µ2ÛÍàó{莑 ‘°Í¯íƒ[)ì·1ÆjH”œï¢>3¤ñ÷jã!‘Û–ˆˆˆˆˆ¨T¬à%zý©±™@ …Bõ¦iAJ¦eWÆjš†‘PélIï+ °köqÂ{]fƒTµz× ùÜå¨_íë2ï5d¶ú„”BäºC´_P¯{êM˨Þ€a}š¦AšrŠ> ë¡zä½J¨à,yük­Á -\Žq›ëqJkŒÕ–Ágén€+Ǩ –žjpu89Öþ\‚Zs0(ãï'ÆÃ"·-Q©ðUŽlÀ§iZX­ÞšÀ¨ž,}a!œ, Û_™è9Ï)V]«†»>w»Ïöé¡›ÿ¾„r›È=Ky¾;¡,bT…¦iBƒif²ËÓ4-Œü€/0ã¶F'ý"~›r¬ý'oSçµåù•…ƒâ¬Ïí±+áS‘«îj…1òƒâR¬Æ Úø«fðpÈmKDDDDDT*¼D•GÐ-ËÌžVîžjž©’"Ëy5x’.Ok9Wü*wÕ×+ú½½¼ýT³ýy¥ð,Ëþ®¼¼ö¶Ò4 š¦!¶™=_Gå{cŽH&Ë^˜w¼J©ŒÊe«l¡„¯žçÚâÙå¹í7¼CîÍùý^Ó·šØ z‹­@º5ÔñGÞ_ЯDDDDDDU…/Q2- ² à›l41~>æN™fu¹ö 2¯òR}+²Èõ¼Pë…±zŸ1‘U×uBû} ¹I¿‚:þä׎Á{ƸMú €ôŒŽPtHç;é?Î~A®÷~ï¾ê½¯šÇŸˆˆˆˆˆˆ¨ x‰*̤3¦¦‰ð…B!ÔZa ë™—‚"=ì«ÙÍ÷qNf6¶Ò3Ù ç_+Æo"-wqyÏÏKW(%ôQ+k•I¶Lûô Hi™(¿¥kEŒ¿©ÕB73“Z¸(² Â9ÆExŸ+=m¤ï²ýÛñz»O”(oÔL½.¨ã¼¿@_¹ ˆˆˆˆˆ¨ÚhÜDC®1´RédÊ ÷,Ë‚š¦£NN¤}¤È§ÒóbyñÞ¨fu2·)ýÛ1ø®´OÕ·âÜÕpWJŸeȼw5!3ä ;à–%aI»åA*•J¹Û•öŒ;þi½~J^Ägñ½^ì6)K\¦ôÙ”}À{Ÿô|õÛKÝ'L½!hã¯ááÛ–ˆˆˆˆˆ¨T x‰^jœe|£©a5àMב%/t¼ Nä…gJý¥ZE«íÊÂSîåxkUð@QÚ ïãQü±åˆ MÓ ¥„e™0M RšH&“ÃÈødPÆ?ŠNè¼ÛXyÕÔžÀÕ éóaÿÈUzž/à?–RÙ?QZux1éšhPÆßOœ‡En["""""¢R1à%ª Ùp@fhxÈP>)%tMC“+c‘¿êQ¿.|ƒ]·úV©òU+p=‹“êc5à3Mš¦¡U¶–¾$ïuß`WäUìæ"?ÌÞ&rq¤Oe¦È …ÒCUxDa‡ÆÞX]VÞJ‰²b¸V´AÓ4d2™ìv´¤D"‘èƒ𙨬 ÎqÇ?U;«ì…úõÜõí{ëi‘ ÕV –gl­ÜE¨UnÏ qÕ}ųųA®gßjõ¯7¤.AªnvÐÆ_5 Ÿ‡Ä)×ïl["""""¢ªÂ€—¨2¸ñ˜ ÕßïV¾L&]×1 ³Ç](£Ÿ®÷v;øýRÝÇgƒ6'°“>a,7ÌĹ®VïBÊÜ)øÊû>½WP¼ÍCsÄ蚎d2™«„µ,Äãñn)g[WJÖ’Æ´~îÄ—îm™ óCUï¤f~¾ÒRÆ×½n:ûƒç"}`YbUïTH5Î ÒøûÙËÃ"·)Q)ð½þÔ˜5 ÕÕÕuP øF“£Ðt ópRY !¯È KÕŠJ¡„¸ÂSu놴€f¹•·ÂR*+Ý6R@XbÌ Ð~]‘{NÁûõ½¾“m}Òþ|m4]ÃÈÈÔmÙÝÝ}vÀ—)ò ;þ£ §ôÕ¤wB3¿‹åù*=®üæ…ýJÐ+”ÇŠ"n^¶ëSÅ[î¥eü‹y…‡FnS"""""¢R0à%ª n„–Þ½{÷n©„RÉÑ$ ­2RÆb}Z (íÔÐWJ„®Þ*OH)e~e®%€ìÅà‰¼eº-¤7Ôõ©&ÎÿÊ}%ˆÈZ´ÀÈȨS½)aYvïÞ½@¹>¬2(ãoÖ·!]ØÒ=}x¥²=Å8=rÕêlaºÂ­=ö„»Â ƒ¥ KŸ6 Â'ü• v ]Ó³¾-HãïçE¹M‰ˆˆˆˆˆJÁ€—¨r¸_ò¹çžÛ›L¥’jåa2™„®kX$» áÓ‚¡Xu¬ðVÚºÔ06/è+¬ÞÍ ‚³ÁžÈ[vaXœ8 5p.Rí+JlϰDœ]×144˲'Ù2-Éd2¹eË–½’È|ÿ¡È©e/Ø­Ô–E*t¥2v¾íÔpW}¬%ØUÛ3H¿ª_O_^©¶‡ð{Èíåİ#±Óƒ8þ^ÏóÈmJDDDDDT ¼D•Á°2R©Tj¨§§gŸZÅ988MÓqN/kÁÂ/(óLx%ÜŠ]@©¸…üÚº"/t[1ø_ÜêM¡Tñ '0Îoéà¼r‘¾¿iϰT[ ]Ó`†3¹–)%z{{÷¥R©!äz°VÊ$[%ÿPìÌI½ŠcT졼7´ž¶ Ò;©šÒÒA*•2¿/¯ßämc½7QÆjŽ4/ÚøûÙâì452Î6%"""""ª: x‰*‡y¥ :th‡%e6à€®ë8S[^z$•žŠ¼*Þ¼€U¹ž¶‰\e­ÚcW­Èõœª¯ÀR %ÄÊÄkù­|«‹=­$D1ÜòÐ hºŽ¾¾¾¼Óó:´À0ò¾@ÿPë9zÓBæOxæ­î–êyV‘IÑ”@WúôÙfáþàpÍÛÏ×[-,ýÚuÈò6âHÛª Ž¿WÀ6§Ì6g›U¼D•Á½Lا¼øâ‹[,Ë®<´, CCCH§ÒhÖ›±H,wBé»[¬ŠW·Š÷ÎuÛ-ˆ¼ð–àJ)” Ø­ôžžªÅƒdTóÂ?Ô•¥¶gX‚½©d‰D–• I_zé¥-Fœmì6A«¾#ÑS'ü"~Õ±Òo25å«_h›w1í‹T¿Wƒ_OЫ†ÇÂ*ìÅ[l-E²é4ÈúYAÿbþÈC#·%ÑxðUöP#O=õÔŽ¡Á¡¾lÈ'-ôë®éx£öÆÒ—èWÅë^Æ©âžjK‘w[6¼SNå‡RÙ«öëU_?o’6¿ JkOÐ;^{† j.‚®kèêîvNÍ·Ü€´ïé§ŸÞ;àK;Û:ãoÌ{ÓÄ–îÓ×÷{«°=CÞ;ßûM¶&­ü~¾ÒÛãYÝO\ú’!+‡Rú¤[öc„s_îB H!후¤„ïÇOÀ ¹ Ür§Û ÿÀÙ§’Ø^–(yrµ7…/E}¨Gö`h(MÓaYöûÙºuëcm[éï¸ãoÌ¿3÷?0¡r‡1äj¹q“p*k5ç«pÞ•°/ÎÐÛ×¥º×øísJ_eÌ¥Ú{×ÓžAZ(ìÃ\¢¡“¯‚VÛÔñ/æßü'½ ‰ˆˆˆˆˆªV`ÞuëÖ5^~ùåGË}Þã?ÞÊ€—*\ö$PCÿó?ÿóÒÕW_ÝFOvC¾}ûö¢­­ W„¯Ä“##ô˜ ³ÃZ™».¥Ï#(¤² ç¹n ç¿Ùû¤Ì[¾»,7<̽"/°3ÐÍ»Ž’ÃÝÔà¯f\ MÓ°{Ï«°,·Y` ¿¿ã¹çž{À³m3AÿÁEoAÓ‡ Yé²_@ºa®îZÎíÂþ^¾M:¡nÁNÐ[°'ånsCdµíB¶°§zWxúgûó–³NZç¼µÆßën_PÃCㄤmHDDDDDTµðÆb±ä‰|Ñ âÆ\)ÃŒööö‡Ï?ÿü¸§šwwE"‘@s´‡Þ„Ç3»P5ØÍ²ùÑ,Ôº]™+ÏÌÝŠüª] !”èXy=u²47,v×ηŠß‹2Z3À¥µ—¡94ñxG:@Óì4B´··? `ÀÙ¦)TnõfÉã¶`pÁ_!Úñ‡²_DÀ T¡Tó:a¯ð»BäŸ-ÈþoÞ y¥g̳¯éÓ–AªAïÂ]^´¢¾%èãïç€_xò+gV´h4†äÀIS¼øÃ~ àë†a¤‚8ˆ‘Hd1€w¸ @€•Î]ÛÏö#~™H$öq—'"""¢éˆ=x‰*‹÷4}cóæÍ  H+×OtÇŽÐu×Ö]‹ºzñæ-½ =‚(2ñV~Ýl^·¿®Êy&äÊÝ/ *r³¯i!¯÷¯7Ðõ†»ãUïÎ3ð¶†ë é:þò—íÙí$¥…á¡áßÿþ÷0Pù§ç—5þƒ§ßk½xímŠüžºjÿ\Y8yžðöÒõ™`MX…¯åMÔ¦M7AxÅ ~щ"aOO•08<<ÜóÊ+¯>dI RÚ!ßáÇq´§M51\Swm‰;¾Ú Wxªe '[ËZEÞ¤\B™|MH¡L²– K ñ”`Xø¶eÀ„Ã]x[ýÛ«‰áhw·=¹–ån+‰Wv¿úÐððpìþ«IgÛÊj1c&§Ý0©ƒhÞ¤kjЫ~uªl…'ìU¿JO¸+ÔPXY¾T«öâ`¸ ÀwëgVËøûù €xh,Ûζ ‚UÉkL‰H$ŽD"·x @9íÅžˆD"·G"‘0?DDDD4]¶Eù瞛ÃZªNnÀ—†}JùÀ=x¥g/PwxÛsÈü‰×Ôï'r@ÏD"µôºjÿb6x+ÿÝ+k¯Þ ÷ÛV%¯1i‘H¤ÀoP^°ëõ·VD"‘ë‰D?TÁûûc°«ÐO”?%‰+¹å‰ˆˆª[4U.Îiú†atíØ±ã×n¸gY}}}رsjCµø`ä#С—´ÐÂJ^¡´f¹jÞ¼V ¹6 ÙŠ\·*×¹Ö Ù*]‘«îÌ«Øõ„¹ÖäÂ]:>û(jCµøË_^Doo/,gI)±cÇŽ_†Ñ…Üéùn¸'«eüõšZ$Îý$¤Ð'õbBú·W>­¤§å‚÷qrœvÒ3ÉÚD+w¥Ð1|þ§¡×Tåø{½»Ÿ,•æWÎ6 ¢€¹Ö¸À§<`¤Ú-‰ÌðŒî–ò‡šKüÁY&U¯»è~ÿWTùëÑ Ø€wÛ¶m²¬X¹—mÛ¶év »W¨;ÙVÿæÍ›ˆ”–„”¤eáù-[aN¯;o«¿®ä…燼€°¼}x ƒ_ox+dñ‹g{ò*½]EAØ+Êw`}ä,qâñ8ž}öYXRB:½W>øàƒ¿ÐÜäZ#Ü+küeÛ™>ã]“~Qµeƒô„¶ÂÛC×Ós×Û¢Ay®ì kr娣Ëof-¯Æñ/æK°ƒjg×p¶U`†ÑeÆ6Ã04 ã_ ø@ €·¸¯ŠÇî_œ?Îc¾`–s¹{œÇžï,“ª×‡ü€éÖ’ãìqî¿ÀZç˜qw""¢êä Þ¹Bˆþr/°+cˆ*ƒ™°«¶étúèÓO=½É´LKJ KJ¤3<üðø.²ËÃg•ü!¯§/¯ü·ðTï ÿïÕêLu’¶‚~¾J¸ŒÒÃݵgãú¦ë>ø Òé”îI˜¦e=ýÌ3›2™ÌQ gšÈ{¥êÿäòw#5k唼¸Púî nåáoÑÊ]o¯ßIÈÌ9™³nªÖñ/f/€oñð8®o9Ûªª†1bÆý†a\ï„;ª¦õ‹D"«lça)ŸO$}‰D¢Àß—°è β©zÝàÿa…¼‰Dâ¥q2’H$I$÷hæ.BDDTý‚ðÖHYÞïéÎãG8ìnÀ—‚=ATÿÖ­[Ÿíèèx,7‰”…îîn<ýôS¨ Õà“ÍŸB›>«äÎÙWËN˜%è¿z~½–êZÅ‚]µ¯(9ܥϧÛ>ƒšPúÓŸpäH',KÂ’,ËBGGÇcÛžþYØÕ›ƒÎ6 ÚäZe¿ cäÂ/Âl˜3%/îVóºã[,äV~µ¯,ÒÖJ˧µï„Ys¼äËÐkªzü‹ù€]<<µËÙFAF£G¢ÑèóÑhôáh4úåh4ºB}€a/†q%€;ûw5(å#‰D"ûs\"‘˜ÂeS°½ ÀflÉaÿ{7¨|˜›„ˆˆ¨ú9àh>0‹ÃNâ†|£°ûˆöÞwß}?L QC¾çŸíííˆÕÄðÅÖ/¢Q4–ùaʯæÍ½"wZ½_õ®J_‘ß›Wúµf( vó^¿Z#¾<ç+ˆ…cxùå—ñÜsÏÁ²$¤”–Äààà‘ßýîw?ЋüÞ«²ÚÇ_«Ÿ‰‘Ën…ŽLùÁV¨=sÇèÍ[p¿œšP7»AÂQ$¯ü&ôúæé0þ~’Þë|¥êÝ6s¬p€[¼F;¢Ñè§¢Ñh6À2 ãn—=À‰D"+q|{ƒ^á¼U·«<  ‘›áH$rq$¹öD£DDDTåð®^½z¿åÅÎãùC‰©eàôb>ô§ÇÿôL&“VC¾ÍlÆ¡C‡pRíB|¶íó‹òÎT,¬æEÞDlÙê]ïY~—l‹OkŸ`·œp7,ÂøÂœ/aAÝ8p÷ÝwÔmÉdÒO<ñÄwFFF!×{5ƒ)i ŒñGlFßô5H½vÊߌP.¹‰ñ|*ºeþã§lcèµH]q+´™‹¦Ëø³ Àçyx,ðygÛT«…°ûÉîŽF£po4 c ì Ù¶xÝn¬’× ã« ÀüÀóðŸHñbOhšÆÛIw¶ÏSÎ…g}M‚›€(ŸS @ì^jó.¿üòw¯X±âã–eÁ4M˜¦‰ºº:|ä#AKK ÚGvâ[=ßÄ 58á–j&&Ê8zȱ¿8ô4j|qî—°¼þ,ôöôâû?øFGG ë4M‡®ëxùå—ïxòÉ' @€!i»÷ê„Æ_t¿ˆº'¾‘JTŇ@ÖF‘¾òVˆ9+§Ûøå?¼‡‡HÀÏaWïR4U÷Ïì€f€“¬p ìpKõ35 cÄYF€ÿpj±×1 £"î‹D"¡´ ÞD"ó<·ÔÏöŸ‰Ä••ºÄb±unkü&a€OÇãñûú¹À¬+ã9Ïžh¬¢[³•±ÿæI$b2Ïå?DDDÕGã& ªxn8•rªþã?~ßþýû´+í*ÆÁÁAÜu×]èëëÃò†³ðyßĬÐì ¿°Pþƒg6oµnÑۤϲÊ4;4ÿ´àÛXÑp6z{{ñƒþƒƒ §rÓ>=¿££ãÁ'Ÿ|ò>䟚ŸAðý ¿˜»É7VãÜàç"síÐçž;Ç,ð4‘xÚÙUÁ0 Ó0Œ”aÛœIÕþÀ|ŸÐ£<ôfO9Á. Ãèðvä÷Þ Š¥Uò“ñ8>á.,†Í¿€]‘»®Ìç^à7˜F¯ÑôØ€wÛ¶mšeYQ˲"e\¢Û¶mÓ9ì@nH•†sª>€îxàŽ£GnµC.;äë÷ãöÛoÇþýû± n!¾=ÿ;8{Æ9“~¢Øj ‡1þ›¨sêW⻋þ ëbï¾½øîw¿‹þþþ¼p¯§§gëƒ>x€näNͯ¦ÊÍ ¿Ö¼ékï„97¸È[óÖÀºî.è-‹¦óø3 »ºó•i|l|ÅÙ£Õ¼’Nà{€%°+w]«lvûò†±ÁlßÑV%¯QÉïoaÀö‰“`»ïšÄ2ÖÁ®î¯öŸýON$¢ØÀÙü1šˆˆ¨ú¹‚w•b@a”qPËa#[Ķ IDAT§€²`O•‚]¡uLJÙyÿý÷£¯¿ÿ%K*M%ñƒ|Û¶mC,<_=ékøëæ› èw:ÞÓú^|}á­˜ž‰-ÏmÁ÷þùŸ‘H$²ë)¥…þþþ—6oÞü )e'€c°OËO9ÛÊšîã¯74úú;Ȭú ¤ÎøK¡ÃZýaˆ·ü3ô†fŽq½þ ÀžixLÜã¬{o5­T4•ÎeG4ýa4½Ê½Ï0ŒAÃ0Þàï•§œàß”ï7!Øýx‰æÀw§¢êzß4Ø^‘HD»x‘»Qõ l¦mÛ¶5®Zµ*QÎDkRJ¼ð ‘Õ«Wrè)ÀŸY»GcìIDÚjkkO¾æšk¾‰DV¸ýXÝÞ¬—_~9ÞñŽw@×uìÞ…Û»ÿS*z%†âSó>ƒe Ë`š&î¹ç<úè£Ð4 º®C×4hºŽÁÁÁ—z衯&“Éا. ºOÍŸÔøËî—¡?õMˆøþŠ^I9s1Ä¥_†˜sÇ¿tó<ŠÊ? }ª¼;Ü=T +ãéÁëçYŸ0 c»òœ/¸MyÌ{ Ãø/ç¾+<æ]H÷à=»bs<&€X"‘tž×àh‰/s8‘H̯Ô} ‹÷ãU<ÊÏý?p“ç¶}°'QìƒÝ‹~1€3Ì(²ŒØ­[þ«’W”=x‰ˆˆh*Â@bÀKÓüsë†|3D´…Ãá…k×^õ¥¦¦Øy¦iÂ2-˜–ö-\¸ÿØÇ1gÞd¬ ~wì·øå±{0l WÔŠÕkõøë¶›p]ëõ¨ÑjÐÙÙ‰Ûo¿¹pOסk:Œ­>úè7ÒéôØážáüBWíáÞ¤Æfò¥_Büù§é¡ÊZ³ppî!Î~7´ÇZÜ{ùjö4ì¶ US¹[BÀ Ø•é5 ã§Êóþv/^8 à Ã0ûþOßÒ*˜d °»¯ÂîWûؽVKQ铬uàø¶Qè‹Çã-ø8œ `‡g¼ÿÀÎb?ÞûÜvÀõ°áŠ6EïɉDâÀ¯±J/^""¢ê i¢¯¦iü¡†ªå³ë ùZ…ó.»ì²Ïž5뚌i²L˜¦]É©ë:Þqã;p͵×@Ó4ÄÓqü²÷ü¡ÿ÷HÉÔëº2aÆ[š¯Á_Ïzb51˜–…î¿¿øÅ/`š&4§jÓ ÷Žö}ð©§žºÃ9-_Tk:ô]’ñ—ÃýÀ ?…Üy„ùúŽ?ôZ`ùzh«?­~&ÇrêÜà=Uº~?‡]•WU=wK x]j¥n3€Ý°+àë†a|Õ¹o=ìI¦²*8à½À—óËüc"‘¸¥R÷X,v3€ïâøôâíðÅx<þÓ|¾àsÎõ¸{¬$=·= àÎ:Wü!ïƒÝ®áGn,ñy«‰{W¾çaOøÆ ä›Ä•ï¿ç'Ì ¬,^"""šÊ€ ¶mÛÖ¼jÕªc x‰ŸálÈW  ÀL­ ,XsÖò³>ª ͵Ã=¦Ó—Õ´,¬Y½ïÿûqÊ)§@5‡ñDü ü±ï!ìÞq\ßôòúåX×òf\:ó Ôë3 „Àž={ð“Ÿü[¶l®9¡žsJ¾¦iÈd2GÚÛÛ¿sèСçaWmöÞl,‰éîMÙøËÔ0¬ÝC¶ßtßùXÄœs -;´Ó¯†VÃñ?ŽVøOg|=vx/pºõDyÞ=®7 ã¥h4ºÀ/`O¤¦zÚ0ŒKœç΀ýÇ·鵆a<èÜ—×Ë´R^ˆD"O¸´„‡ÎM$]Îsuóœ'‰Äe<$Â0ì>»K`ÿ1c,nÀ;àoÜ´•e‹"""šÊp &QÁÛ°zõêa=UÙçØ ùÂêaŸ²ß‡ç¯^½ú£MMMWJ)5w.ûbÁ²L¬^³ï|Ç;°fÍ! !Ñ“ìÁã9ü9± /½„¾LߤÞ`s¨g5®À¹‘ÕxcÓÑžå¼ðüÖ­¸çž{°uëV¥ÏªMˆ{ÖÀÀÀcÛ·oÿQ*•:äü2o8¿¦ÀpoÊÇ_&ºaíÖÁç ;·ÓluZß mÞJh ß}Ñ%‘Ùÿ§À—`WµÕì½Â®Bþ Oî*ž€÷BÃ0žUî[`/Ýó´K ÃxÚyÌ/¼Ë¹ýû†a|Ò¹ýSþÅ}B…¼«aWoŽg~"‘8ì<§É9&è㎌ŒÀ0 ô;†®în>tûöïÇ«¯¾Šýû÷òÆÏ⤔ #ñPÇŽ{S©Tì`/\Õf<%ŸãÏñŸ çÀž©þj^ç÷’†]±ûm™ŽƒáÓƒ÷݆al‹F£KaŸ‚¾Úçi}†a´8Ïo†ÝªLÃ0B>·W|À ‘H¤ÀoPZ?Þ±< àúD"ÑÇ{ $Ä0}Þ?Xw_ò¡D"ñfîfDDDÕT¤#GŽ,imm}! EOÔkf2£··wÕܹs÷r× *øl{{³Î@®¢³) µÍŸ?]$Y‡O~=ßl*•ê0 ãáC‡=dšf€ä*6GPØk•áÇŸ¦Æ\ð~§žà×ÞàßÜ û´ìi+Ðvœ_&[Ù[é"‘HÀ}s"îð‰D"‘âG¤í‰D.p€ J|ʳ¾˜H$žä^X?p€íÜDDDDåýò_ÑŽ=zF,{¶¦¦&v¼^#NÇûûû/˜={ö.îT¥ÜI C°ƒ¾Ø=Zݰ¯Îiü­­­Ë›ššÎ¯««[‡kšV;oÀ²¬d*•Ú7::ºc```KooïäN¿B.ÔKÂ>eÛ„ìµÊñ§-»êgX {¢¶™%>¿öi¯xÀó°û¡&¸i E£Ñ0ì ó&N¦6Y]þ À½“·fgŒD–¸ÀU°û´®tîÚûC¸7‘H¼Â½)ðÖ;cüSn """¢Ò¢ÉþáÇÌš5kK(š3U•¼îr2™LWww÷ùóçÏ?ÈݦÁçÝýì¸îéûneç åR/„hhnn^ÒÐÐpZmmí‚P(4; 5kšÕ4­^FnâSJ™²,kز,#“Éôe2™îd2yphhhw__ß^)¥ä(·RÓ= ß­ØXµÉñçøWšØ­bÎ~"”±=ARìОˆ¨\aØÕì_ç¦ """*ïþ@8zôèŠX,öhMMͤÛ5¸ÏO§ÓGãñø_Íš5ë%î 4 ?ûve§†Ü„\!äª;Ýà¯Ö¹ÍýÞ}œû\5౜‹;–Ü¥aWfºß»Ušåâ>—¡ÇŸãOD4}] »Ý†ÉMADDDTú/ùÑÞÞ;å”SvÕÔÔÌžì²Òét÷k¯½vƲeËâÜ hšÜã€Øéž‹þ¹·iÊEe)·S ñLÏÅ},À`ãÏñ'""""""šÐ/÷ÒÕÕuZKKË#¡Pèär+y•¶ ÇŽ»jΜ9»¹ åÔ°Ï[å)|¾z?nHgù|µ<÷«'Ž?M{÷îmK&“Gä$“É#{÷îmãV$“7ÜS«9ÕÓõ‹]jà_ñé ÇŸˆˆˆˆˆˆˆ¦›×^{­5•Jí–RJ˲¬±B]÷þT*µûµ×^kåÖ#šÂà¯ØE},qü‰ˆˆˆˆˆˆˆ >|x~2™ì+äuoO&“‡žÏ­FDDDDDDDDDT!îºë®šd2¹Ç/äUÂÝ=wÝuW ·Q…9|øð¼d2¹C u•pwÇáÇçq+U¨ÿøÇ5£££{Ô ÞÑÑÑ=?þñY¹KDDDDDDDDDTévîÜI&“Ï;•»Ïïܹ3­BDDDDDDDDDû÷ïoN&“ßÙ¿3·Uƒø¦eµÊõ&åz£÷±}w.ÜbDDDDDDDDDT Ä ¾iY€kÌÐ `&€yÒê Øà¯Ä6´¿ª<_‹mh·¸%‰ˆˆˆˆˆˆˆˆHÅ€÷ˆoZ¶À2nH«ñð!3üÀ#~ÛÐ~À½³ïÎ¥¢ùc¯HnU"""""""""bÀ;IRÊ•þ¥`à q™{=¾iÙ?øÒ_bÀݾã½ y‰ˆˆˆˆˆˆˆˆ`À;iRÊË<^°a…ÈnÛø¦e+¼¨Ü=»JºÌ)á¥vø¾eYw2Ü-ÏöíÛ7®\¹r#·U¼“TJÀ ñMËþ`1ì v€{»ç¨°Àr—Êø¨×às± í{¹õǶ}ûöE~ àœ•+Wr_'"""""""¢ªâ&8a> à@lCûá±ß´l€k|ÀjØá® @p=€™ñMË>ÛÐ~€íümß¾ýÓ6hâÖ ""¢ jll|ÀùÅ~Ü\Å­DDDDD¬jœ¤R+xUñMËjÌР@į"7¾iY=€¸ÀLäWòîpSlCû å7íÛc°«v/Uog/U‰Øàg> Åõ¯N3`ÏÅPÔàà ¾!""""¼“UjÀÛwçR¡iÚ)npì€7à4a·l8à›-Ëʸ•¹ñMËbþÀ[_uý€¿‹mhßÏ‘°mß¾Ýw<ðQÀ„| @€ŸÂnñt€Ÿ+ù4€…ÝêéÜû¬®?Ñ4Á 'HóÇ^‘Îdk·Á®6 +w7Á}àãz4M»#¾iÙ¿Ç6´ïmhÇ7-û€¯ÁnÛàºÀ—`W²Qõø2€¯8×ÿÀ»ŸTð €_ ×’¨À×¹þÕ¡±±qÌ6\¬à%""""и N˲~ `ùá®J°`W«|ÀÃñMËÖ@lC{/€ÿ»’wÄý¹ÀGâ›–Ýß´Lç&""ªK•ë3ü?{s&€ÍÈï7ןˆˆˆˆhzaÀ{9-~ë|Û `+€nØ­†a·ÌÐ`½p €?Æ7-û`|Ó²–؆ö€/¸Ï³è 8ÉQõèñ|ï÷Çaݹ¨º¸þDDDDDÓ [4œxÿà9)åf!D€ÛÐþZ|Ó²s`‡´7¸ ùý‘¿`€¯Å6´wÅ7-û€õ°+Z»¢ec|Ó²Ä6´á&&"" œ0ì³w.ð,ì³t&ºœï8À“°Û¤¸þÓGccãsÎúûÙ>88X‰E³,0öpÆí(€~‡ô"WáªÐ ` €(ìɋðψ°ßyn%™”²¤õBHÏóJZ!D?ˆˆˆ¨ª±o×ä » %L²æê»s©p'Oóß´¬Àû|@L¹kÀ›üwlC{&¾iÙFØ“¨ùblCû7§óxp’5"" ¨Oø—ã°Ü¯"=i§ûúû*·occã çgÆ’Ÿó:ºÀ›`”n ™p¦`Ošg:· Ánw‚fÖÃ>ûMsn«P ;ø­ý‡‚Îó;ü3€{*äwˆ×eý…÷€ˆˆˆª+x+LlC{À]ñMËžƒÝoî4燶zü€—<àk”§_à›ÜŠDDD3cœû÷Àþã«ùx€Ï9×'º\®A0 8š|Tù~ØùYw®óû‰D.Ќ胀Ö*ë'`‡˜-ÎóÛà<°+Z5ØAê×P¯”rZ¯?M½×-à•RƬTnÚ.„ˆWûŸùÑ]Mò£c¯wßKElCûKñMËþvå‰{*Ý*ë`¼{ì€=ÁÈøelCû£c½ööíÛW"Wñ_¹råö×clß¾}ìÓÐ\ûW®\¹ŸG""šÆ~»Ïþÿoïþc,;Ëû€_C!F—ì•Ä” Å„øH´Q‰ˆ<î4j^£D¢©ÀK[¹‚±%MãªÐªX«m-*ÆD F X'²@I[fUIÚœ µ»TWt§º©Áx}úÇ{®w|çÜ™;¿vïÜý|¤«=÷üxŸ÷Þ«9óÌ3Ï{bà¹Ï&ùéÔÏÄW“üJðP¶/@–~ÿÄcÙkÕïu²9ù‘ 5Q9IÄoÛ³S“–Ïï÷ûfj²s²nÅsS“–ÏÉÓ“ž“ó>{êš%I»¯Q)e³ëº6~àî/ëD]×­MmºXJYŸÚg%É™$§’Ü:pšK©‹-¥\ÜõWSûÖ¥mñl¹ö®-º®;ÝÇ~ûŒ¸×“¬oûò/ûî$;õOØ&¾™ä'ïýÒ×.?ð²[’±»¶‡q¤÷LÓ4çæ¼F·Ï×ôÎéÄ6\7'ù¹Ô…V_1õÜßï¿÷'Éó’|(É+“|9É[R+Y“ä¦..¶Õ—û{û“<&þãe4½4ÉÛiŸcžàýb’—mùÿ§’¼15ñ›Ô Ô÷¦H”]~nÙLòû÷ÇÄmIþC’[¶l»'ÉG!ø®ëã/¥üiÿüžã/¥|hËùã/¥|4ÀÒ9òoŸÜÝÈpåên.$YÝ-É»¨ Þ¾j÷l†+Vw‹ûÔ¤š÷ò/ûëI.Ÿ¼÷K¿?ÏÁ}Rõ#ûˆñR’SMÓ´Mð¶m{²ÿ¡ê®Ìùƒ©‰ÞË»\K‚€ãì·3Üb ©¿à}¤ÿúcI~fËsŸOí¿Ÿ$;%+É‹ÿøF·¥®ÅpÛNûóo»å烯¥VbùÑ$ÿ)W«T§+Z¿™äûrµçìV6É£[þÿº$Ÿ^„à»®{Zü¥”ÎØo®øK)O »-þRʧ,›ŽøÆå ÉÝôÇmljí+w÷“ÜÄÝöó—“÷~éß_ƒänR«l7úäìþïÖëñ9Xr7©U,°›3;¹y%W“›IrÇÔó¯J]@)SûMûñ£Ñè•£ÑècIþ0»$w—ÀÖ„äŸKòšûýNjæIró;ý¿“DöË3œÜMêÂ|Ù²o·¨ñw]7)e×ø‡’»Ç ~àÝtÄç_Ëþ“»·÷•°×ÛÆ÷?—Ù½fÏ÷ÍŽ?±×kömvKîN®}a‡ëž;„¹º}Îqì6·§VÀ2z,É'g<÷ŒÔ–&¾0õü—“<Þ}Ë×øä ÿ×S+C¿Þ?=þ$Éh4º{4ýv’ßO­RžÕw÷ó©m þÒUâî;ìþß'S“–¿Ñß¾%É$ù®$ß›äÎ$w÷û–$“üŸ-çùÙ$/I]tì{’üp’_LM’ŸNýEÁd®ž»Èñw]w¾ëº·t]÷]×}W×ußÛuÝ®ñw]÷’®ëžÛuÝ÷t]÷Ã]×ýb×u‹?pˆŽ²EÃf¶'87S“uçJ)ýq'SÞ:“í•—J)+;\{%uq­ýº\Ji»®[Ïð]ƒíúk¯f EÃÐ9R{ën ¿¶Cì÷•RÖv;y_åÚf¸×íf?¿ç¶¶;hÛv¥¿ñ›§½Å\-Ú¶=›Ù½–ϧ.èvnฦã=³æ¡išµ×Ü:®&Év»sèÐÝÚ?À5ò†þ{ò_Úþ¦\ýEç‹RÛ¼:µéës5é9Ôƒö³©ýk?yÄÿìÔDÖ“S÷¹OfÁ«ûv Ó½R‡¼3ÉÇ“ü³þ~çIž3›:ßqiÑðõ>æ®íO->yF’?3tÛÝ¿¦ÿ9µâ÷EÙÞ®`«'ûóN ZžÑßkÞ¿Áw]w]â/¥Ü`ée‚wÚCINïÔO·ëº3NÎ%ÉÓÉÑC¾É:áê×Í$+³Æ=g‚w×$íÉå¯?ѶíZ†µ’¬î”Èì“«ٹĮ Þ>Ñ:k.ÞÞ4ÍÙÝ^‡þ³ªŸ_Ü4ÍÅýŒ¡išâãÀ‚»;É¿ËÕ¶Im=ðòÔ>›³¼0µZñy[¶]Iòw’üÚ ÿ3úxÑhôþÔÁ_M­Þ½yÆ®OML~fËëüÈx<~ÁÀ9ŸÈì àEJðþQjïäo¥&#ïÇ=Ir~'Éwçj‚²¤V|¿?µ á “Ûèþßo÷çš$öŸØò¾õäE¾ëºë)åKç¦ktK)§vKR–RÎföoÕWðkÒ/wÈ®ãÞÅýóTà–RN§.*6íDjµÁL}õîÐ>—²Kr7Iš¦iûùÝ<àTΊóMó$wû±lì0–5Y–ÔKS«SŸ5µý–~û¬Äß(5)ú¼©íÏHòÑŸ>®û¿äé !W«v?•äû<î7÷ñ“üÂN÷“ý½êÖ×ùÛÞ £ÑK³CrwÁ¼¡ÿY¤Ûòš]N]ì‘Ô¤þ£¹Z¥ú}üïJmSqk’oäjoÚo§&I'=joJòœþQ’üïÔDù±Ž¿”ò®RÊ2Ä¢k‘à½Ô'/çµ6cûêQ ®o±žáŠÑûX5|¾”rfÞûyêÖBˆãÇIDAT»Ûüš1þÓó¶ 蓼kû ´¯œj3q_Ó4ë{9W?–¡˜ïéÛJÀ²¹7³“˜?‘Z¡û÷rµ'í-I~.Égö"eÏÊÕ*¿eŒÿ/¦&®&«%µ¥Ã·R“Þ§Rÿ”}!F£}ÌÏKò¡$ÿ*ÉŸ ìz%µ%Å«¶l{8SE}«‡O£÷ü$ùóýkõÿR“’ÏNí½û¬þñXÿz~ Ékû¯'¾šÚ¦àsIþo’ÿ•äOS“å›[¾þŸIÞÛ¿gÆ‹|)eOñ—R^[JùÖ–ã÷)e`)]‹ïÚov.§¶s˜vòˆÆw6à ‚Ÿ§òvgé˜[»®;µÃ1CÏï«açÖWÙ^Úg¬§¶]šÕ7wޱœKíÙ;O¬pÜ}q—ç_’äé{]jÿÒ™šàÙÉ#Kÿ—¶uqªLòîþñÖþ¹7¦&‚Òx<þâx<~Íx<~æx<þòx<~ëx<þõñx<+©ÿæ$KíÍû ;œúþþž÷y[¶}ÀG `ùuÞvÑ>â¾»rp)år×uóî~ò°¯¿ãW_ü¶]=¢—Q‹–Ý{R«3ŸŸúWG“*ÎmÙçW“|6ÉËS{Ôo¦VÞ(ñÿëÔäoRÛ3ü^’¿’§ÿyþßJ­Žýñ$Ÿ9î“2N$¹¹|(Éë’ü£$/œÚõJêÂu¯Ú²íáÔ¤/Kæ™7R°× ïîÄa$¶/d{•ñj¶W÷U"_<àµ÷:+ÛîÉp"ý0¨à`Ù=žÚKw«'¹;µWëÌÕdÝŸ¤&Do´ø?Üoy’¿œäg³½÷êo&ùFj›†Ï,Á¼ŒSß7'ùÉ$·¥&ú~`¿Iåîc©}ß·CU0ÇØ “àíûîžËÑöݸ¼ çØ¯½&¨o½ÆãSÁ Àê§Å¿Íçj«†iO$ù¾e ~<_F§&¹“Ú®áãÙžàMê"m¿”äÉâl,§›n X×3œˆ<¬¾»pÔÞ™š¼Mj¢÷wsuºGSûìþØx<~l<¿Gr`ùݼ]×I]xÚaöÝ€#5¿2V“ü‹$?‘äE©½—+ÉÃÚ0Üx–>ÁÛ÷ÝýàŒ§³ï.Ow!G×f¢5½Àj<1Ék¶lz«Y¸q-u‚wKßÝ!‡Ýw÷Z¸VÉèÃXÄìLÓ4>bpt–½ïz®OßÝÃHÞ1°m¨rõÂ\¯‹˜]:¢9v°´ ÞëÜw÷ä5 u¨ªwå€çÜëñ¯óÀ i)¼ Ðwwå€ã_Ú>£¥ÄжƒVÏîõø‹ÛV}¼àh-]‚wAúîÞÚc¿V¶]˜±ïPÛ†;ö{á¶mW’ܾׯÆÐ¶í«xÛ¶=Õ?V}\àé–±‚w=×§ïî´S‡|l;cß¡mÛž¾†ãÞ8‚9˜8›äÓI>×¶m×¶íåÄKe©¼×¹ïî´ÓûŒ¡Épí`UrÓ4—“»ŸÁF?`X4ËRÁ»žÅè»;íöÌ™ì«}?2ðÔæœçŠóD’Ý(ë[!œ;H MÓ\Lòàc˜;ÉÛçÝ3æb}ŸC<åãÀ²)‡u¢®ëºÍwî§z¶Ov~nÛ`K)ûžÉpåéf’•þëæÂ¼XJ¹8ÏX§œOrzúØ9bHjòÚ<ƒkÛv#ÃÀIM¾žë«}'­«©=wï˜ãô盦YÝåú+©‹Á˜ñz¬5Msv‡ãO¦&ªß6k.š¦Y›cº×?¥½Ëd¼™/Ay¶%[gŒu3Û“œ›©U²¹ºHØjêbh³úÞ^(¥ì¥òu%³¬{qi`L»&xû1œÎp%òÖsŸëÇ9™‡•~.Ní0ö MÓÌÛn¢›3λ' o8Žži ŽDÛ?¶V¢žHrOÿ˜ÇfjÒsnMÓ\ìVûÜÆþ`jâõÝû9¸išIßßYU¸·îðÜNs±— ç3_Ò¿É[SÀõt“)8¥”3I.ìóðÍ$«¥”Ë{=°oApg޽z°išÓ½iš3Iî;¤©ÜL²Ú÷ø×úœû­x§pœIð­ÕÔjÒ½¸d¥”Òî÷¢}’·Éð¢gC6SÛœÞaŸË{ÃZj¢ùÒæï¡$+MÓ´{¼öúœó¾â- Àq&Á{„J)—K)«©Õ¬»UÔ^Jò¦RJ³ŸÊÝiMÓ\ì¶/î¯~j ›© Ô7¥&QwkUÐîc ©‰æ·go‰ÞóIîlšæTÓ4û‹SIîßmˆÞ¥gÅ\]×LM:6yzbq#I[JY˜^°mÛžKr×ÔæûúªÜƒœ·I­j^ÉöäêFjïß=¶cØíš+© Ù5INö›/§ï“l‘5Ž3 ^¶iÛv#Û){{Ó4gÍ,-–@_{˜N]ÆLÀby¦)X жmRûÜ^LMÆžÝO«ƒ¶mO&¹}à©‹¦‹ Þårkjk…·¥öºÝSÛ6³/.p8$x—Ã¥m«û<×ÚÀ¶ S ‹G‚w9 õǽ§mÛ•=¤mϤVO;gŠ`ñHð.‡Y Øs}OÝ]µm{:ÉžÚlšfÝÀâ‘à]ç’ll¿=ÉŶmÏÌJô¶m{ªmÛ$™qî3¦S1Ë¡o¯ðÁ]v»”äâ–ÿß±Ëþ5MsÊìÀb’à]"mÛ®'¹çNw!ÉjÓ4—Í,,&-–HÓ4§“>>ttt’’’ŒŒŒ¶¶¶000¼¼¼”””BBBØØØ¤¤¤ÀÀÀÊÊÊÌÌÌZZZ$$$ŽŽŽ~~~rrr€€€¾¾¾®®®|||–––hhhjjj´´´ÜÜÜ```²²²°°°xxxÎÎ΂‚‚XXX222ÄÄÄäääøøøðððæææ***bbb^^^èèèppp!þCreated with ajaxload.info!ù !ÿ NETSCAPE2.0, ÿ€‚ƒ„…†‡ˆ‰Š‹Œ†)ކ)4)ƒ3•„* 5 A9@ ¡‚+´&<…‰ ´Æ ¹„)KFNˆ!ƵÒ‡% ˆ"×!'”‡Ïш ,× DÊêÁ‰# 6. „`xUè-TØáÁA d ‘Ãé1ã” ‚†_¼r`Á…A”»‘¯Q…'LìpH`A¹½Q0BKAÎ Ê1ÈÙ” F ` Ác.pdld€‡Ë(Ø`b‰“ Ràp"Œa=xa!/{‰˜6ºƒ€BÆ?6Å%bÆRu$`2$.‡.6dCÔE c!F(C€AS%hE¡‚¨‚‚Ì´@ø  ’$'rbPƒI )D vÔ £ÇŒºŒÀ(ÑøwFj™2µÐƒ3>Xèp@œ cF<:à¡IˈT¨#´JD'7ݼ-üMKšÆ %&Ù`€»£@!ù , ÿ€‚ƒ„ƒTT)…ŽŽI((K/™4FFš¦K  I§…AFL¬ FA±‚ ( XMDF»%½¿$«:(NIÒ .¯‚Œ<»<(0 6[C¥©I™B!Ê$EZ 3ÜQ¢8È$ÀÊ8V+r`B"‚on)•‰‚’O`0€àLÞ‹' "(H€¡c# † B Ö?“„081²…[0 ¡¨ ' ÆBÁŠ~`+A“³FB(€àŽ¥M;zˆ"ƒD<šô”ÀbC ¡§t1J€'UœjÞ̉Ê! äÔ$à€ êîuË §8ä{eÁŠ#Q²”‰%UPNÂ(N°Ò£D&£°š€$sĶ`G˜ÀeJ& 8D0± A©„)жƒ’K¾jÆEʽ@L¤:D8'‹N [ <-\¸¥Q'["& /_‰´ %%: MÎ.O%¸. T‰ :¸&9A*G¤,N&Î J‘.  T°`„Á‡.–Äs¶¡Å°BNp‰! 'ˆ(Äà”{¸Xñq±P zDÈ1@Q6ä}è A…&-†ThÃ#=Bl¸A¤•!BíYÀ‰¦ Â@@ˆ-=!˜¥­É–2Œ@øTHU¶AµTh ¥A‹S¶<@qb`PéJ)¸Ë–Š]ƒXàÒmƒ…°Ÿ*phJ¨#F,ÔÄ;é@ :qMÚZK:@É’(¥ à%5¡Q4À¹Ðƒ5”P˜a)Pb+J¡F„"“‚lQâã݆pE ÑŠ8‰’y3 #¢$!@Ô'IJ(é;ÈÁ¸#9Þ«&T€A‰#8è·Ÿ \ {‚Av„%HZ !ù , ÿ€‚ƒ„……Š‹Œ‚SM‰•…[@K”–•D %b Š`‚-b¦%QEA–-AN"´¶* Q-)*=Ic0µ/XG¦%S¯‹E ´5 M <5´Iª‹/5˜˜Ú`EÐ (<Т‡>[UP`¢„‰(5YPÊ”‰þx¡ÂÈ’&¾ ÐÒ#†(8¤[tÐA…™Š8Èa„!ARà\µC.Wp,ÉpeÀ'0 µt‚‚.LAâoŽ-FZ   ú6\ ` ˆ— µ2%O ð,„èƒ,‡É8@Å„# ‚ …>I¥àÑ‘©ähi ƒ!ƒ¤0ð‰¨‡ zlˆre,YF»8™BÃkPJ+ªP ŒnFˆ@uX²@'KA$À<èÀ.§,¤l”` ×°}DÐäÊL)lFD€¥-H )àE !… ÂƒOT‰Ðå÷íAXPFÿUB/ˆW pþ-X` !ù , ÿ€‚ƒ„…†‡ˆ‰ADAŠ‘…¯@<EAš‰D/%% ] Q#‰X=2Q¥?@b C[:¡ V‡C5¯=çMÐ?2¯+3ІDÃNœt €Gî¹x2Q ("4èP° J0)BH.~ˆÐPÑP 6DØAQÑ’‡Txa…`n™`Ç z¹s‚(Lh¡€À©D,XÙ·³ƒb¯LdH`—1 . ”¡Ç½bF:t…°@‚ \Èwqu¯D*‚<$©Á`ŠÐ¢Œ{%I.|¼0P”V‚*\B€HR à:0B (?Z™Õ˜9¢ P`ÁAÎÒ…‚`ñ Æ!qèðÂKf60¹'ŠiC>,@Bµ-ƒï°r¼-¨=”bIôW5¦T'TáÂL„¼Îå¬#¤=ø–þPAAžô°QBCÜ¥=‘¥PQ¡À Íí$€}˜RPðQi¨Û†v!ù , ÿ€‚ƒ„…†‡ˆ‰‚_Ї/BS˜]F=@9˜P&%[Dƒª—ƒ2¦Fª/D­ˆ]WA°Ef J 3A¾… [ IƒS½)H´Bˆ  >Ô… %+Zˇ-= ·UhPŠe¯Xèò@R6ÔÀBÖ¡0XLô€À†ÊÄêÁƒ#!råÄ „`¨€ %¢pé‘ &³ Bf4jÀ“v¦P@„` @$€¢ž½'PоPA¢Xe‚·]¤ŽL!ˆ"É>t(ÏÕ¢DÙQ¤¦ÍAøÑ¢ P¦<$ n0|T·P ìI‘d®@üS`×€/¢ÀÐ,!ÂÊ7‡0ÈP†… ¸€P IUÖ šÀfŒd †0kž`äšaD! è@І8k–‘¡E•#:&D¸"”5XK€ÅÁ ’Lù I@“LXë˜A³B‡ 餄.h-õ4a„ %À‚ ž-Ë D¡Ä(VH !ù , ÿ€‚ƒ„…†‡ˆ‰Š‹Œ)Œ“‚ ”Œ(’›‰"% 4£Œa°ƒIFPš‚R N)ŠWZ<ƒ‚] 0.FD͈FG AŠ#!5<‰*HŠÏ .&QE«†AÖ‰N$OÆ‹ÌKô€ÂZ¤*ì6€‚ Nª $J”V=pAצN€À°ŠAvDi"1Q… .•ˆBePŠ;´Ddà8 .”xdU¡©/¢ô¢Å1CwzáGC  àcÄ' € E„ƒê©XÀeC”+aÑ¥(’ cË Hz”§Á|„*Œ0RƒAŽÐ@®qˆ€ñRF–Ø`!X!SjÄh,A4TÁ¤”Ÿt(éuÒV,™bÄD‰\T±Ð£ñ„´ yxb‚‰Ì2È ½ #.JÀ2".!C¢p™"”!$taaÀ)(ŒX Ãð9¦¸š±‚ô†`@@DƤPÎ<,°WnR0H@ ¥`ÁÔ Àh-$ˆ}WpÐQŠ!ù , ÿ€‚ƒ„…†‡ˆ‰Š‹Œ_’>‘“‹AO(Pš‹D(!„™‚O<ƒ- - Š>®)´‚C & Š[-ĈN\«‰M;D̉ $®ˆÚŠ–ì…è»öé>V]Bz0K (vxÀ·è  e… %DäÐÕÈ…>@¥°Ðã¢ÉD" ‚EÄ‘- øàÂäE.T:0dÁE1? Th`¦M"6$`DG B<¸¡„I ²J1" Š îu@Rc  ¦HkTH—ààxPE…*(etÏЃ N|ؽ…Tè 7‚QŽ”€ÁEK‡iDv(Èð Q Gl™ŽÐX$QP@e…':lаP!ETV ðáó⥠UÈPô"“ #‚\‚âC †TŸÀGðA80ÐÁDÂê&#À`"Ê‹Tl0±a‰ÖC¶¢D`@‚š&¹X0€ €…%piÀAئDlõ… P-"À…‘ÀÒCw%DÀC<EXÀ@pÁ€„h/W\’N„;nzbget-19.1/webui/img/download-anim-orange-2x.png0000644000175000017500000003537413130203062021501 0ustar andreasandreas‰PNG  IHDRŒŒ®ÀA>sRGB®ÎébKGDÿÿÿ ½§“ pHYs%%IR$ðtIMEÜ 9 !_ IDATxÚí½y]éuØwÎù–{ßÒºé™Á fÁlr8$gHQ„hI,Z±X)Ë‘,Ë*“±*²U)ˉ*v˜D²B¹J¶EŠ¢8aôG,Ê–"KNìÐIK ) \<$Ep’³ƒÁ2Øzy˽÷[NþèïÜùúõk ;(|U·ÞëFãõë÷~ïì À­sëÜ:·Î­sëÜ:·Î­sëÜ:·Î­sëÜ:·Î­s-~;ý1ý~¯óßËúÇÁ`À·€¹9`Á+øðåüÌÍ ~A±-0üqšüªª¦þÿ²,7}“:·ßìá·©A€`zCpMÓ\òßo­åÍ€ê÷û pðàAÞ¾Y¡Á›Üì{¹äÈ%†Àáœk¿B˜úø“ßWJM}3óïcøB e’ˆofɃ79,ë ™d!ƈóóó¸h.ËÙ³g™ˆX¾7 ‘$ðôû}N’‡oFhð&eÃ×@Q5UUᤙˆ|/ƸAÊ0ó–^ DäIˆˆ–Íš„'ç¢*ëF‚o"XZ»d0`n‡8çZ8ˆ¹¹9Ê¿! @cÄn·Kr?¿mß±ô³ˆ€1y4E¹ˆ¬”b`ii)æ@åðˆÔÙD]ÝÐàÍ"Q&ÕNJIˆ¼ñHŒcŒ(ßËÁ(Š‚6“2MUU,÷å–ˆ™ˆ˜ˆXš„'—>òõ$8[UW×¼Y`Ù ”;w’ÜwÎÑf€„ˆ™QÀÈ¥€s!µ4 ùZ®º®£R*N¤”Š“ðœ?>N‚#êjÂUç ¼YÔÏf 8çHB$ Xk•ÀB  ŽV¢ÈׯÜäuádE#‡D¢Ä"u'á1ÆÄÍÀ™â]Ý0ÐàËÅ@išFÀÀ•e©”’Æ)ÆHÆÔZ 0˜¾9@›IçQû&zï#"Fçœs€ˆ(6M²¯¹ªª@D¬µŽDÄÖÚx!pn$hðFƒEl•••§iš&QʲT"M¬µ*ÆH9$!¥µV€J)ÅÌ”n1Ýoa’ûévƒÑBˆÀ!FĈˆì½o‰1ú$]âh4 Dˆ("b+iž$eZx.Îììlœb_7hðFƒešT™U“ ˆD1Æè ¥µV1Fbf*ŠÂ$ÕCD¤•RcTJ)jš†¬µšˆ0ûe2mÀÌ,`D¥0s!D" 1Æö~!QH0…‚÷Þ¥T@ĘKGkG£QœÇZsi3Å(æk ^'XpZLE`©ªŠ&Õ÷žÊ²TÞ{òÞ« ‰¢”RZ ‰1ªd“"Ò1F…ˆ53"ê$Mäk•#’EÜnFDŽ1ÆôýcŒˆ™90³g怈™½sÎçàÄ}ŒÑQÇ^)åEêh­CNUU!·qVVV‚RŠË²Œ€¦çZ@ƒ×–iRÅ9Gsss$R¥Óé(ï=9电V…TJŒQ (ÆKD:„ µÖ 3+¥”ADþPˆH 0ˆ¨2u$Ï“cŒ,R%‡cÁÇ3{¥”!8föˆèÀ;ç3»£ü»Ï¤Ž×Z‘8Zë µŽUUDdcL´Ö¶àlEÚ\mhð:ÁÒ†ôsXÄV™”*Î9%êÇ{¯r‰b­µ ” X"²ˆhQ‘F)ebŒU‚£½Ÿ€!À Ît’€’ßfö1FÏÌ\úºI÷]Œ±afsÎÅRÊÇ]X;N)åEU9ç¼R*ZkÃx<Í4u= b¼°L󂚦¡ÙÙY•¶-,"U¼÷*½é*Å"¢%¢‚ˆJ0é~‘î·À‘afeˆí=ýfþîY?¿» »æ¬ß1×ý>ÅÞL'tz†K¥”æÂ`LÕjÄñj£F+cZ=_«•×Gzéµ±:ÿʲ>;ª€€ñà˜cc¬ ½ !ÔÌìäŠ1º'¢ µ1&ˆQ¼ uU¡Ák K±&Yš¦Q“¶Š÷^…´RJ‹ôPÀc f.´Öf.”R%XD,“±»‹0ûþ»V{dg}ßÝ tGG³¹Ü¿yyÅ ^Yѧž[²¯|öxÿ¯i9I—œ÷¾BÄ:ÆXÇkf'pfn¼÷µ@ã½÷ZkODÁãýÚ'$6Mã/Í…$ÍMÌv%‹s޼÷d­Õ!äœÓZkí½7EQØ‚.˲‚ˆJ"*•RH D,öï¨nû¾½Ãw<º§¾¡¯ç®…ú=3ðKÏœ*^üÄÑ™/]Ñg˜¹a抙«cÅÌuº'ÉS9稓Ü(¥BŒ±É¥MÓ4"uâêêj0ÆÄ­@sS3á:ã…l–i°h­uRAšˆLQ6Ù'E²QJ"*µÖè(¥º ”’ˆ:?°oé‰ïÞ7zÇí³jþzF²_:Oüëçû¿pª÷B¡bæš™ÇÌ<!Œ±òÞ±öÞ×P@S×u“윭uÈíf“xÍUƒ¯,S½¡Í`QJ±UBÊSÆuQ° Ž‚ˆzJ©.u4¾oéÝï¿wø3¥êÞHøç¹ÿ÷•þÁßµ÷ã(I›Q‚gÄÌ•snB¨4537IU­ƒÆêºö“T¯×‹×Â{R× (wAXš¦QEQhçœÒZï½""BÐÖÚ"ZcL;Zë¾1¦¯”šQJíPJÍ"âÌ÷ß=xâï?yæG¿=>Th2pƒ™už¸Ý?ôÞ;V9Ø!i"jcCD¤”R¤”‚}FD”l;+¥ÐÞ{°ÖbÓ4 Ðív1eÑ™!Æ'OžäIaþÏÌfÜÐÊÊ m&Yq™‰ÈzïM·Ûí0³5Æô¬µ=­uO)5CD3ˆ8CD3ož¯îûÈ“§ô½÷„·†,Üà§_Pç]wúGžÜ5|àÈÀœ=;VM (Š›ˆHDDÌ D„/Œ123Ã4hÆã1034{÷îÅÍ:-r% ¹bÀXk§ª¢]»vÑh4¢ÍÔÀÒ4IêÈt»]1d»J©õ3Xf s?õÖ3é/¿©úósêÃMvvö°ÿÝw7o½¯W-|õlï5$€(ÐÄA)ˆ(Ñf!l€¦Óéà&ÐL}Ÿ.š«Ì»¥ª*º,Zkˆ¶,Ë’ˆ:DÔM׌1f†ˆf‰hæ;o¯ýé'Îþ§÷.À^Â+kƒÕCåbS;ï#k…xuIBÀ½s°çýw¯¾íÜX-YÕ«)€ˆˆ)Ç%µ7"Í|!hêºæL•ÁÃ? G½¢êI_Íx‹Ø-’ªªj›»Í)¦R$X:’¥3ÿÅ£g¾ÿ=÷„'èßÅ&@¬WÆ4:3¦á¹Z/ŸÒ¹£C{öKÅëËV’ffÔêÑ]õžûgêÛîš »;an¾ 3ý¾îv \[i¦TŸ|çøŸx¹zæ—¾¶ó÷’:R’U;0„€R¯ã½Ï¡áN§£Æã1ÌÎΪ••• YþTó,k„­5á]]/éBñ–••ªªŠœs433£œs”¸Ú9§SŽÇj­K(’­Ò¢”š™Õ~×÷®ÓÚ7‡wl÷ù ¨¿u’ýÇû‡Ÿz½ûjŠÊðYx?æiù;ÒFIµÉËÝeè¸}ôÀ[æë»ïÙåwïèšÞå¾–¯.…Sÿà »k©‚ó°â½_ ! bŒCfŠ+Þ4MÍÌõZxÊ»”ÄôÖÚ Á=kmÏéJºÚx…`™o‡4éYkµ÷^i­ "Š[2s©µîj­{ˆ8£”šUJÍ,vüâGŸ<ùŸÍÎèÙí<·±Çpø8¼ðO-|Jr< ’jWBÊ6‡”Hl3ÒYþˆ’M¡&òO:»5 ßº»¾óï¼méûvö.œ¥‘_ýÈŸìþø©¾ÎÌ«!„•Í€™‡1ƱsNb:@c­õ.Ë2¬¬¬„<°—AsYQ`ua™o¡¹¹9Õ4Ê<"<"ÃÌÆZ[¤ÜO‡ˆºZkev±ãî=§|¶«úÛÅñˆ:üóOÍý«?|­÷LúdS¼c˜â£ã8Æ8Š1Ž`B§ïUr1sM ó×r?…ø›`áäP-âå™§•w¼o‡Ûm5]’º/ î¼éuŸ«FDÄdʬ½Àkõ lÇ£¨¦²,Á9·Ás*Š‚÷ïß¿Áž¹[æ’™âµy"ñм÷¤”R)묛¦ÑÉØ5J)c­-±PJu“*êk­w âÌŽ‚çþ='~|¶Ü,uÀð¥£øŸýâ®ùÇÇËÃc+éS9Š1CrbŒcfÇÇ!¥€Ú{_IÞGr>KÊÿ´à¤0¿K‰F‡ˆáé3öØ'^ê=½ ›Î;`AÐ%@cܾúðg_í}«‰R]W[À•¤DÛÞ2 ÷ž{½žt9€6ÍkºÀ´·‹‹‹(^QgfftÊ:ë¦iD²h¥”!"›â,]­u7äf‰h¶44÷óï<þcó3zçVžËs'ã±øÔ®ùûÇú‡jÏ+Ì<ˆ1cŒ#±’4€HQaì½—üN-0È•J(šB#0³KªÍee!D_:Ý9ò¹WÍó÷vÇ·ížÁ™í¾¾]«Êw/üý£½oF^+4絟´¥¢3ƒ÷žfff êMóš® 0Ól—IUäœ#c 9ç"*ï½BDí½7`ʲ,´Ö¢†úJ©Dœ%¢Ùÿþí§þÚ=»qïŞǰáúã_ë}úcÏÎýÁÀÑyÃ{ßJ%ÆX…*›¦iZiáœkBMÁÉå½wˆèч":fv©HÊ…$±‰<" ÌWÖŸy­÷ü`/¸»ŒÂm½Þý‚:oÝ1Ø÷ÇzßLˆ%6WXsª˜™9j­Ñ{ÏÖÚV5Õu-p"n2ÛUKê2aiY\\Dï=Šë¼cÇ•J´÷^å.tÊYcL¯(оDnqæo¿åìžÜß²…ÄÞÉŸþÜžñÌ9ó2 cŒƒt cŒ#ç\«~bŒ"ÖιJ¤3;¥”‹1zgˆÖjZÚ+„Œ1E‚¸£€ …SIº¸¤6bú¿!½Áñ¹%söŽvž{lat÷Îm+Ï5ß§™{»ã]Ÿ?Õ{!oI!zL‰ l­åE!„Øív±®kN‘ã< |I¶Œº¶Ëdø?7tEºh"RDdÓUZk»)yØ'¢Y¥ÔŽ÷ß5|Û_z¨~?^Ä{ûÂ+øõŸûÒžß{XNvÊÀ{?dæ‘÷~è½§’‚ ¢óÞ‹ ⚦i!ɽ¦äz‹%¥•!Æè­µÌÌѯD"xD ɾÌìÓ'_J9yäÀýû#½oÝÝ©æîÚ ÛyÍÝä}8|®8™T“gæ˜JC‚ÖºUM©³½÷LD<!ÁEQðåØ2êJØ.yø?7t;ŽÊòD†™MQ¥TG)Õ5Æô‰h†ˆfnï†Å¿ûäÊ-ñ½‹Ï¼¢ýÊÓóŸ€3D …!„ç\彯­µ®iš†™DtÎ9$@PJµ-!S® ”Š)>2BÁ3sLÿŸSŠ@E]€”s2"ÂNuÎbcXàÛ·óº?0ï~î,;5Ò« šZ\¤(›¦ ©ë€C\–%4MÃý~|9¶Ìå³.üBÀ¹¹9 ÿ+Dl“ŠÉÐ- °Öv2XfqægÞuò¯/tá‚ENŸ{E=ý«ÏÌ’™%¨µšÜæA2`ÇbÀj­[i’J&=øT?”RAk߯T„Ý^Rg› ˆ©Ï¨•̉(†‚µ¶5BÅ‹á5r‚ôZ§Øõlç伪»÷ÏÃm[¶ð{F÷úèÌa×Ô]’‚‘ˆ¼÷^¤LÈUSŒ1;ç¢ ¸[æ²ÛeRºE¡ÅЕh®1¦ÐZ—ÍM¶Ëì8ÿ¾'ï¼°Ýò•ãø­_<´ð‰ã*3˜¹5ns#f®¼÷•Rªnš¦N% x­µŸÃ{ï3µ“Kü ÌCí}kmÞcŒâêÆB`fvÎ+Ƙ÷d·Ÿê/½^°_í¹cîÜêk_eï팞ì½@ ˆi£µæÔâÒJ™+i˨K4x×¹ÒIQ.]ˆ¨•.Ìl´ÖËséJ4w±ìñÕ6ˆW¼p6¾öѧnûm%Æ8ðÞ`ì½¥8J-ÞN2@×bŒñéôÌÜV×u’;â”#°t:&"Nßc'y/œÀáÔ¬Æ!Nµ-,*ƒ%³˜^»?9Ñ}å;wîßÑÛº!|û˜?¾ §̹º/öSŒ1H¸H™+i˨Ë%WG)©HιÖvADBÐÆ«”jÝhcŒd g~ê-¯ÿÕÛgyÓRÊsC¿ü‘Ïßþë.ð*3¯ÆιáRé£KFlBp oŒñ"M’a›¦‘ô€Ø0mc|jãh¯¼ÿ9®¹S1Æ»Ý.#b+ò'ÁI% Q)…!„¨”‘4IÚP‡ž:Õ9ú½wÞTl#2üÈ®ê®O¼4ótrùCòÈœHLc §žo!ÄI[&ípæÌ™ 53W ˜Íu"]Rö™Ò„ú›µRÊ(¥ ©·UJµ…PÏü‹×ß³™W4öè~ö ?WÑÙ”Wxï!„aŠÒV)°Ö¤¸ˆ×Z{cŒOÅ@°Ö†¦i|Œ1c¢Ö:Zk£µ6.//Gï}tε·rÕuÍιØ4 ÏÎζó^¼÷ì½Þ{q_Y¢­I¢°ÖœslŒÁBΗ¢¨T(¥Æùèž{Ï]á¡­–k”†lkúê™òU$á~BÊÄi¶ "‚µ–' ­®.0âJ‡Ð{©§HÚYEé<_$]­õ Ìü—o{ý¯Î•¸iRñ7Ÿé|úOÏtŸOÆ­xD£L²4ÌÜÔu]‹ 2Æ´E@ !´€Hlzã֚¥mCkÍùe­eD­5 @u]óÌÌ (¥!Ç{ÏÆ)•ä03k­LÉLÑLtrlÇóºîí_غ|×\Üó©—ºO»Mrë}’2¢1fª-“G§©¥‹s©õ0í|¹¼iÞ{ODD©ˆ‡Œ1 TÖÂZÀZ{HñخѾ»çhÓr…ÃÇá¥ß{uöPJ½÷ƒãÐ97N‰Á¥Ôº&0I÷'u•Rœ³Ö®âÓ~r'ƧVU…ùÏH™éÊÊ §NwÜ4 —eÉIª´áyï=¤$cD êy 3›ÿíðŽ?yû§ïÙ=c¶”•ï´zhé»þ÷¯Ï}+f.˜¹€%¡’aŒÙxÜläu«s 3¨KêH%`”1F‡TJHïPGRÐÿÛo:ûƒ }˜ê!Œ<5ÿÃS»cìx%‹ÞcŒ’ûi ©ªª™lü²Öçœ7ÆÄ¢(¢1†———£Öš1\w:îõz¬µ¹>Ož<ÉùuæÌÞ»w/ÊÏEÁDÔŽUUqUU<33#@±µsIƒˆ ”BçÇQ¤Œ”b¦’ bfõêªYúsûüøEÕ´Ø÷»þõ ý¯$÷Ío…<"Æ—‰ÖZ±ÁX)Åu]Ë„¬ ÞÒÕ’0 Ò`nnŽªªÂn·Ku]SªIUÖZc$­µ†ÔóLDv‡ 3÷.ð¾Í÷ß¿`?¿TÁ²”#„FÎ¹ŠˆšL²4Zk'°$H´.Á\¢LŒ»h=È¡C‡Âdd{² "Iœ8;;«ªª‚\=%)ƒJ)ôÞ7ιv6Mš,a@‘=tÚ¾zøÔðØc‹t×V^ÿ^¡Š¸gåíÿ÷KýÏ!b‘ê +¨sM§ÓÑ©Ÿ›dB—÷çççqee›¦Á$U·\·ÝÔû†ÉÚ“Ãe<˜1†ÒÔ'¥”²vÍ4·Ìlÿ“{–ÞmÕté¶2ö«¿õÂŽÏK· ÷>Ï5IÔzQCÓ`‘"km,Ë2öz½8;;â´B¢m>tèP\XXˆ³³³RÕÖÚFÉU÷Zëè½÷ƘvªƒRÊ)¥r®­½µ^¤êc_ÛñG>ný¹½ïÞñc©Ó³›>˜šˆTŒqÝ¥iji³qùW ÈD׺Ys©.£Pc¤²,µÌg£µ.±xÛâøÍ›=ø§^êªTÔ4"¢*ÆX§vÒ¶4·Y&%ˆñ\öuðàAî÷û\–e.Áâp8ôMz^0ô!„vzƒ@“l™¹>¶Jçž=mõ͸c‡^Ø7ãvQ‘¤Œµ~s›€Q2 RFºÉû%bÚÞ…Ëf2C=Ie>­²( ¡™’:jÇn0³ÙSú¹…¾žß¤þ¶úW/ïø3×!„ ç\-bŒ®ªªF‚qÒw<9á@`É¥ÊÁƒ/G²L•4Œ¹´1ưRŠ¥µªª`Œi ñ”’ðù©Á‘‚-h~÷Åî—¶ód~àÞÁÌlEÂdspT§ÓÑÌÜJ™Þ59—8/±½¢6LÒßë¼£‚Ø/HDh­¥dÈIGŸ•±¸{ùÓÇ›~í¸z6M:¨RýŠ4¬{"j§;¥hjH5 1„0–«1â+ûð´ºÿàÁƒpàÀ¨ªŠSö¬µcÄ$ùP)E!rÎ"òˆèÑ‘‹1VDT¦Iùô™òø¹ÁÊ`Wßn©Úð­»Ý½F› ¢1ŠZÒÙ‡soinnކÃaÌì˜+#a6™µáäö‹Åƒ…á À˜ø¿ëT”¨'§*AÂa–qd’!÷Î9)¹ð!„:¯òûòñò¥­>§¹YÝ¿gÖ/ȇ’ˆL¡5ä=)Š‚.W´^Öä#Ù''l—e©ÒH0J’L3³ÙÝÃ=Óëõ¥xæÄP!ÔˆØÄk"j –d° ”"äªHb-b³L®,xŒÖ.Ë’E5Iì'›]ˆ(8çBÁ[kcrƒ¥ ´\õotŸÞÎsúówÞ ip"­µafÒZ‹—й¤¹ÔxÌ%yIbðÎÏÏc®˲l%LjÑ FHDæÑÃMù|õL盵´®®ëFfÃyï½L¡”º™—"£JÅÀ½š°\ š~¿/Ï#Ê„…iR&•EøTÞ)ÝNÚGŽ®è¥óC7Üêóyh¡¾²l2¿/Mm§Ÿçj1|«ªBÙ-uU%ŒÚï÷1{BdŒ¡D÷ºyrO.ŒÜì±~ïèìŸf#¼ácôùÐä\ºÈ<[‘.â Á5Z`5í1§©¦\ÊÈì^ï}HÀ´ãËD¤Šî¥³úÔ–³Ø³jW‚Dœ %!à¦<˜«ÕUÒ¤‡$Ë ²Qí"aP)EùXÓ;{îÎÍb/gj½Œˆ>Å)ù£Oƒ½ G¾t‘7ìJ¸—jÓLª&¥çRFªù1cXjƒ½÷í EðÏž³[v¯;Ísõ3+Dlgˈü\µõÂi%ÐdlíªI˜ ¾f&ö’8ý©ˆˆvwÂÔTÀ‘Us2UÝ7RØ”*öÛOd>Q[kÝV‰tIQÙ×aJöf¿C¤ŒØ2Ù®¶’/Æèý|ç¤Hi’ObºõvDÊäÕSÔÑ–Š×à¬ó˜d¹ÀÜÒßÒ‘‘XÒýÀÌa¹ÆñVéNÍ=xcŠÕ–ÕÌuf]ÝD²Î‰ˆ‘n+ê©í¢uÀà*xÉž²¼Ñ9'3þó…U¼ÙòÎõÈ­lR«–ÒzÙ[ÐŽžWJ…á·,alGYÍ0$éÂÓÖÞpÀL~Úwt¸3í¼÷µØ)RkcŒu]‡|ÛÙä‹/ŸØ)öË s¦©%_¶·å=PI-Ip20sðLõVŸQhdºC¾økR:ç»+o4`Ú­e Ym"aØ8Ì'oËX÷8òÇN±_nØ“{K™í%«þb6©¨Û‰j ŒaËÀ“À’~OÌUú„¸$)M×L$ãtÛ¢ à§|¢l?“ï¥ÄÞ–¤Ù txšZF-,YÈ@`‘NNÞÎkŒˆ(.ºHëBùn4Åœí|èèJK“Éû!„5cÏÁÔÚ¿o¬¼›|±Ó'rn²#Ƹ碖òTD}³…RÊ!bÃã6¥KÐż[Sšëò¦çÏŸ7„„IMá"y©ÖÃéÉ)–}Óí'ršÍr³2YgcåîµxK¹§”¦B8¥”¤ šsµ~}«¿{ä©–Q&Rd&¥ yïõ´ÿ»YJåŠ3ùæÊ×b¼Ê~¡“U¹:Õ›Z³mP&`g&¼¦ë M ÎdnI옴@4Q(Š"J©ƒ÷¾"¢ê“¯Î|Î3mI9Ã/QÅÌ"6iJ…—ü[nø^ª—¹-`$O“‹W‘“kxiAeæ8-jÙÑló(ä4H'ñçpIDAT¶˜$»ájcD›yKI=…‚oš¦AÄ›Tš:zmTœ:ô}åb¿hì±ù?ž]øílf_Œg7ü´°Dž´Írp×V%IfY ®Ì >ÄjŠ+X¤v Ùˆ«ôÚ°vŽD+³<ÈMs¤=W2ØIU­µO…îcŇ0ø¥§wÿæ«ËüÊ…ó“/t~÷d¥O¦†¿QÚ˜R˾I)gÍí—Kñ2·?°/ÓÇyˆ>‹À¶}½ /}2+5¬lp95¢‚R²ŒRze—43cY–mz>ƈóóó-(Û©ç¸Üky³Äø%¢Øívy4yI@:çÆˆ8 !¬F€å¿ÿù;ÿ§þtÿÿ|ñ4>_t>B8½Ì§ž:fÿøüÒÜ?þÝ—ç>+i²… ylbŒ®Ûí¶;³%q{©*I_î'Å9ƒÁ@ÚJÛ·äFR×3s8=0§ïœõ뺭u{ÏÍè•TÖIJ)rΑä¤bŒm&¼ßïcÓ4W¾ß ±˜É×MnG£Q´ÖÆÁ`à´Öc$ï}ú˜H)…’cúÔ‰¹/~òøŽ¯üøƒg¿ïcÏÍÿaò†ÜZí3sCf{ïGö.1³¯ëÚYkcUUÁZËKKKÑ¢Ž¦eù¯¨JÊó#"]$›œÔŠ É´'ÿʪ=2íñÞ¹0ºOT’@£µnmš¼RLTQò¬034¯JîäJ±ÿ¤ä!ûÔDz,Y.ÂZGÁ8MÕ’Ïç™yy¶··ÎÅ—sK1ÆA]׃ãx4U©*h­ÛÝ‘›$m¯[»hyÖ9ÓâïHDQ¦p|}öð´Ç¹o¦Ú'YmcŒÊ (÷˜òÚá¼'jZ=Ç”á×ÕøÃ7WãRX%u2ÃáÐu»]Ç÷¾!TιŒ’ªYeæ¥T‚gÉ9·BXFÄ¡snHDÕx<Ëb®´GRÒm´y¢BñªÃÓÄk„ j)M¨ô1Fwr¤—Wܹ{®¹35»éTb¸¡¼0Æ(=O$Ø1“Ó M+åuËDySƒë÷û!Æèœs1¦5ãœüÍGVõJ‚g5I•UçÜ(Æ8Çã‚3Æøt¤œUæßL©Pä­FË·-aòÈåÅ _)’Îà^;Êç:´Ëªµ™ýføKY§lõȯn·K¹ZšèàÉ€Þˆj)—2bˤ2Îàœkçï9çÄÍ®±~|wó½ðjùï’ª’±²ã¦iFÞûÊZ늢p9,yGh.].´óú’¹Pä27à$?"vŒ÷>ÈÆø$aDt_=ÛûÚ†Oõ¾Û—•fDÔZk“¤ŒJEåëìQK“îuæ-á Mù•šD1!ë_ŠJ©iް)ÕîÏ¡×ÒÞÈq¡"¢º^`× ‡Ã&„ଵa³ŽÐËqèrÿàÜð•P·Ö:dáîBhÒ°›æSÇæM à½sÏèqÙ’&nÊ@ż ¹-f–knnŽDÊTU…ÙŽ ¸^’f³u†ÓÎÙ³góмu•…Þû˜à‰ÖZþ;OVoŽÎw: µö ¢&Æèd܉H•ñx¼aAúd¯ùUfZ¹£P*b5OjI²Kr#àÒŒþ†›ãçaCêîyØ—6Ü[­ukÍûÒŒ¥dš4•‹Zš4~'b2×\=mö;6‹M–kÈkØét ( 0Æ DÑß¼ÛøÅü¿œs2œZ’•^kŠ¢ðù~ë -Fß®ír9¦¥s²^uR-%Ã×{ï]ŒQFuTŸ99spZŠà/Þ}þíi3›ô kDÔEQ˜‚’¦rfFï½ô*·à¤½’“RæšAs¡Ç¾P‡¨õn·KAÒšûç±è*ÿÀÿzø¶ƒÝn7&(BZª–——].UòÅ9,ý~ÿ’l—Ëò’¤’,«ÓÔÒh4òbÇ$µÔ bóG'v<»Úà`çðΕw!¢UJ•J)«”²ˆhBZk­´Ö*ƈÖÚvž3£sŽDʈjš0€¯:4›<&BÚP7Å“›*mºÝ.v:Våj­ÑZ ?ñ¦sÿù°)¾œ&z¥T !äm·A¤Šµ¶y²ɲ•Âù˲a$€·™Z’V‘4`Ùyï+¨˜¹:tÂ~yòñghñ‘Ã; PJ•DTEaµÖF¤ŒÖZ‹”h’:j¥¨¦ œ\—Çä5 ÈÖ ,›å¾fgg±Óéµ–¬µdŒ¡N§#{aW·ùËOè_ !ÄT嬵¡(Š ReÒÀÝŠdÙj—Å¥Û…º«ªÊk/¤ü0ÀÚš˜:m¯þù‹ ¿?öè&_è¹ïÜ`­©¼ÔZ—DTfóNt +k­Š1¶ªI `ç¥-¶-4é>œíJœ‹üìU8 –ɾ®^¯GÞ{ ! µ–ˆHu:Y|N}âäßlXŸú7¯Ì¿’Ål$µÖ, äYè+)YÚ÷zÒdÝ qòäIØ»w/zïÑ{£ÑʲĤ20 JÄ”ë!çš5 N‘qéþîh÷³q]ûì\ç^^Ò/¼^ËY'ÀÚXô(K¦˜Y¶AQí~ ^¯£ÑhÝç#¤v$€ü=ÖZÜʵ™$™eÝ,<Å9·!µB ™™*ŠB>*­Rˆ¨öÌèâ?ÞwêŸ>w¶ó3Oé‘ʼ¢(ÄmÇŽ 2>¶( VJA.Yžzê©+Ë¥Ó¾(‹‹‹˜6€aŒ;23VU…ÆlšåS’†Ê€!ˆê™¥Î«ï»kô]†Þ(G¼vt÷§Î~)¥ÖUÖËDç‡ -ÿ–™³íȺ®kÜ»w/îß¿3p.êö^Dšl€çñǧÅÅE\\\”•ÁíðDiüËniß¾}¤×Žê÷û:­64̬•Rôß>öê?$å?:|ï/Èë`Œ‰!?Âx<ŽÞûuãN”Rm¬ìJÂr¹ÀÀþýûq4µe"e˜¹í~ÔZS]×h­•é‘2ŠB{V¸Ç4Žsþþubßbß²¯Ÿ]î“нdÅTŒ%q °Ö¶3qóå H™”È„#,..ÊÖøi’a«×ºÿ# TUÕJ]ï=‡Cš–0U ª,KEDZ)¥B¤GþÚƒg{hnõ¿ùâé?ñôÙÎÉ õΜ9ãÇãq\^^ŽÆ˜us‡¯†dÙ60MÓl€æèÑ£°wïÞVʤO:13Öu ¢’RCx;ìY2ÓD¤ëûž;Î?Q\· á®þÞ¯œî8UÉ^€cÔZ¯Û˜T]»ü;‡¦,Kìv»8!Ù;è½Gf†……Ú»w/NgË×p×®]ë@õ“¹úä½o#Õ;wî$k-õz=2Æ("Ò2àÙZk’g¨f4Û=tê·Vëâ©_>|ûÇ•RÁ{ïŒ1aeeÅÉȶÜ[ËÕ,—,Š'G˜8p€Îœ9#Ff»Ô\vT§¥æÆ{¯z½^‡ˆ:eYö‰¨oŒÙ¡”š{tçpÿõ¶3?©i=À'Wãñ¿÷Å{þçãª÷~Vsçܨ®ë1¬mz•§Ïö´Õl’lË Â¼Ì 7 ·{&ÝäܨÍl·?„€³³³˜>8Zk­@cŒÖÚ€¶Öª>þÊ/ÍÕ;ékw}Ï+ãîëñx\#¢?þ|]UUØÂ´Ð+ ˶$ÌfªiRʈ-“êcÀƒÞ{ÐZSŒ$–’Ƨ#¨Ó•ï1M±oÎß·Îgî,ƽÿpº÷œ¬âMÒ&*¥À{/[Æ8×ë$s˜RÂ;‡Ãv¦ 3CÚó„¹½³•k4Q>²ª*Š1¢s®•&…¶Ö’Ä‹z½E¡²É\ÚcŠ¢(’:Òyó‘.v‡?úÍsý|úÄ®o$éê ¬®®:QÏ“ÒEk=)YàJÂrE€¹˜-“lLO!õþbêWk­×Hz¶wô»Wž-Öoe»c6îuο°Úy=©!ÙÃ,-,«§IhŒ1”Wèu:,Š‚1LeIX.¦]I浤rP¢ÏbÌæ ¤Øê÷û´&H´Ê’¬6ÍØ5ÖZý¡ûN~Ç›æW?z¦ê}úžÝ÷kÒÜVU•CD?Ýx<ŽçΓ;ˆ‘Ûï÷yŠAÅ`¹ÀljËt»];*¶ ôz=B€4™ª­ÙEDRJ©ÿpº÷ì{o_yW¡±È½¦v5[Õ/œ™ÁZµ·ÐHïyZ)™!Üvh­É9·aˆc§ÓÁN§ƒƒÁ uws[gÚ•¥%Öy;HQT–å:xRLE)¥Ä]Ö`:NV)eµÖæ}wœ½ï{ï8÷±šÍ©Ÿ}æž«ëàÑ×u튢ð+++[‘.SãeÛÝO}U™”2Ì ÃáºÝ®~`ŒÁ´vCíÛd #"Bˆ_^-^|ÇžêÉÜÕÖôØîæ±gΔ‡—Õ4i=M»fÆu]ƒÖšŒ1­=!S˜š¦É©Ÿ"qäVšvíܹ“´Í‘ÇvΩn·KÉŽÓ½^Ïĵ];EZšZ‘þŽ]ƒ;~èþÓøÏïþÑ#ËÅJ/ëdú…¤Ë”q'ÛÚƒtÕ€™æ-äRFT“Ø2ÌK/¢t€Öº4§Mt®)Æ+c8ñØîêm„oD¢-±yrÏð­_85{¨òè‰%8‡ˆ÷PÀHC•1_C#?;Y_#÷»Ý.–e¹áJsœ”.q–‘ù²Q7ÅŸôÎ; "š¢(L·Û-ˆÈcв,-"š·Î÷|è¡ã¿aˆ{Ÿ}m×OüÁ‰]/¦ ¸RꢶKÚHÂWS]0S io÷ïß©ÂR QMÌ ½^G£§7˜Òº|uÔ9Ï ¯<´Ð¼9ßRVh(Þ³¸òö§O÷žíÒüĵWÓ"+PJA*ºâ¤šHŒï|x£1µÖbãH½ zïiÚµ‰Ôã½÷ªÛíR¿ßWÝnWw»]£µVÌlºÝ®5ƈ(喈죳ÃÝ~äįèçž>3ûÓ¿ñòâçS·bã½oˆÈ×uíªªòMÓ„3gÎ\Ûå²€Ù š£G‚l™4€“EQ@UUÜëõ(Mg€dhB2b‘™áùA÷´e?~`—{$_ñWh(ß}ûðÉSCuäøÈÒ´qLã¹ÈCé1Ð##`Á9‡Zkêv»ª®kžØ¼‚I…¡µëºÆÉ²ÐÍ®N§£z½ž.ËRE¡Ë²”!âÙ²,e@aŒ±ˆhÑü…»Î>ô#ûOÿZ~ÇË˽_ýåoÜùÿ¤!‰Þ9×4MÓ(¥Â¹sçÜx<ŽJ)¾^¶Ëe³4’cš4€Å¶)Š™Çã1t:pÎIvÒ®D™–Ï.w_ëF×Ü¿Ë?œCc˜wì?±S9øÚ¹Þ«©c’ˆHê€I$O‹¢ ™.Y×5 8ãñxÝö¹/© iò""Õï÷UY–Jâ&éµT©ØËt:«µ.´ÖV)%+ m2r‹Ÿ|äµ~÷âÊÏ å+ËÝý£gîúõ´ƒ ÇM¡QJ…¦iÜêêj°ÖÆiÒåZÙ.W˜ å˜´Ö ñ QM¶Ö&!ÀhŒiÁu¦”Z å2ó×WúÇ(„•ý;›7å6 !нsîÁwïY~ä›ç»Ï‚ñ©¬ed½Ôk­ U·Ûm»¼÷h­U©Au»]¨µÎs_Jr`ÒÉ@Dò³Š™epµÖZÛ¢(¬1Æj­-3²îGk]H}ÏþÙjþ¿~üµ_¸«7üÅÃgfþɯ<·ïwbŒM]×ÍZÜÏ5ˆè«ªru]Çc%ñBJ„K^œ2ŽhMäaúZz§ÚYÄÉkS29ôƒûN¼÷±Åчf´»  Šzõ«§z¿øÏ^\ülÚ'P§¥§ë`‘¬ôËf3hd=ÜÊÊ IMÈV QJé²,Ë$â;Ð1Æt± e¡±÷·<þƒo^ôßi)N*ðò²zöç¾|×ÇQv;»‚`²+ûÚ“C“Æá£€!ª+R—æßÚòî©~`F»€ÀN®ÚOÿÊ7ïþ_V¼…œÖºiš¦vÎUÉ}^'YVVVxZ/ÑV Ý›˜Ë…Æ{OÆ­ÖöÝèJ·Éx,1M ”wt㮿ñÀ‰¿rßB|\cÜPrÚDrçFñØóç{‡ÿÝkóOÔ2¤á°6y<0éûR´•EÂõÂf ˜”Òh‡|ð¾Sï}ëmãœÕÍ¢¼‘g†ök¿ùÊž_üúùÞ‰´ç±N B›£ÄZ\UUMÓ4¡(Š(°L+·Ü ,7+0—M”R:\­uQE™ÜÓNŒÑj­»ˆX@qÏl³ð£÷¼þC÷Íû·N–IägèÕ¸ñqieD¯¿^Ù£/ ºÏ<9û­aÀ:I™(ƒ¥@ëmÅú»W÷?Ø=°»tûfº|{¡x¡TqFclï¹Æ~ëÓGwþÓÏœØñ-fn”Rs®€šˆœsÎF£º(Š0œ÷ÞE9" ,øªˆ7«¯¾š°\ `¶Ô¸–e©ôÚ/Z³G•´™ÖZ“ìš"Û¤Z2³%¢bOÇïøë÷Ÿü¡Gö¸wo+¦©©5l ® Çx®‰´ª1Ú^æ ¨m¡o)v,E{¡Çµzèõþ¯ý³ÿPŒm‘,iP¡ !4u]·öJÓ4ÁZ_{í5Ÿ÷[OÂ2¥ÖåšÙ.W ˜K&)(MœR)$¯¬µJ²¼k£c´ÁLNj‚+ˆÈ¼kÏêýî¶Õï¾s‡{Ó¬»®Ö è™üÀë“Køæ7Wg?÷oíúrí×à@D/ |J©&„Ð ƒÆZF£‘3ÆÄ¦iÂh4Š«««a,yÉå”]P×|ÕϵìÜMîrK±QÓ4m2OVæ†ÈZ«”RÚ{¯R•š±ÖJÁ´€cf–óf‡wÞ6¸ïž^µoWáïÜiÝBiõ.kãLG…b+OU]7j¹azi¤OœªŠ—¾9ì~ë 'f^NãM}¶MÎ%I∨Yë7sMjè Î9™Ð7ƒ%¯¼Q`¹ªÀlšÔæšw0¶*ªÛí’ô‰´p¬µFªÖ˜ÙÈÊãT d•RÖÆˆ(q‹eŸ`IQ½i×è¶{:ÍâB§Ù³Ã†…BqQ©íñ#ÃâÈ¡³ýWÎ7j,6N’Ä`^«ßŽ™›ä‰9™‹#ÅOÌì'A1ÆÄlFÌTÉr=]èkÌÅ Éã4y+F®¢òÒ‘8Ò*kŒQb#¢$þLšdµnýqZ6¥RŽIÖ ¢¬‰I·cääùˆÜzQqÍŸnI1'ÐdøB0Æ„Á`àC>à±e7 ,טK‘4“SDÚÈ0!(È$ÒFN²sH–ect*qÑ!y¾iÓÒ2gX‚y"Qd;3çœÌ¿ YyÐZÇÁ`àE¢d‹B×MÒL~ú… Ü–kÌv$MÊlGl›¼Ä2—8RÌd­¥$}H½q(_\*—„øó¡ÒSÆÖ¯[ÍBˆ©^6Ç"â°Ö¾´ÖÑ9꺎Š šS6Õ^¹‘a¹¦À\ €µ*›©¨¼M#'_©›KVU…ý~ŸbŒ”’y“ DÓž¯ ~ñ4J–Óðj™å×NÂ\ZZj¥É`0¼|1©²‰ ºá`¹æÀ\š-I€µŸiGàX['(c3B”ˆ”ÿÎ\å墢ŽÚERN$Y’U×uÌ×Úä?#plEª\`á Ëuf+ÐH¦{Ò¶I ´RfnnŽä{9PÜÆäÄÜd™8@Á¯,9JÄÀ€,7‹p +¨A•`‚!ù , ÿ@€pH,*‹¢rILGAQÊT&™g”XP$ªÓ­ª Èp1r’Í™¶Zë‹Éß9÷˜| jfB Gi~WeE y† „Š}RinlHCq‡N„…N ]‡aGuz§‘¢CŠ ˜`­ŽL¦‹j†ŸºzLÃÄ¿gƒÊRÎÏ¿dªªÍÐÎÒzÔÕ ÂÅÃÇz ËʽD¿çU sP¶Ls¹J÷6L˜ƒëHQ V0¸'¤ÆhZ%䞎8@@€.‰EDƒtC l”V`XšS¢©°ñ¥Ë?X q°‘!¨+›½Ãà@*-ÌÒ„ÌÀFxB’ÎÂó Ä\€Få§3ªÖ®·† ]!ù , ÿ@€pH,*‹¢rILGAQÊT&™g”XP$ªÓ­ª Èp1r’Í™¶Zë‹Éß9÷˜| jE Gi~WeE y#DŠ}R† inlHB!‘$CfC N ]‡E¢›j® ¨¦¢$s·ŽL”j†NszÊEÒÓÍgÙÚÞßÍdGå ÝàÞâzäæ ÑÔÒÖz ÚöÏ\ÍøU ¥j cb€Á æÄ£„„ƒ ÌÓª\] 2!d‡ZnVéTgÈÁÌL0 Á‚F 2T¤€È‡ û `æÇ•®2Fj °”âÀ«k °XT‘.5X:)  Ó5,'RÅÓ,ÃtPuyz*R0|š!ù , ÿ@€pH,*‹¢rI4ˆ‚£ 8e*TJ,(Ö"¤Ó £Š*ý GN­ÎÝÅy#¤÷`xDdk\GnEd‡i ˆC Dd S Gm}GI•B$J ­ ^¨E°ša½ jD °xÆÄK n§­x‚ÚEâã©x¥è¥ íîÖçGóóìïíñnhô¨äãæa¤C÷ J·‚V8pI”hÒ> ðKE˜8ÀD\Æ2$ÛÄñC†!"•Ð¥j1Ž´YB² Ó‚S ÌÍT@DÁ„5d8Ÿøé52“›^Iü$ÊÙí  ?”*aijŠ'T5U›*¤ÍG‡ N:J‹lÓ±#Ï2aÕ-;nzbget-19.1/webui/img/transmit.gif0000644000175000017500000000476113130203062016766 0ustar andreasandreasGIF89aôÿÿÿÎÎÎúúúààà°°°èè莎ŽÈÈÈœœœØØØ¨¨¨ÀÀÀòòòvvv†††¸¸¸hhh!ÿ NETSCAPE2.0!þCreated with ajaxload.info!ù ,® Ž$AeZ ù<ä ’„ÃŒQ46„<‹A” ߈áHa¡¡:’êID0ÄF„Ãa\xGƒ3€×!Ä ßO:-‡‚Rj—TJ‚ƒ* ƒ t †ˆŠŒŽ„—~—" ds]š  š)t–¥-"–i;H>³n§Qg]_* ‹ ®R±3 ÁGI? ÎË´¨v$ý›j3!!ù ,° Ž$À0eZy¤0¨£q £ãŒP¤Ð£W  )"; qXˆ^ÏD50‡† Ո̢%‹`‹£ÔrÏJ{ 1‡ºÍ$ʈ…‡!!ù ,´ Ž$@eš6$‚Æ Ž`Œ 3*Å=‹  …ßÈ Pˆ\"FÁ©’í²`PÐ-­ƒÓÐd5VÁ"2Ÿ|?n"!( ¸Š‘€è•)e€„4xyc?   ‡‰3‹…™™ ”#wyJ l% o€^[b_0 V T[0mœ $ƒ4 >„'VZ ¨cη3ƒÆ$Xš¼%!!ù ,­ Ž$`e𤢍:D3 ÂH0¶,'j0¾Qƒs‰ ‚L(2HM¨Òj#Ðȉ…BŒ \Oéi`u§†=YŒù€EVL=I  ƒ…>‡‰‹•• suI WJm| \"_…b0 B¦ cV"d]*K1" H|@B?ÀI4…È#  S$¿-|¶|!!ù ,¯ Ž$Ð4eš¤a¨:D Äh¶Œ³·œI Á/€K¤$W-á 0(`3œÍÑFãÙÀ=±pf@ïýtéøQ£ìÉ  {f~*€‚„yS*mg) ”enu E^Z^ g@ kw(b& -w#"º xW"¼t #Ç#”%U$Ë`¶t±o!!ù ,´ Ž$Ð4eš¤a¬:*ÄØ± à1œˆ–ó‰v/€Kd¨ÉzÊâé–<Îp6%tP5Ù¡êS|ÉîØH(²FÕ¯›c¸Œ€`05xz*|~€v‰G„0t#  F hŽ0  #C d 1  I¢#(i - “ À uEL q ³Ì" h%±$Â$<Š·q!!ù ,­ Ž$Ð4eš¤a¬:*ÄØ± à1œˆ–ó‰v/€Kd¨ÉzÊâé–Îp6%tPÝñt¸ª¯é„5Å©3ÔÈn»G$ò€´ @aˆÏëwy{hoFS>k#  F Y" Š% E  Cb AI4$ (z¤:2• mI L½l#Æ# ¦F­#É#š>²F!!ù ,² Ž$Ð4eš¤a¬:*ÄØ± à1œˆ–ó‰v/€Kd¨©ŠVñtKG‚ˆ22ôëŽ7‚D"¯ªé$)‘„±é•Qø¼Šqp8 y l |~€‚„6zw2j# F Œ" ’% VŸC œ ]¥6a$¡ Q ª:2 \  EF I—&Ãx ¯"Í“¬F4$Ð]#¸x!!ù ,³ Ž$Ð4eZiä ’J16„<‹ŽˆB” Š?$r½œTêHzDP'"lä(†1±5–y½Ãá8tg†œ—p,’qÛ”Múÿ*   q ‚„†ˆŠŒ€”"}•# b?y{ {)Šs -s­:–9>e ¤E,C\3 ‡^·3[ ¬ž¾¸°S·Ä|˜»²?!;nzbget-19.1/webui/img/download-anim-green-2x.png0000644000175000017500000003326613130203062021324 0ustar andreasandreas‰PNG  IHDRŒŒ®ÀA>sRGB®ÎébKGDÿÿÿ ½§“ pHYs%%IR$ðtIMEÜ ;+µÅä IDATxÚí½i]×}à÷_Î9÷-Ý › )H¢È’%{<‰5²]ÕX3Iy‰=žÉæÉTÍ—äcò!“T*_’©I*•©©rùCÊ[äESq•c[Š<å‰#ÙÑ´)EwŠI½¼~÷Þ³üóáÿåéÛï5ºF£!÷­ºõ^¿ÞÞò»ÿ}8>Žããø8>Žããø8>Žããø8>Žããø8>ãÀï¥3ñ¿^Ùí›ÓéT޹;`Á|äV~æn‡¿‡ ØçÏŸ§þ4M3÷÷«ªZø!_ºt)í(¹Û!ÂïQ ‚.\Àüà<0B7ýú1²¨áp(/^” ÈÝ Þ¥à¢ÇJÉQJ …£„$¥4÷ïÇ·=ÎÌs?L"’>@‹@*$‘ÜÍ’ïrX¶AÒ¤‡‚ "¸²²‚»A£—P̃e}}]Q¬>D Â3%K¹¡Á»–__¸pUÕ4Mƒ} 2”RwöÁ‘=½ G ‰ž‹êÃS€sC•u” Á»–Î.™N§XÚ!!„‚#Š.--‘¡§B!"XU• Ì‘4ØÿðJHЦi’ÞGD)áØÜÜLˆ(óàQ©³@]Ihðn‘(}µS‚RBÒ—""‚ ˆˆèãØ—,Î9Z$e 8¤„£€HˆHQ ><¥ôQxúàìU]ÝIxðne(ËËË$!ZHŒ‘DŒRÚ”`ÝHͻճmÛÄÌ©¥><©Žª«ž«.G ¼[ÔÏ"PB ª k-+ )%*à ý?úµ1¼/’aí¤‡>žï«DI%<(©1&-gŽwud Á£Ë@) Á”9çXï—dpPD(¥DÆdfUA,}©3Oº„»1Ƙ1…R#•QòÞÇâkiÛ6fÕÔÁ³8G ¥¤ß…Ô Ì ‰™9©cŒIƘ´µµû¶Í"is»¡Á;KÒ/aQ[eŽTá#cL†¥”(.ƒR!¢‡ˆ-"Dt`À2³M)Dä ‡€5˜òÍOsÎA¥øüu ^D¼ˆ´"â  !xñDRJ>«2ODAUU!Q²ÖƦi¢B3OEÝ ƒï,ó¼ ï=Çc. [rÎq)UbŒœ?t.@Q@*"€Í÷«|¿†ˆ¬ˆ0pæ^9Í÷Êi^S8J'` –¸’± eˆ £k¨ýL¡Á-Ø‚ ™àzZÃëí[p-¾oKƒ‚“O_Ó¦”Øv›RÉãEÄÇEfŽ!„À̉™£ÅûPQ·¿õ8?ß‚· ‘º8›”R­àˆHBh3T!¥ÔQ‘V¥M©ªÔ¦1Ƥ½@sWÓsq7›e,Ž1"²ÖZ—퓪%C2$¢Qe@DÃê“þîÃéûñAúNF²Ó ö»õ×é+í³ü\†D%Ì4Æ8EÄ:ß61ÆhÛ¶m™9¤”ÚBEÅÒ®QhÄkn4x°Ìõ†ÁBD6ÆÈD¤^Ï ¥d¬µCpÌ<€ ÇÌ´ò³þ?–síY`9P §aÓ´°Å‰ØÄx»üH¤ûÓêðÃð1Ø µð&ldhˆºÀ!AŽ4Ënдm+ªÊDÎ;—/_>Põdng¼Eí–”.//SÎuné6ç˜J•aQ'Yˆh Ëã¿ã‚¿¯ýD¼YP¦œ`‹¬a+®ÃD&¸®ÂÕø½í_Ã+i êÂXD Ëî!YåûÓ}æ œá8I˲ K0’e ¶’Üã‡ÃŸŸsiŸZÿ=þD$aͪQWA˜á€c TUÅMÓÀx<æÉd5ËŸkž¼~äúÎxI»Å[&“ µmK!FB k­É[B0DdÑ1󪬂JX–q(§–~ÚÿC|¸}`ß/pÍ6áexµyŠŸnŸ£W ŠˆFf5¼¯% ¥Kª)Ê’˜ˆH“•†W`ì‹çíÃéaZ…{ñLß²þн¼ñ›æóq"×`=ƸcÜ‘‰ˆLRJSD¬½÷ˆ4š»*½( îYk“zNéjãÁ27ÞR×5õ="5ns2P ܈ ˜yÄÌãlÔ®Ѳ9 ÷/ÿ\óŸÇÓ~e_/lbc|Ÿ[ÿ]óņ×Ð}L)DŒ9Û¬™fæ«?Wå¹'ΑcSÜZ0öœ<¸ôÙø\½EpÞ´ë¿n~5¬ÉÙH)­ghœiAc:­Bc­ vÎÅ­­­Xö hn) Ì˼x ---±÷ž Èdh¬ˆXkmEDªŠF̬°¬˜“pÿøï7ÿ8òKû%ħìÓ_à5½HOÀ$¥4ÉñŽIJi ¶RJÓ”ÒVþzcœæ¸HÝ;[i5¨Væ… äïÅxÖš'ÌEŠ(æÝ ƒtsê~œªê½øÁö›ümñÔ·.9&"ÈÞ¤”’ª&çÄwxNÎ99{öì{æfl™›fŽWTæ‰Ô&fÞK6v-3[cLŒ1c"Zb戸Ìc<=þ…æÃaÁ)Çø´yfýóæ·ëoâÓ©‘õ|U*(zn¥”ŽiJIa© ‰1Ö)%Íûlƒ%çºÇó}ŸŽcó"¼Z?Î͇æ4ž»£k/иê<| }Šž…ˆ!—Th]ŽˆˆÖwФ”:hƒv9tžÓ<¯éNÓÝž>}Õ+J)áh42s$Kg·ˆˆ3ÆŒ˜y”r+D´ÂŽN®üBódÕß³'Ãñ’}uã·ío×ßà'%ÀºˆlªT‰1nfŠˆuJ©VpbŒ*Þé¤H™ ,Kòc>'}¶‰¼ˆh‰C”$±}Ž^j¿I—Ü)¼O§å}Û ã4pïÁ÷5é[ ³Bs-æ‚Y)(¨íU”&é¤Ìh4‚2¨7Ïk:`æÙ.}UB c …9„À`bŒlUU3qÄÌgYAÄ•åÏù_w·g÷bжÿûÒÆ—ùÒ®¥”6SJ“¬ó·JP2$µêDl½÷MN¶!„6¥Ôæ:Í{D YõxDì—$h™B ¢˜.æäaJShê§ènð¦}>UÚ×û+iX=çê‹ô­ ‰ÚX1WF"’|$fÆ£Xk!«wÀÀÌÛ!eö«–øaé€9}ú4ÆQ½"µ]œs&ç„:)cŒdé2vÎ-iä—‡ŸñŸå ÍGöØ{có7Üo4/à 0ÉêgSÕNa’¡™¦”jDlBµJñÌìED«ä<"n«–ƒY1T4ÆDD스ÔhN))L!¥ä@«ñBþP$ÿ:¼Ý>Eß¶âÃt2í+Ï…§Ò²»OµÏò *UdfÀhzÉQ_±ÖJJI¬µRJƒÁ½÷¢öL¾)[†Àvé‡ÿKCW¥‹AD&"-v¨*"¢%D\!¢ƒÆ¹O7?¸»÷ÿÒþÕÆïØ/¤VÖDd6cŒª‚&ÙF©E¤ƒD“yˆècŒÝ×ÞûõšJï)—b-ÅL)…Üt–bŒ!Û!ÿlÊÞW˜Õ–‹–rJjÄ×OÒ³î$¤w¥3ûyÏéþt/Šþe|#«¦Í™˜á‰ÌÜÙ71FÉ] BDÒ4 ¨-㜓[±eø l—2üß3t9ƨ®´ëœÑPí–./tîý‡þˆ‹»zí×Ý““?°_€MØ,ÕP¾æìocl¬µÞ{ߊH‹ˆ>„ €D‰¹¦V‹¯ûg$¢”ÝíX–ü3Úr¢Ð¥¬š$K€Î@mž¥—!ÇçÒ»öa}<ì_ÁWÓnd; –ëŒEûŸTÊ8ç „ Ãáp›|+¶Ì­³-üŸRÂ¥¥% ÿ3"vq—lèVˆXYk‡,+°<þ™ö?{w/rŠOT·¾hÿPDÖcŒ)¥ìmÆÕÛÑ –Ï€¨á äúÙ¨åÅ™´|@O§¼¯’£()¥­µ E[JN8ó/Ðv@#~(Ý·÷7^°z˜õOš§%Î`Qh²ŒÆ‰³0°Xk%ƨm1BH:çæVl™[Fm—¾tш."v^‘1¦Ê…Oc"k2qü·âÑ ì–ð´{vò{ö÷RJ°)"qBØ€:ÆXQã½o²ÑÚ@WöX‚fïNÌ*$’¡<£^Áú}kmÙ[ººŠˆ1ÆB%u]“ùþlDÉsøzu†Vé¾tÏžßüQrîœiŸáçÔ¸V5ID RVMj‹sî@m¾Iƒ·ïJw†î<é³l—K)G¹ði™™Wø$Ü?ø ÿ3àÒÂxE|Þ¾6ù÷[ K–(›9à¦q•&×ȶ9&² ”™ÍÔxƘÄÌÉ{K ú§~Ï9'D4{ Í8±Öv}ÕADýÐ$7¼•))qØïßs¹Æ89" ÿ½¢½Q¥”IólDcŒô ­n/0êJkÿóh4â#UUÅw‰1šÙ¯ÚYöŒ˜y–GÏÿžŒ “Ší¹/ùïð¥lܪG´¥’EaiÛ¶Q[¥'QbÈ$”Òc2™Ä”RŠ1&ff–ªªºûzZk™%„¼÷Ò¶­ŒF£nU NŒQŒ1 " @ æd¦Ö¬ PºNS3‚ñ~Œ`sšVÛ'ø¢ÄYN C–2’1f®-SFç©¥s³õ0Ý|¹ÆµqžrÃ;c8·²ºÜ\æ±2çâ¹ÝÊä[î;õ_˜'5TºÍ}XÔó)+ìKPˆHÊÑaÖÚmC|ôèOmšËŸÑ2Ó­­-ɽߒïÄ{/Î9Qh4<c„<‚€:HÞéÞ´`׿HÿßéGÌ»ñþ°·¬üRp£Oão|‰ÿ0Çœ*© EDŸ£Öœ qƒåì¿ÛZqWJ˜~ .·†pJ‰ÔØM)qŽ» ˆhc/K̼,"KãÏ„ÏÁé8×C ‰m7³úµØ¤õ"zÛ¥ö5ZÛ4MÛoü²ÖjmH²Ö&f–ÍÍÍ®ÅÔZ+Î9‡bŒ=_xá…tõêU)ϵµ5Y]]Eýç\Ëа{Ó42;”R’¬žfo23Î4¢`¶]ôÔÒL–·èzu!}hoe'¼L§êËOäæ»4­siRŽË$5~½÷:5k‡·t»$LwÅ,--QÛ¶XUyï)פ²µÖä9,œFziËp6œ[œ3'²¦å9Ô_‘Ú+Ú»ã- š×%XJ”ÞH°Öƒ\ºt)ö#Ûý.ˆ,qÒx<æ¶mÁZÛ©§,eˆ0G˜‘™‰ˆ(O–°)%CD®y^<˯Ú†‡öôœ Õàðã[_£¯­Âšuo«ª2!¯S.ªª¢#®¬¬àÖÖ†0KÕ=Wãí7õ¾c²v9Ú´<1Ý("rÆ'"NDÜàýÁ0ΗnoÛ­¯òŸærƒZUPε)¥¶ßRÚ‡Å9µêÌ9—ƒAÇéäÉ“i^!Ñ~**@.]º”Nž<™Æãq É9—¬µi2™tž3§c0Æm¾'"OD^Õ©&Dµ´BDê­/óÿ -íù¹U‘ ˆXÁ¬ëÓå Ó䬓·t:W_--—PÀ@!ººZ]H©såt“sÎä²F†Ùô„™ñûHúð¢?îÿ’¿–¯’iŒq‹ˆ4'Ô@×FŠˆ±,M,%ˆñÜòyñâE‡RU•ê.M§ÓP@£ó_¢B““ž¡¨©Ñr‹©ˆ4þm¹Ÿ£W÷üž g̽roöD» Æ—mÖa%4¥†˜·wá–€ég¨ûT3p»'¦S t. 9±|BNÂ}ó[XqÍÖÓ¯™ÇµÿÛB“+ܼˆø¶mÛÒÀÕA<¥RXJ©rñâÅ[‘,s%ÍÅ‹S)mŒ1ÂÌ¢m¬mÛvžZ‘³ EMM«58e-ÎæŸáãû ¨þ@ú„ˆ8•0:‡™¹ª*SHÔé]s&†â^Jv÷mÃdý½Í;ÊCÈ{ˆˆÖZʆœvôé¤[}<|?àüxCx޾©¢Y¯º,º"† HÌÑÔ˜Cá)Ç@vÀr;F|O§û/^¼.\€¦i4‡ÙøÅ,i™)¥D!„HD¡(§ðY% òE2ð/áëéŠÙ¤Õ°§jÃêÝðÈ£M˜Ù…™”’Ñ‘m¥¹s~T×u*옃‘0 &Gí¼ä¶?!Ê}Òš°`˜ÙЃr~~ü¥þ3ûÕ Lƒˆ]u›ž1Æ °xï£J—=À"p@#0z¿»ME©zÒqªš~ÐD‘D²šõ9yØ•@ŸÇïìõ9¥{ý’]…3Dä28jHUIyÞݪh½¥? ´–·ÅdmíäÓ™rFD,I«sŸÈeûV¼×òƒ6CÔMÔÁ‚e)B©Š˜¹³%æÀË.£3†uçuÌj‘õŽ!„˜R ÖÚ”/„Š™2"â7Ç‹û’|•Cœ„ˆ–™­N Í^ê¶ùÅ7¹)/I ^]ø TU…ÅÐdLi4cMgÃÂA>þ;ô­¢N‹›Ú\þTºè<8˜¬F®Ž½¨ªê¶Âr#hr|Gþ,”2ˆ˜tÞ]¾0–Yõ-¸.WÌdÏÌéa(F°Á;£ØXgs‰·m}išu·Ôm•0jmgµ…ÅDmÊ·ÛæÉUïMï[ô·š¿0QžÚ Hœ¥wÂŽâ¦|ÕJNnó†àXÍû›óTS)etøsœe2C©rUÂ䤢O—ñòžS«p*W5ª“Á:¢4hÕÞ\`ø<0ó<¤r[ˆµ¶›ÈM³ eâÓòà¢ØKÜÀ5D DäÀ‘ÖÊjãYWU¶HºèvPîÍÚ4}Õ¤YïyRÆ£%—]mq¶cBû*ìÙ½†¥`ùþt&‡0ºYÅÚŒ§ŸI ‰jˆ~lí¶I˜Ý _"b&³qÄ'dn* ¾‰oäB¦6—>Æ\±ŸÊSgØêävï`鵄ÞvXnô?Tʨ-Ó4M*¦€ëPhˆZÈÕ],àÛoÓ ûyÕ{äÝÙfìæÍ23õj±çFì÷âVï ÕsêR÷AÑûÌLš¾×Q¦À±’¥ù5/üJjŬǃÎÀUÉ¢ @êºîŠ ‡‡6èx4-SÔ¿hiD*ÕR6~£Žc-û›1Æk¸Ž»ç{®¹©m 8ô³¹£^Ò"=Ø3²Ô†AÌúã_Âr!PW ¯@:Ï¿ÜA³K¨ûºÈ(ú(õE’¦´eT-és.ÔRÊ¥Ûês‹¶•6¡Þ³„_Ž'³tÑ\23.òn­/<4•¤K!º?>+uAt`çæZ’ð ¿‘k:"j%\Ô.?ÈÕlå¹@í)¡xÇ6I—ke0)8ªfsÝJЦ¹<^>ŠHLS™îYÂŒqœëm¨,=ÈF·ëÓM!™t¢qn»(6¬ƒ‘SQ:™rb3…R)Öõ _´¼ó¨úü˵8ª–´ó KXUɳ¨v½ 3€ªlÞÏÒEú›YŽ$0ý«G0œû 6ªrŠ&²ä½¥dÙö¤‹+vŽýrdŽyjiήҎˆØ( BÄfÏÿÐ%«u.»Iç[‡C<#¢-(Ö ÐÂÎa>м•›ÏÊ+¶_5w¾·T——Æ}É;†Á¸ç7Ú%RX2œi^jD/º›‘ÒthïÍ·-¤…P ob±ýLw'Ê^¥Ù:džZªëZ÷B–!ƒn…ŽB>íG° ºè‹Ásˆ"–NCÓ4©g?ÝSŠ:½¯ãBS#íù“JÉQ~'{ÛÞl¸Ë5ÆõCQµTñâìÐm(^DZ"òˆØ’`Úç¿ÔVà2†%ÙV*÷SÊÆÆF:&Ï+Ñ}Ï&ÍÏ‹`·7º¼"eˆGýèÅc¶ÙX¥{]ï]»mžás´{–W›à•=˜Ûõ5m±.–Qòy¿»(¥ràÀÌ“ €˜í’ÙŒ5ÞXð t'céîH’ÝMÇœtôsKåHýP­µ f#Fšcˆuýæ«èyO’ ½JÏQ 5"¶¹†8”}áýxÖm•0š§)Å«BÓ_ëë3Hó¢–èÀ•QÈy윣W#Zä-eõcŒ!„Ð"b‹ˆm.MÝJWérzÆ>qÃ?±íôÜo3ûUqu]‡ya‰2i[äàW%if9‹ØXØ"Ú91+U4u#Ó!·¿” ”µ¨å×Eä®9´=W3ØYUDf¹Ð} ³ÁØœü¾ý?åe÷â®ïû¿µ_×á ˜ÍÊé Éú]9ki¿ÜŒ—¹o`J}\ºgzÕ@ž¹¦Òf{$™àúŽ?8H•ÖiäüiÐIwIçM÷X‚²²²Ò²ŸzŽ£à^—jI“‘UUI]×áB˜"â$¥´! Ö&¿>ø_âGÿ½P]Â){h)òw«ËðáŸ4ŸÿOÓ¯™?˜Í÷݌Ҋˆ¯ªªsã5q«Ÿß¾_í^)ª³1ÛÜâÞBÍ™«¸NoÀönÇad<™Vð®ëfWf¦c¹Wº[Ü9Ñ{eåûÝ‹éEÁ»‹­išdŒIÓéÔ33äåbMîcÒ”P€Ð<é¾Vÿ¥<±ôãþ3“/Û ³tŠ1NdB˜dɲ­ µmë­µ©mÛh­ÕÆ>˜3Ã÷ö¨¤2?RHhÛ6F° i\!Ä+ôÒ\©u>>¢iùÜ=Iš4ë/—I³2c^ÔsàQ—2ÄÓ«>÷S‰¶¢dé0ÍíÁ:àùšˆ¬ál8ȵãµãUY !\‘Ͷm7SJÓº®ë<É"–»#$mÇ­.=¤ÒÖIÈqàŸ±OÏ}«éœVæå~l-ü¡²¬°öDÍ«ç˜3¼ñŽ¿jø–±øfC4Õuí«ª’¦iÚlÔyXq¤º*IDATÒVú¸!"×q(c¼×C×cŒkˆ8 !Lˆ¨nšfª+sÓ_WK´¹¹™æT(Þ6`džZ*½¢\’ ³á¢îqN)ù´†ktÝíˆÇàjz0÷b›ÜˆÅ¹ÜJXrÏÎKÏ÷*ñB³M”Eâ¹@<QšN§~8Fñ!„ÖÓŠÈ4ƸcÜ\þ1yL|62@›ÞûÂVJiÚ4Í4¥ä1A%Œ–³–sozв×hù¾%L¹,uq™UÃW‹ŠÎ^ÞÄe‡'â)4³™ýSl«çèjRU-õ:ø°:’Д tõpœ-cÐu³hÜ#éG·.ÂïÇ':}+¥4 !lÅkcŒ·Öz]ZQvVô‹åwÛy}ÓÀì¹,UQÎOˆÚ1¹È¹Û笻œÃ ö;ÃŒ‘ÝGü‡´€ 3Û™ýËœ‹Êw´³”ÒF¡)¼%<ÊÐô#¿9_¦L7´Që!7¾¥JîÝ|:¼µU"¢&ÏÈiëºnSJÞZu„ÞŠ“pK6Lßð-Äk,ÂÝ1¥Ôæ.¿¶}Ò<9/€gß›¾O·„ä©Uœï—Í;zk–––H¥LÓ4Xì‚;%i­3œwèûW¤ º÷2ƘŒ11¼Ë=ŸåsÄkUUEfÖîÉVUPnþmÛÆ¦iv,Hï÷šß6`æ•;*¥¥**ï‡b©Šò€›[¼B;‚Pø@:y$3W`1ÚŒÅ: B{·U-õß^LæÐÕÓ¢ÿ±(N¤ö_Ï""qε˜Õ“2Æ_l^Ãß !tÍoZp•×ï}·ßz·Åèûµ]nEÂttöëUûjIk=bŒ0)¥º~Ú|eÇ{W}Ò\{°ó­ADc­µ¹å³›@cÔ^ånlšÎkéI™Cƒf·¿½[‡ªõÁ`°íópÎsŒ1èîã*ýù拃¯ ƒn~°µ6câd2ñ¥TYËp8¼)Ûå–¼$­$ë‹Õ¾ZªëZ'V†¬–ZDlÃ_ÙoÒu·¹ãÉ<æf»©Ìì˜ÙiŸ0ç#÷>q–6,"B ¾”éÀ·š!o¨›ãÉÍg0`ÑAŠº~xé³Íá¶ÜŸk]Ó/!„¨ª¨ö¨#Oö#YöR8K6LßÚï«¥B´àcŒ5Ìf¿Ôñyþój©½ŸÏ†`¶Ci§†;f¶*e˜ÙsNHÇWè•ÐàÅÿB[*Ñ[kUÂl“*[[[qÞ|œÝ$Ë^»,nYênÛ6@×J¡›?²¾mšúOÜ—qb}ÿü;þ³0k*0ó ƒcòâL“C眥L§šÔ!èžÉš|…Ïg¿ç?»C΃E{Ó‹¿I::ÅZKDÄUUi#-ÿ|ýŸ‘çËáñêÅ"f£ ÌÄÌ¢ ¨‰PÂr’ʬð^ŽþØÕ«W¯Âêê*Æ1ƈu]ƒsNÇ•ažÛ‹9×C1F4ƨÛl!"™3é^¼/loŸ=™NÊkæ9Yã5È«e´“€™»ê<ÑÕu û²u]oÛá,"WØíúz¬µ¸—s‘$é²mžB¢ ”1¤”F#²Ö²NëšÙø†‘«¶òŸºú/íóKÿÔ?g^Ò~lçœzBñÊ•+Q¥}m¥dyæ™g–›¦{SNŸ>y˜BAï=2³ÞR1P[8 "rx™_©.¤W,žB@söß°[;txq‚ÙÚ™ns^õ¢Ð@ºm[\]]ųgÏbÎ ÝÞH“ðœ?žNŸ>§OŸÞXTX²ÁÞÍѹÿþû‰ˆŒ1†‡Ã¡É« ­ˆf¦êg×ÿG ¤uêŸéû0RšBÓ4±®ë”§wR…ˆºXÙAÂr«ÀÀÙ³g±®ë.IXH™®û‘™©m[4Æ äž_˜õZˆˆ¼>èÝöÏ–ã%jÒ«æU˜õ(iÖ[OMAt+õ9ö¡Q)“Ÿ#ˆœ>}Z·ÆÏ“ {=·ýŽ‚Ò4M'u³ä¥y ÓñxÌ'Oždç‘Æ› "Ú|²ûáæ‚tý¿æo¬ü“ð¿cÔPE\[[ MÓ¤r¤¬†:n‡dÙ70!„Ð\¾|VWW;)“¯tÊRFçÓª¥ß {ÖÌ4"šø¢yÕ} }FqÛ‚>ì“Pc#Æ1ͬÞn¦…ŽªšúÐ8ç°ª*œN§ Wv¾ºáĉ´ººŠsàÙóyáÂ\YYÙJi£”Ƹz=ËËËd­¥ápH<½Ý°%;3`"²¢“¿síón}ðõö÷–~•ˆb^7777}Žþ¦ÜÛËí,7-Šû#Ì.\¸@ׯ_§–Kͽ÷œ7È23Û#ƒ! sKD´dŒ9AD'élx¯û©ÍÿÜöˆòº{½þµñÿ&"!„5؈1n†¶Ú¶íæßéxÓ<±²œÓ%Û4&¡1}ƒKÃp¿GßM.Z€Yï¹B¤nJ Çã1ò,"g˜™gOÃXfžíÀ¶–Ýß¿ö¿6+õðÎüH¼b6±iš¦Aݱ±Ñ4M÷0-ô@aÙ—„Y¤šúRFm•21FUQ ±”<;€e¦vŒ<àÙFór\¶Ë8öÏ™oçB&WÌsé:mæµ»4YòtntáfwEäy2”“²´N8ƒ³mŠÖn’EÁ(¾DaŒ1v)ŒSafî`¼ŠYD4fù#Í#á“×~‰{9üúé}ðˆ¼÷Þ9677÷"]æÆËö»Ÿú¶3GÊÀt:…Á`ÐAÃÌØ¶­@®ÑÍ£Ít~Élè@@‘Ëæyûhúä6WÛ Ùsr!½`žN[Øêî!…E·Âk½m[`f2Æ@³T"5ˆûÕ{ÖZêIž…fii‰ªªÂªªp Rƒe;Î ›R2Î9g­­²¡[‘©ÎÇâ\ûUùò‰ŸOoÒz^ísƒ[ÜMºÌw²¯=H· ˜yÞ@)eT59ç:ŸGÅ>‘Žé~žˆ6y ›ô]zOø°¼ó}­y·|4}Ë=)é¢*À™lïÀDDôÞë4¬NªT+ìœ7:V%OÿÌó¾4¢"âŒÅ"ÎífyyÙ"¢uÎÙÁ`PåÌ•sÎ!¢5çâjúÌÕ_Çöë÷ü“øTõ|^pÀ3ó m—¼‘Dn§:º)`æ@ÓÝž={¶‹uÄq:‚ÀªšDƒ6M#åt$fµmämsÍZ‡‡Â‡Š&¶a¬ìùôqxÉ~jò*´"OÁÈ‘NÊ•'zb¡¦:p §4ViÞ¹@êtuÇÙ¤ÑhÄUU™Á``s² ÎÓÁÒ•rœ ÷ÊO\û•T…“Õ·Nþ·íŸ ÿ4OnhcŒ-ï½oÛ6xïãõë×ïˆírKÀ,‚æòåË [fçÀ˜Ú¶•Á`@êå "dÛt Nü®y“…¦ø°lÛŠ¿a˜óñ“p•_J×h³0œ1‡LuújO½!f¦ªª¸m[)‹Ëc ZkÑ{¿£è|ÑYU‡Cãœck­©ªJgåšÑvÎ9ÝPcœçøcÍûáG¯ÿrªÂ‰êÅ•Qÿþèÿ¯S½÷-3ÇõõuŸ*ʲ]n˜EÐhŽ©o«mc­EÁ¦i ª*u%CÓID„øšyÍ·pÖ`4U²ô^ÿ S!„—Ì+98Û¡˜{›TòÄ1L'!ï½(8MÓlwŸí+Ô¤Ÿ‚Ô?‘‡Ã!çiçF7ÏcØc«ªrÌ\^PÅÌ.—oTƒŸØú™ø}ëÿ½˜8¨^Zþ¥éïŽ%*k›¦iSJm®éõZ^9Oº–ír Àì–cÊÑWT¸È3µs¥æešºÊYo}á_1¯Ràu| |°´i€…ଟ}<&¯™KPSÈ­*ê®kÞŠõC <›Èš å\ÁÇUUq~>åPA.ïë9 f³‡MþÃÌÎ9çŒ1Îã êÖý0WÙrü®xÚ~n㟵n~Sõì‰ÿ¹ýÃåßÑ¥a0ëzl1´më½÷)¥”Ö××¥_“{#Ûån®^½ š˜TÕÔ4 ,%SaˆJÛ¶b­Õy2ÂÌÚÌñ5~¯šç̓ð©âöIœËá¤ù@øwÍ2¸ð‚yY?ÔAe3›œÌ9 Ϻ -g„»š¶ÈK5:@¼÷šÎÐQ솈ŒµÖVUe‰Èåš§ÒÄ3 ¢1f€ˆD¬DÄþýÉš~pýŸÆqû.jìuø³ÿUøêð«ðÎîJ¯l"뺎š/Ê)Ø«ír»€9ʳ^ÈŽô~™6К•c·ßZã!; *˜-ˆ"âÀ³„ˆc"Z¡¬Œþný‹ñ=ÓÇæt]uך¯»ß OU!oEÉ·mí5'yjg^&µ;g~Óœb15¬)‡: ¤Ýš V¾F²‡?Öüh|ßÖ/¦¡¿€¯Œ¾–þï•ÿN6išB#"MÛ¶M.–Ÿ5þy/_¾Ë^¢2•q#ut;FÏX©â¼Eèeµ™fmoMŒÑ¨î·Ö*4c"À‡îãí'ø“ÍϦ•vî h|cðªÿšû­ðmó"¶ºø!o[í>cÐQayã½}¥þÜV­Á@pž_¯6Þ¡J,D¤êS[ŸŠï­ÿa\nî ©ÝàoþyóÇÓG¬61ƺ‹Ð5X˜EÐèz8ÝB»WhˆÈä´•1fCcÌG0 ‹ãêǧŸƒóÍß”*Î*ðÂð›ÍF¿ÐívÖ XÿŒ:@@K( 7F-ljPÁPÕ•aÑQ%("8úÛõgã¹æ?ˆËÍ™™^ èÞ}ÉÿÁÒÿ.5må&³6„ЄêcÛ—,“ÉDæõíÅнk€9hÈcˆH%1ÆhÜb`ŒQhPÀÀž‚SîG¦?›n¿OlÜQrŠ {xÛ¾Šo˜§ýÕWÂuXƒw†ê  ó›{C‘Ê÷‰sÐP=³.„P}ªþ”œo>Wêû»xã•Ñ7Ú?ýóôªùnV èt¨VaiÛ¶ !ļlt[výqo{°î°€¹%hòl²Çcsdt½Ž¡ˆ8fåªß+gªžþT|¨ùh¿Lb[ó¦›BM×q‚Wd^–ËöRø–y6ÖÐh$5€ Ðö‹qÅ÷òá<­¤si”Þƒt& ²Øw&›·‡Ï¦'†ÿÒÿ•}67Æ·!„š<¢Ã×uÝXkcÓ4>ïÜNo¼ñ†¶È.ªHÕWßNX˜}A£‘Sç\×PªªlÛXDXku‹jEDqˆXÑ 9Q}ºþ©ô¾­Ú×Ñpk[3‰5­ã¯B€ `q©’“ì`9YYJ. wxiý¿3±æ¹Ñ/7<ü×ÅæØVgéÖú¶m}¶£¢÷>ZkÓ›o¾rÓÚ\XæÔºšírÛ€¹hbŒ8¹¬'Qi£98frÌCƒ`¶'7¿UˆhÍû£ÕçәðÁtOsê¶½žmº7h¾%¯»¯ú'ª?mR8tƒlKDmJ©ÝÚÚj­µ±®koŒI!„X×uÚÚÚŠó`)K.çì‚:ôU?‡Ù¸šÒåÖb£¢“±kTËLD&KΰXk­Õ¬/":GD,äñ!<–!?¡ÕpŽ–åA¥3<‚SÞÆeûjOoTmÛšµ0Å7i‚ß•5þNxÍ>¿m^ÈÔu/u›w ù|Û¦”|¡Õ…a!„BˆÆ˜´–²ð¨Àr[Ù/4!ôÞ—ŒŠªªJ“d­åìI1ç·,u,Ì6غ¼]Õ ¢ÓšYuws‚³K¢¦‡â}n5Ý/Ki•Ær T`+­ÓëéM~)¾h^Lœöl]¤uQV¡r¼Æz´øIÝåBdæT×u2ƤÍÍÍ´›d¹“.ô¡s#hÊ8MÙŠQª(•6ª¦r©„n{ÓȬADãœ30ëËfc̶õǺJ0×Þ”÷1§#4à(:½¼„CoÓì–›WbJÉ+4 IÛ¶!¥™9N§ÓR %(³±ew,‡ÌÍH…¦/mrmnYj@Î95Œ5o¤ÅÕ¤Ë2ÕëÒпÛtâà.ïƒ1𤀔ÛWò ¿PŒ™ y PšN§A%J±(´›¤YvŒîbàX ˜ýHšœÿØN¿,²/q [‡ôVÒð}.Üê&ujfZŸKVWý±õÛVóÄ3§ó$NUU]®'ƒSÖº”%¡;Æ€”å¢áÍÿ¿ ”©í¡*Ú¶MÞûT®µÉýßÛöíEªì²‚ðÈÀrG€Ù 4šéîÛ6„®ziiI›•¶IU/ÃápÛc¹Õ­µÛàèï2è¯ñ)—9èî&• ý Rz_o÷)UŽ4,w ˜Ð,´mJpTꔽʥº*À€>@ùCÚ–>8ú!©ä(éßê}ÝC´¨Ór?^ÐQ‚厳GhJ§/uÊmï‹n÷Ë<8æRJ…¤ü~jå~l•£Ëfð̵oœš¾ÔQ[g7`özÌf}}}Û:äþÏíC¢ÀÝË‘f¯jJ(%Ž>Ö·uôñr'J–rÊnG Fa Ë<˜úìA¢ÜU°9`ö¡¦v¨*=æIžòû})Ó‡i‹àèÝŸq·K”#Ì>¡ÙñX Тq7zl»=VBÒ“$‹À¸+a9²ÀìžTÚ<‹ ÚïÑ— ½¨ì¾¹[ ¹«€Ù47z %ÐÍs$Èn@ì ÃÝË]ÌMB´Ûë»Ù×-7ù½»’ï `öÍa¼Î¿°ÜõÀLr|/Àð×˜Ã„æ¯ ,ÇÇñq|ÇÇñq|ÇÇñq|ÇÇñq|ÇÇñq|ÇÇ÷ÂñÿK‹‚ÕIEND®B`‚nzbget-19.1/webui/img/icons.png0000644000175000017500000004663013130203062016260 0ustar andreasandreas‰PNG  IHDR¼,!4`bKGDÿÿÿ ½§“ pHYs  šœtIMEà ,> IDATxÚì{œÕ™¿Ÿ·{f`d†AAä¢QÀhzÄõ«5WWW¢ÑÕèÊêÏ[Ì&«kÖ$î]H\·]ã-*£ñB0ÞVì *‚"ŠÜâ8 ̽»ßßuj¦¦§çTõÜÞê3ÝU§úÔ©:uê[ïyÏ{À0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 ÃèˆÃ0 ì1¹˜Lr«VËöؼ2uÞK8Ÿá+^È›ÿÖ½f¤§½õë¿1xïãÀ‰îó|`ŽîèÉf³‡‹ÈõÀÁ@ м\/"¯E˜õ`¿nÒ¼ ìoWÉ$ì (VÛé0"º¥À¹À?ÄÑw p×›WÖG$4{”ÿÖ½fÔ‡-<{;£‹ð·O |þn! £ªg©êª:;϶ÓUuI¾md/`#ð,0-ä²SÕûDd!ð÷€] | üYU£ªC#*Û~!¥$]z!ßJ'°^r%°Në Å"—盃´}]Ü Ü ”¢ ܆ÅXbw7àAà—yÄnÝmÀo]Ú°Åfó¾â…Ý–pþ€Éßè?DùðÓæ…ªžÜå¾f€óEän_»mq·ýLy Àç:î„Èáî{¨ÖNUý pðœˆü­ªŽ>Æþ ü·ˆœÑõ®ªÝ÷ƒ€× Xç~ \݃t8+@sÄuÿmà UÀ:6 ø3|—<ë t<Á6 ¢ÀçàkÀíÀõB—_·ò¬? Àm¿QX±; ø_à„îòð­°,½ƒ=£ÿ@e¹:G\þZUÏ áx`ûe½p|Ä.î!–Ø­Êd2¼ "ëVÿ PüˆdEäkÀBàUÊ­ I›éõžÛ_×—v“îVàL<«[Ôì‡gÉ/-àyÈæ|oèdÝ`à2à÷x½*÷ãugÆcúµ×^;ëÚk¯µß~ûÝZYY9jüøñß¿öÚkw÷—A.v& °2» b`¦Ûg‡q–Ò|ù×o¸¥&_þaXY{;£ÿ5…°nDnQPÕ“‡rj™€®;MD+àyÞˉÁá7ÌY!–ýQwÑÍnÝ ÀÀ yÑ­«6ªêñX쌮÷aÀ«Á·¤@uNi³ÜžüÏ9Èåx]Zw³ X÷_Ž'Z‹²O±kàso]¡è o©»þù\—žÎÈóHå þ›mßÏ€¹6q°1ÜÝÿ_¾|0 娽Oþnü®Xì½£Ù:Éÿà&¼^E€¿þ™ö.m‘æßR>ô¡ô°ƒ~PTÿÆuÅ©ÆS£Èßè_…ô;Ïâun‹µK_DSÕ3ðº6ây„®/vÏ(°ØÅ=€}±»¸0äß?øÄ»ßz´ª–ëDäuUýTD¾Q9_éź<(Ns‚æ!'~šó‡Ýº |\3\¾§@lcz°n ²Þ`ÙD'Û¿†çO|’k" þËÜ¿‚¿¯°'p0Ï­ëƒT¶)Û!vqûLqâ4Ìük€›öؼòþÀºû׌˜ ðxc "Í¿¥|èCM»Ṫ÷)«¸ j)N5Ž £–KÃAÛ±Oè˜Däa× çÍNì>\às|^7šÏ€5af ª¥yb¾ø½x xÙY}k(l7{!™ ‡gA?ÚçYÀ½îÌì¥ã:‘6ÿr#:êBìôÏ|QÒ\àòïá^:gw#ø£<Ú[ƒåØ5Ïú£ñÆNì…×Ó²l€ÕùI½´og¿±‚6Ën?¸m‘çï,»ÿŒçâv)p•[EþÆ ¼}‰áä÷Õ+¡ÍÊ»:ËÁ 9  ¿h5^×bØB?ì–³îEàXà[n9BDžvj#:_pž|˸öãYî–àY{¿àê»ÀZ<—‡ÞêÒ= 89â<†õp]¡(´à;8§Îó¯¬ßh¬œÉqt«© ¬_‘àî ‚ÿ­ Ï¥¢«ƒkhs»‰Bði œƒBùO¿öÚký—o¿'©P%Ÿþø?·ëÊ|²{F DVõÒ¾ýÆ^x>»÷ç¬?6ð"Òaß|wéçÓƒx¹ò/ªãºLYÅ¿Ñ6^áÆ¢ú7®í*cp0ÐÂ’ýšŽÔòY{Ï‘{CÌ>î„ÖA9¶=ßï |ß]¿ìcñâîî'"ÍyÒ”K3™ÌØx<>YDÖEp½Å”Ãñâ’FQ®~Š×uÙ•{*ìd¼Ø¨…ªû9‘µuyИsÏå[{mƒà+„éAcÃÝËo>kþB<—«õjß{;,\o0Ïmc Þx‰SÉßÅ> 裃ÖÞþ-pÞ®¤‹AcYt»¼åßR>ôáÀ µk‹S§aƒÖ=É¥á†wf¹3 !²TõNü êÝÃw80,›Í6Äb±kDäç^ï®ÄL¯Ë{0ÄõëþËNäªK·ø,çžË·®\G{ö;ð4÷Âu€Þ±pÎæ9ñ{g/ÖÃÞ*oQ |‰A2ß›xá·ô|Pn?ìPþÛkáí å7úÆÂ+"âYÒ^N FcpâöL¼îÔ³#»8¡=¸Ê=\2x–Ý t~®ªã\þï9Á»TUçŠÈøÅ®ÏëtnáM²{k1^„ˆBNôÏó’/ä‹g€¹ò¯Åó›¾Âbë \àÍtøkljßBÒ šÎÚ ·ï8!×±÷0ÅÞÖ½flsþnŸ.…mOÄnXùƒƒ(~Óóô ¼Ø—ÆÀ¥'±˜ŸÃ³xt’®œ› œo)íg¸û÷R›»®iÔÉW€CÝËǼ}ijßÁjá”8Kç¹xxs}zWáuýß…eséç3|Å =Êë^3ê{*fûKþ† ^Ã0 £÷ˆ»ž"Ï”Ò&|½\Sh‹Jð!°¬´œðÌ›ÿÖ½f¤£š½¿a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†aá#ƒ´Ü·‹;­ †a…Å‹S€InÕ*`ÙôéÓÓ…Èÿ9/äÍÿµy3EþÆ ¼ªº?0¨Š€TõéX,¶ ›ÍN†Äb±÷CÊòÇÀÕ]lߌvŸ÷^vÞ¹ø¯åYÿ0pZ¯íYÀ÷q Q^—7%Û°O3pð³ëÞ."²ißÓK€ý·cÛ`¡ÄÕ½ÈPÕNÛ9-‚ü¶ûþ‘°ï¿%"²ÿ¶n‹è:´¶"r¯ªÆE$3P+öâÅ‹Ks ˆ=¢ïà®éÓ§×G$4{”ÿkóf Èü¾O,ä¦XUüøW'ðNæŠÈKªºZUÇoŠHcÈ‚«+jÜß[€»€cñ¬»ÕœÓƒ:Yj®ç³@ppPî^®¶8¡7—»¨!ÀV·þ n]Ðàw…[w¦Û/Ìúw%°LUãîûpU=<'Í~ªºRU“è>~P÷¹Ü-š³øÛpߟè%ë`vžm§;Á=»—ÏUsò(tpp¹ˆt¸ÿܺv÷Ÿ[Úý§ªÏ«jkSÕrÍ!Xÿܪ(êªú¬ª6©j»6PU¯¶¨ê+ @/^¼ð ðËa‰ÍmÎßí3 ò7¡àžp¿W#"å"rs,S€X,¶˜°4ID›ÎìÍ´Y×ׇ˜ç'y,iKrÖ- XÖüeAˆÇp»Ë£´ÆWœø›éÛðÝ‹6kãŽr pMàáv:žõr¥;¦åÎÒ{‘;çÃKEä'!å]Òü¸@÷ó³]Ô­nÛýåt)Š¿v¢÷¼^•x`ûe!ç}hÑsŸ«ïe@q@èú/¡ %$¡óIkæ’œu‹\Ú zëþSÕò(êŸ+kÞúç¶ERÿTõvUíQ¨ª¯¨ê-!çÿ ¹¸´›¤·ŠÈ™î™çæ{5xîzoÐÖÃ}çZþ† Þí ›Í€ç&Ðìšë'å×ÍîA£‡R!ø1^?x¾¼G†ôû½í/ZâDÅáÑ÷žU[ðÜ pë'9qpÛæïדcx)ÏËÇ(w ;ål+uV§FDœâdà&Úºs}æŸáùòœ-"·†Xö]zf’»NvB§IU/`mÉ\Úûˆû¢÷×9b7ü$伟èä¥ê‡îźŵ;#€ß¸ú ´E¡ È{ÿ9ërd÷__ÀYKó¶®üyÛÀ0­¬À%ªz?ppv'Ɨ˹.Ý%adê¨]ž³úà°›é>¿{,nß Û®üݾý:c ^9Á5<‘|V“‘"²7°;°30XX€ò•WºÏ À,`…³†„Õ¥VåD]Ê}ŸØ á¯_ãÒÏ )ÿf'ô|ÆóÏ[pß魯‰è}oÖ[ólO…™™ˆ¤Dä*÷p»#G|ù¶sEäÞ^¼ß~8žU5ìîUéágä½¹b÷ —6LΔm7àÍ€ÀØ oÀj̽tŸüѵCéê^•V=nœ§Ã¬(ï?©qÇùýçòéäÛ¡ ‘3Ed¹Û¾ÂYUÛµn¿0™í )§¸6Ðo£ÏvíÒC„ëÒŒF€ksoúæ¯VÞÿÍ_­ü«[îþö–ÎInßHòmÞŒû_›7ã¯né•üÝõ¾;âüÁ&x]TDä:i>‘ODd³,Ô¨êÖ”o¬klRîAûFò|9ðùíNÖ‡ÉEÀwßçú¼n}¿«9èÆñ]à¼ãˆmÑ À¸(N†ˆ¬‘ ó¼õ¬WÕIuçö´>ú”ò ½>ÀÃyDoðÅì Bòî‚õÀwÜ1”๷üÅå¯î¼êDï®Q·"Rˆ6 ÏܽsWêЪê%n€jÞ6PUÏ‹àpfljÈSx½‰k€YîEûH·=Lrh­þ'Ýܶ®ö0ù2ç…î\ÏtŸ£Êßl‚Ïo²¯»°;þX$"ÿqÙâx® G¸Æý‰xýâx¾`¾Êà…_Á ÝI´ù˜® Xƾ_éËßïŽfò[}Ãz^@›ŸrP<ê^f÷‘ëw^„¿›¦c´†B0œöV]ŸÂ$ÕÕ´¹°LtÂ6÷˜ö ¤é÷ˆÈ6ÝX}úÿÎSÕtn´†ˆòêЊHÞ6PD¢nÁ ù¶ª.q÷߀¸ª¾ ¬Åó°¡Ñ cÀ ^™„ç®P¡ª»ªêè.ü£.p,J2x‘G)®z™Œk@W°ÿÖzžeû6÷}Út,uç&LÊis¸/R»jâŽ)Rñ£ª?ÆÄdzè½%Àè;Ž”Fô»¹ò§Üyÿ í»s£â¼jqí£7DE €Üß®Þû,.( ëµh½ÿD¤"§nwÿ©j”/qçfiýsãEÚµ.BK‡6PU£n/Ç õ{÷Bõ{¼†ßÓÜ߀óC½«r¾ïü­ÿå£ÿ:þë4ðÆØìÙ;¡ç  ù¿6oÆ,à)à)÷9ªüþd©±ý °Ð½Å‹ˆüEUÅb 9éJœø,ö‘Âjï·±<êŠÏe“{àTÐ6 $8HÏ_¿/kïÙx“¡DÁ®À“xÑ;š©xV½îxÔ‰££ð"¬„Y:´.,c‡6À…ï +ßNï?ɸ{#ïýFýóóþ–ªªÿ]U—ç;+tÞô!C»6PDÎÍ“¦]b¤Tõ:à§xƒê"éÛx½Ž'‹È¿¸gïÓ¾{þm<ŸÕ?Äæ•9íÔ*`ï}Í üÚîüwtö³ÞÎßœ‚w<°FUq£4‹esÒŸã… ºODΊ@põ¦àÍ}¨Jð–ÐgÓç1¬¼/^ü}Úzò|jh³zïéîÿ MŸ>ý—aäÈœ¶+ÿ׿Íùý‡X˜¿#"~ãó¡ªé‚Šl6{­˜e.8ú9•§'£Þw`×°o ÐË´uÑœìžïúUÀ«.}˜#”:yy8ÙYÖŽëaúíá|¼îéix½ÇÅ®«—ÕxQAü€ô¯†,^op>‹?vßksÍÒ©ªÞè¬;+ ô2;Œžûwn/Á°d¾Ø}ÏÒžÛ…{}Èyç+ðÂ’mÆ›Þ\ÝçiNìÒ¯9jzóþ£;!-"Ã|±Q^¶N\çmÃŒÒ "9©‡*Ž9BÄ]xÝ÷¹Ïƒܒ+öž¢k×#Ëß„õ†=ÎYNÞTÕ󽼫êC"òíæ3O“¿ë´+6á…+ M÷¸¿“ñFá¬X«Ýç/àÍ<…KƒOϱ3–M(–Z÷͵òuÆÛxáš4Œ:è¬+oºOU'iNÄ‹¿|„ßµ¢àÝè^¤¶ŠÈN]¤Û‚gñl†…xhoÞÿNÅ›Tâ&:=ݽl\Cøî çˆÞµîX^ ”SÝ‹y6ªÆ4`ií¶ ÙÂ[+"ªÚãûODöZaC*÷v‹âÏE·m ˆ ¸pTnºà_Ñ}ˆ§€ïLŸ>}]˜ù»éz{œÿkóf ¨üþAXÞ JDV‹È.x~Š?~üÖùW}1‹}+±‹Ëk[¦eÜ>aâÂ[X·:ðÙ÷WNEx=׺²]°t¬®ryDùÆÝ›ô¼nãŒ[ü‰'*Ü÷·}gu e0¡«S×Óõ@¸§Ü¹ˆâüßê~·»:5ßý½#äûà™íÓ‡‘?"ʃ®~Dá»{íã ˆÝà AÏÚ½ÕÄUõ '¢ÛÝNT¶»ÿœØ íþëCõ¯7ÛÀ^Å Øoá Ï7k•Ûö­°Å.€=Ê? ±ÙÛùý±S'â…9êʲ odø3G©{°…­`.m£³£â2<Ém™¹¨Ù=„B{ñPÕxWBRU¿ ¼,"K­ºZ†à 0 ׋У6@Dž 1ßí¾ÿÜ,… U-‘UmmEäŽÁRÁÝ@¶)´E%øX¶£ÔzŠHÖ!ÿB ëíü Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0 Ã0Œ>N‰Ã0 Ã0 Ã0 Ã0 Ã0Œ~Џ¿q`˜Ã\Ù)0ºy8ä¢V~Ã0úñ=ZìtDÃG‘wý¢E‹ ’ÿš“ó®ßcóJ»8ÖDJ§â`áÂ…±#<ÒÄCß!øó‚H`}Ö]ÏŒûœ ¬·ò†AöæªÃëƒñ|f›€×€ëcs“¯õòáÝ ÌÝ}HçþÐCàÕW_eÿý÷`É’%½.t %|;º&|^¼ÈÛèÙõáYú‡¥x]}¥n)qÂ'ôš·Ô»¿MîÁ‘¥ÿY?{ù*{U}äx’ÀiÀŠ,t‡óo»ûá}wÿ´{»ûè>`Nln²±—³X |ØæWTT¼R[[{XEEE-PîVoF×ÖÖ6TTT|âÖž¯­­=¬‚×'jáÛS±•èí©Ø5Ñ;¸‰Ú¥a„ Ù€pÈ:1aô®ÐóEÞNÀÈ}öÙgüœ9s¾úÅ/~11nܸI•••» >¼lÈ!%MÍMÍ[·l­«©©ÙôÉ'Ÿ¬zçwªçÍ›÷ÒòåË?>¶ÄŸZùû%q`_``²{8uÛHX ,Þu/}áÀãÀ´>tL wL;4…öÀlà¹ØÜäßfo® |Œþ ü8Ë àó#>–ƒG\Þ>Ï8Áy(ðGàX`}˜zÓý-Ï©‹%îÅxxàû¡…®€QZ|·Uìúû„%z·Uìúû˜èœÂ§Ä‚" ‹‘ÍfÍÖ;×;îDÌÈ¢¢¢Ýò“ŸœxÌ1Çœ0uêÔ©EEEEªŠªwi@—IÄ«0‚N§ÓË–-{ï|îÉ«¯¾æ‰t:ý©N ©•¿ÏSœœ“óÀ:1×ÜGÊó(prÛ·îðëÁcÀ)ÎwiÔB;{sUð&ðblnò·î®¼³bs“O¹u/GÄæ&—„|_ÑfQ~ÏÊŸ[_ƒuûa÷2 Z[[+õƒN0ÊYx×¹üGõµµµ¡×Í®,¼¹„)|·Gð½*xÁ¬¼&x£kè{Slm/Å@0îÖ[oý»“¾qÒì1£ÇŒöEÞG«V±ø­$Ë–½ÇêÕ«Ù¸q#›7o`ĈŒ=š &0eÊ8à&Nœˆˆ "lܸqãã?þÀ%—\rð ^÷a }ÇÇu°—?—áÀEÀ%À®;ø[ë[Û(¬3—ë€z©íëéËÍõÀ’ÐÎÞ\õ(p0*67¹Ù­;x˜››|Ñ­«Äë%x867yFHÙ6»Ï»?®Ê“î^< 3Àr'¼7…)x9µµµó»Iwp{o Þ°DïöŠÝ°DïöŠ]½&xMðl±; 3{ö죮¼òÊ«&Ož<9«ÊæTŠ ðÌ3ÏðIýÇÄödbŒØ® •eîê@k”ìzвdßWÆ–ŽgæÌ¯óõ¯#F "¬ZµjåM7ÞtãÿóÀ‹À<_×l(KÍjeʨ G(£‡)åC¼*œj6Ö m–}ç­1ŠFNè/åÏ=ßq¢ktÈ¿½Ñ‰Î_õB™g rÖ5Óùd…¼ùŽaðTo¤B;{sÕ§@SlnrR`ÝIÀàY×Åæ&_ÏÞ\µØÜäøëôÎî%î=w¾ßt"|}ÎùXå^JCà Þ3P,¼=ÈUä¼FŸ¼Ã† ‹××׋ˆdpþÂªŠˆˆª6Ûé/q÷ ˜pÛm¿¼àôÓ¿u^qqqqCC=¿ýíÃüöÑiÙ·‰øá1bcÛö û( ¯Æ¾b'N9ñ¾qÒI --¥¥¥¥åÁ¼óâ‹/¾XgõËôÅò?öðƒ6z+'ì™fê¨mÓhï}ã醰xó(N8©Ï–?ÈXà~àȈóYœ ¬-`Ù>¡½Ïf¡Û¾mTÁÛ ïYÀ=½%xÃöá5¬G?9 IDATÁkô'¢´V ”©vh‡KÝ›¸½ØQTT´ç#>ò“#?âØl6K2™ä—·ÿ’u»­E¯ÈR\¹}Õ 61¡¾v ½ø ¯^õ*çýÃyL›6­xöìÙß?~üäSO=õêt:ý!^·c¦¯”ÿöÛÉ”ØGÌŸQÏèaÛçj;uT–©£ØÔð)¿}õ>®|íUþ¡o•?ÈÁx¾¶c בÀb<ßà× T¾ë_÷“ûòú^Êw^DB;ìÖ®m˜›|1{sÕ±x.DÇæ&_ËÞ\u'ž»OØ÷yÐ-ç?óˆ]€/ÒR0tÁY[[;¿¢¢b#žkE>jjkkGUTTÜSè _Èðd†1(o}}ýF‘¶¶% ~Í¥!ZbÀ𢢢=,XpëÓ§ÖÔÔÄã?ÎïþŽ­'o†‰Š„q* éäFV¼Š_Üó fvLJzر ,>kÖ¬KÒéôx‘ ²½]þWþð(—î½}+ÃédØ¥4Ë÷÷­aYª»çÿŒ¿™qR_(cœØ-äìRcð,|'á n‹š;éÀ}ü¾¼Ãk!X—#D£ÚÏßÉÞ\5-67¹4 zŸ &rƒÛv1ï¥x‘CfáE…˜\ ü&轋¶8¼Q (ݘR[[»¼µy¬¨8xÅ€LèF/Ñ+>¼"‚ªšàVì &=òÈ#ÿþåþ|lSs3÷Ýw/¯üõϤf}Ž–j7NÛ×’v_;¿tÒcÌs»rØÎ‡ñÍožJII ¯¼òʳ§žzêîÁTŸÖNË߸b!sö\Ëðâh¡.çîO&3t¯¯ôfùƒì„goM¥ZÌ 0–ÞàYÚ\6úZ”†…xá°š œ¯/´/Œ"‚ìC<÷ýbs“ÍyÒ”8q:˜››\â!ÄñüÑýžÃÎ|x£ŒÃëGiè²aõÓDíÒP(lКÑß„‘1ð^bŠ1·ÜrË…‡|ð±MÍM<øàÿòzËkÔžRÓ©ØUEc 1Eãn |&æÒ¸~ch–õ_ÿ”×›_ãñǧ¹¹™ƒ>øØŸþû¿_€gõ+ŽXˆtZþakqÉÞG&vÊŠ2|o J?YÔ[å2–Â[vsæŽalòjÎÖôÁûr;¶Þ»°¸4²‡ÈÜäZ¼è{K³7W}-Gì‡ç?;oƵ0Åî÷¹¯å·þ@< ·ºåÚÇáÝ5Âó]U[[+þâŽe@²#‚5Œ°d;"XMìšà …½÷Þ{È”)SvëÔì JUUÕ¾vú#!T|ó›ß<êÄ“N:¯¹¥…_|‘·¶.æ³£7µMÿ‘Gèsâ6¦hˆ+øB×»qâ—Vñ›{kø„dÝ[üùϦ¥¥…“N<ñ¼SN9å(<Ÿ¾x¡Ë¯¿ÎÙ{¬¡¨‡5^ƒ á·[ß݈rú˜à“7z£üÁ{û~ ã³Ûcܱâ{=^|Õõ}èžìÍc*ˆÐŽÍMÞ\ìü!{sÕÖìÍUë³7WmžvuàŠØÜäÍ!g½Ù Ú"÷ù‡äŸhãÄÀçýðü|£ $+**Ô_ð¬Í†a Á;|øðÝc±Ø§"’ýàƒ–/_¾–.ü¤ª««ß‘¬[´´´ô›v9vÁ›TaìÅ_|UL¤xÕªUüù½?óéQŸt*v[…n65âþ" âSÄ›pMüÎQ² YWþ.<$>Nâé¢Y_˜ò·¾âùTŽîcõs#^·÷V»U B%Pc§!ÐìE‡·/Ð>¼AzêÏ¦Ø ÒS^»ƒ—P»ëêê’¥¥¥ÇãñuÝ JóÅîСCÏ4±ÚËËx<¾û1Ç=»¥¥…·ßy‡5;¯î výÔS×M°ðÆhoí¬»í¬½m.žµ×ýͱô¦F¥XU¹’åË—ÓÒÒÌŒ£žÇwÇ‹ƒ)Q—¯ìÝ‹]ç®à[qÅ-ZÔ~Ófõ X»²öŽ/úŒI-Ë Qþ õA±‹;¦‹ìV-&vÛ³ø*žõwÀ š~õÕW{MìúB¶+1ÛÝöeÍ+»³Ým7>¡‡%«««û¸¬¬ì¸†††ç²Ùìè\KoÀ²›)))¹¬¡¡á» ¡½¼ìôƒüàÄŠŠŠÑõõõ|°òV²*™C[-»­î κÛÎÊ+Á§‚zÿ…SÔÛîY7Añƒœi¶}¸³{~Àþò&L˜@yEùèK/½ôÄ[n¹e Þ\ó™¨Ê¿êÃ÷ù»Òe]›||±s® ¾ï®³òú‹_n9«nÌ;r‘¶h&ÿSôТ%<²fZÔå÷)Á›.¸¯r ð3zgð–1¸™F[¡ðíMLÔ]‰„Щ««[RZZzt<ßkéõ-»%%%—655ý‡]‚P_^FrÈ!'47·°fÍ>³–¦Ò¦b×t*žrmµÎÆð,¸±<‘Ú-¾U˜_^'¤sT_ãÐÖ켆O?ý”tKšƒ>ø`dÈ/\Ê¿·|ĈXC—µ_üiqÏoW~º¾•WVßV˯Ÿ>Þ¶gCÑv’z&늨ËïsÑŽ@ßQvuÇh…&kb×0LðF!zß:tèá±Xl“?ñ„ˆ " 2äü¦¦¦Ûìô‡z‡L˜0aüøñ㧦Ó-lذǭî˜RÚ„.­þªßÍ¡Ól´ á +ƒç"áŽ$Oä†UcVRSSCKK ãÇŸ:a„ñxÝú±¨Ê¿_|E§;¨„®³îª±øB6àÒ ÁÁj¿~z‚–á|¦¥ì²(Ëäô~P_O·[Ö0 Â׉Þ¥¥¥'ŠÈ_ýuÅÅÅ×466Þi§>tJO9唯*Z´eë>—Ï©Ñ~O Z«8•€hÚE_ F‡¼ž%7Û’,àÑ*œÅ¹EäXyk†F­ÔRWWPtòÉ'/P¼DQþ!MŸ1¦­Ú圇ö® ¾Ðm»‘ôãm¾AQì‹]÷·³°e;ëF†6ÕDY~ÜQÓêê1&4›a†aD/xè}mèС‡ÄãñO‡ ò÷ÍÍÍ?±ÓÉu6iÒ¤Dº%Í–Í[øld'bÏ·î:‘Úê»Ûº´ Z#¦m.­ÖÚÒû~¾Ä®A¿×+ï†멯¯§¥¥…‰'&ð&%(Ê?N:Ä$Akt¬ÍÂtmhýpohÐæ¯µ_Ä/'wÕ®ÙO¢*¿Ï¾xÁõû:åô­°a†aƧ¨™Ô×׌Íd2vÆ£¼¥•••“Òéššš¨þY¥G›H ßÖÅwOhµÞÒ.,™ª ê§©—FÔ)Èàï´úñ¶×r-ÛDK} ---TVVN³pÆtØåßC;Ÿ9Tcn°™äˆÕ€?¯?ÉD«ˆu¡ÈÚ)æÂ•ÅÜv_qê,³ˆæQaÚA$ûiÛ­èÂÂÙk ¨¨'JKh³¡†^þ’lþpdš+n3ªùbUr_ ü¨Âí"Q´íß*t»9Èâldå÷ÚêìP»m Ã0 ¼Æ6“N§I§Óm¯§J×½ê² k;Ã|ét:Úò‹t[Jéæ ¨øÓgx~Ë m‰µçgQ".¿a†a&x2Y ÓØÐÐì ¾Òli׺Vó|×Vÿ†®÷k·¯?ýZ[éDñ ÓaÄãqZZZhlllÆ›e,EùÓñaŠNѶÖNþ’Éj«…·µÐYoPšÔ¾?¯›ˆ®ËW†tQYTå÷iìGu¶Ñn[Ã0 ï±-(мeËÖ:߇µ\+z.t]Ô_ôŠ‘ _ÚlÎv ú´%ͧþ* (étš-[¶ÔáM/«Q”¿©¤²ó”´¹+ü³n¿0êäiÆ¥Óœ4AèN”oóʨÊï³±ÕÙvÛ†a&xm{ ¡¶¶vS:&“Í2FÇtž:(ÊZ?».û¬@ëB› 2ëĮۖ»¿ø¾*ˆ/šsØ•]iiI“I§I¥R›€v|ºÏ¼åo*Ý­ó=²´·V¼+³:qK4XçD±8á‹¶Èä¼Û Þac£(þ4‰¼Mxo†a˜à5¶‰,аiÓ¦U™tš¦Æ&ö ù¥!Aë‰SOrùbÖr’$ãÖe¼ÏdüíÒ&~[÷ÀÖÑm홟DCC-é4›6mZå_&Šò7Œ˜ÜµDÖŽÖ\ßz+NÐúBWÒÞ‚[$ | ~nu…ÈŸmSÅ^Q•ß§?ŵµ¼†a† ^c›_ýê5««Ó™ [¶lfJÑÔ¼ EÛÜÚ¹1dbÍ»™€ØÍ ±V±½ýfklƒV¦M£¶¶–L&Ú5kªz±nv(ãÎûu®wØõˬ™ö¢Wâ–\¡X$°ŸäXyóѼóþQ•ßç] ÕêkÊ«a†a˜à5¶‰†—^z饦¦¦ôÖ­[Ñ2‚ñìÑ^ì’km­íÜ|w†€ð ŠßVÑë|z[Ÿæ¸;˜ Ø)=‚-›7ÓÔÔ”~饗^³pjåoˆWаӤü¢Ÿ6qê _ º1Ü:]éÚ æN¬»#&S_TeùqGôÇ~PWÿH¸–mÃ0 Ãè›im`šÖ¯_ÿñÚµkß=zô~7nä+¾Âýéû: ¾¬xcͲŠxó£!.ô–u@ – Îæûù°…³o1αî9ä(Ö®\K:“aãºuïmذác ‰ð¢4t(ÿnã¿FéÒùùE¯ï~i/„[gPs‹æFcȶíÄÖú½ùZ7ñ8Ö®¬üA¾ÙÇëëƒvËÆÀdñâÅy×OŸ>ÝNŽÑ«DjáÕ.øÊW¾"vúC% |þî»ï>™N§Y½z5Æb¤Œl/ôüx²íÜh¬Î‚ÛÁ‡7èæàoÏÒšFZ]#¤ƒè«”J.>„•+W’N§Yºté“Àçî˜#+ÿÖݾLz訮erÀ=A39n 9¾»­n î³æ³òæ;°¡;Ó0öð¨Ëïó8°¾×ÓõîC£¼¼ü¼òòòúòòrÝÆeKyyùY!—¯¸xX ¬7åÖLYèv&v{²Ý0úµàíŠ?ýéOj§?T²À– |ÿCêëë©­­ÝøôÓO?l!\ëf‡ò¯üh ›÷ùv÷{ù¢7 l5 vƒ5°H@ôJôõûþË?Xuù}š[ûp=½Õc˜Ü”nÇ~ÛC<Žãð¢OÜœ ìì Ì®Ãó[>Κ*c ŠÝ(ÒF¼#ð¦-q¤R`Pa§>thÊf³Ÿ.Yòö™t†·ß~›ªìì]´w^Ñ€Ö*^3´EdÈäøðfÖßVËoPüv»ûOaºHuu5™L†·ß~ûl6û)^w¾F]þÚ]¡yÔ´‹^ÍcÙõ®¿^–àV‹p'´ì¼/©Ñ‡¢üAn£oƹÝèŽ-lÊñÂMŽn–ö`ßÒŽa&ž%·++îXà÷ÀYƒ¬mšÕWVV>_YY©Û¹ÒÀçO>ùä}›6mZÙÔÔÄ¢E‹ønÅ…”IY§¢·U´­¶kn«õ·¦Ã ·\±[&e|ÔE<ÿüó466²iÓ¦•O=õÔ}D×ß¡üZ´ˆ­]N¶xx·¢·Õª›gÀš¤;‰ÞÐ…6[2œÆCçòÇ•ßg+žE±¯q;¶¨hH¥R‹S©Ô3©TêªT*µ/ðUàåˆËU ÜÄë2À¾Àä<¯D·;ñ;X¸¸(¢ßžÑŶýó¬;ؽMéf_Ã0Lðöß_—œé b±XÖN}4§oÊÖO.\xcsKsËG}Äšäj.®¼„x»çq›è úä…¯/j%#íDo®(ÎçÆ'Î¥£/çÃ7W±råJš››[-Zt#ð‰;F-Tùÿï½i8ì*Tâ]¿ 9á´ìæsqÐn„.€Jœæ#®åÕwV²üA~,ìCus¡;¦¨(6:ßÜÕåååw•——™J¥¦R©#€‹ ß•Âç'zs_:–«òˆüáÀƒ¬múÏEo^jjjÞÎ÷rRSSó `#¨B`G,µfå5”à5z… PûÎ;ï¼øö’·ïÌd²¼ôÒK ]SÊ£.êTôŠJ‡0d¾ÛB®U7Wèæ»þG†~4”?þñ9²Ù ï¼óÎï¾ûî‹@-ц¤Ê[þ¥[v¢éÐ+º½Aé,þdÁ˜»šoJŽb7}ø•,M ïòûd3 } NnpÇR¨Ý=€s€—ÊËËß,//ß'•J݆gÙ‹BôžX }òRQQñlEE…nãõÄ <ßêwisóøOà²^®‹ñÊÊÊÙôÌÝÅØN¦OŸÞn1Œ/xëëë‹€bU©j‰ª–%ª:ÄN}d(Ðlxî¹çîX³fõ³™L†|×ìÌÿÛõ†ÇòwïûâµuöµÎ|xUò ]€á±á\;ö‡ŒZ½3÷Þw/^Ä„5Ï>ÿüów8áÓB´ÖÍNËÿvãî´}#Z²St™—ìDöØŸ²¤a·Þ*µÀIx\ôõîÖ ¯Qx>¼çã ŽkÀ³ä½Y^^>3•J½à¶…Í>Ú§3ÚŽ}ö‹è”I'nsèo®èÅú¸¸ßŸa&xC£'"Dd¬ˆŒÅó]ÛÓN}¤dÐXýÄO\½nݺWš››¹ë®»Øôç¿ò³ ·2mè¾]þ€tò¯+ö-ý"·Nº ‹60þ|š››Ù°aÃ+O=õÔÕx!šê)Œ•¯Óòÿai-™oÜIv̗»»~ ¾y7¿ç³Þ.×{Qôúb÷õå·‡óá½3•J]Šç§ùž ÁCååå¥R©{§BÎ7nMà N^œZ@±ß&ÔÔÔHp!¿oµa˜Hcá:ÿ]Ü/CT•X,fqx£'Œ‘=g}ôOFUŽ:¶¥¥™}öÙ‡óÎ;%EÕÜ»é6¥7íP&»ïÂÙ£Ï%‘I0ï¿æñÞ²÷(..¦¦¦æÙ—^zéjUýØÜ ˜NËþyç±sÍÿ¡ÿ7©ÛÁ^ÿácƒ¾Gͨƒø¯¾Uþ ãÅ¿S ü6Jì–——çZÌW?îH¥R™òòòÛñ|fWà $Û¯«½H¥R©ó.-™ Ê}NæÅ+€/„QþŠŠŠg¯mãn‹kkk ùR\€7@m9ð0 ÏÂŒ„q7pnh•ÊÊN{Kjjj¤«í~{Ll?=dÂ&£0àͳÞoaEßp`ÂôéÓ/Ø}÷ÝÎKg²ÅñXŒ“N:‰™'ÍäÕÆWXðÙ“¼ß°|›~xŸÒ}˜µóIVzO>ö$>ú(™L†x<Þ²~ýú;“Éäx–Í­½(öº,ÿ7N˜IÉÇ/¡ï<·Ñ­oô4âûFó_åÑÇûlùƒŒÅëÎ=2â|âùìÂ!Ÿàõy/Jƒ/J÷æ¤R©ùåååáY"ü7âM6AÑ ù-Àw°¶f žEýúœû/»ç‡y/ôPðN¨©©Y“³ß$àƒšš›m4ÁÛSLð&x°‰À1ãÆ;jÒ¤IWOniifÈÐR¾þõã™5sÅ£‹xkË[¼W·”Õ«ÙвÍé#ŠÊS<† ¥˜Z6é#¤eC ,àÉ'Ÿ¤¡¡’’ZZZV®^½úƵk×¾ˆgåënüí*ÿ¸1²¿AvýÛhÍJtË:hôÊÏÐrd§ÝQ“‰íºñ=á“T¦¿”?÷\|ø0:äßÞˆzìW…,s‚àÒT*ukyyù%À/€—S©Ôn†µ{B¼ceîÅʧÏ¥¢„6k²O3ž¥yÅkg6ãè80Pñ|ª/ ;C³ðö_ÑkbׂwùòåC²Ùlå”)S>í,M2™üâð®]‚‚ b¼I?Æ}á _ø»òòòÙ"2:›õ´Éĉ9ðÀ™6m“&Mb̘1Œ1€Í›7³qãFV­ZÅÒ¥KyóÍ7YµjUð%fc*•zàÃ?¼/ôV-Þ­¬•¿O2/LÔ%x³íë ¹hãìv%x3ÀaxÅW›}»¥¼Éo¦áY$üàM&qO໇·„Ž. —Ò·gÃÛâÀ"w r×_7Hèt#h'àõ°˜…ׯa„'x·lÙ²û°aÃÞ‘Ö‡§ˆtšGŽõWO6lØ#vI"¿Þq¼ÙïFŠÈî»ï¾û‰ååå' :tªˆlSã¯ªéÆÆÆ÷R©Ô“k×®}øoR…F÷ÀW+Ÿ§Ï×ötພ),H ø#ð žopso `á=Е'(x·¦R©ÊËËKð&¾oÀR ø,DÁ ^¨±®&•Há¹1<0@Û—yÀœBfXYYù{¶ºægjjjŽ·ÇBáE¯‰]£_ ^€­[·V•––>‹ÅvSU§x;¼""ªJKKËeC† ù¹]Ž‚^÷"Ÿ7åF~÷½óùµsqXÔG»Ñª›aDÂíÀ" ´›t%xVÖ[Xþí(++« Xx­GÉ0 ƒ~jáøüöÉ{Œ¼på€ÚùSGW«ê\AaÎÈï¾7ÿóùSjPF rWÅœ÷þ¡7Ë‘L&ïf›…×0B¥¨Í›?³¨îÜÌlo*€æ^Á ”ûßëêêÄ­ÿ ¨t«·ÖÕÕíBv¥À·€ý\ž-Àzà<·Žb¼AÇ\ÞEÀ&àMàI—~»QÕÊ_DZì2 ÃoP¤«mªZµàÍõá ä?2ø½vþÔckçOÕÚySõóySËkçOýîçó¦èçó¦Þ”»ouuueÁc Ç IDATŽ ÝfZ]]=2™Lé¡à½;™LjuuµY\ #\®Ãó—Uà¡NÒÜHsÝË¿ƒà- …÷{®LMx½X ÀgÀR`!ð2žóg.M“ú Ì áùô=÷xhRÕFUmPÕÏTu©ª.TÕ—Uu™[×ä–f·ÏL»u cðÐ#‘§ª+ð]Ì‘gTuðSU Zž‘o‹HmξSGTukHd(p…ˆüÁ¼A o6›<,"øIàìX,V[;ê|Uýð½‘s–͡ˆÈ¯Uuß@~o'&‰uÕÕÕªjˆ\‘H$þЉp=ø°oÎqß‘H$.Ì#Š¿|OU'£ÜêW¡"òT"‘¸Öª¡al7WcÝ÷ÉÀ*à·Ü üØXíÒ¬®wÛú{þùï`¸ÿ=`ám'~ýõ;È©Nä7ߎΠ`av¬~LNsë¾ÌäSÕÊ_D^±[È0Lð¿¡œ©ª‹Èqy ®¾ðÛSDVö=HU_ïñy^ çŠÈÝAÁ«^¸±¯r¿›oß¿ùäXùËøç:v“%“Éïwt‘ç^NØü]UUÕýy~ãqàÄnŽŸD"ñ~@ðþ p;^ žwy(‘H|˪¡alq` mî¼.k€À.xÖD¿¦>v=°[óŸ‰×õ~5°5„üs…nÏz|]>a‘à ,pŸvë†á¹ ”º¶n“+«–í>àL`:ðÖ ÞÖüEäi·.oþ"’qÛ[ó‘·ì62ŒÁAÑ64,ˆÈ\#…ˆüQU_Æ’¾ ìøþWyLU;›-íß_È>œï€?¹ãxKDn XXp]Tÿ7îY-AÚ·ãÕÕÕ_VÕ;€,‘ÿvódàç¶4 Z;¨éêêê»U5(vo‘?#TõÛÀÑnýòêêê!‰DÂ÷ÏKªêÀaÀD÷ûÿ­ªe¾x7 c»Èç?Æó•ãùk®r/·¾…< «/L—W8ÿ3Ð;Áa‰ÝSÝqàDÞù®=.Ä3än<¿å`W÷rQìäŽevà%àÌÀ Chù«j—ù«jù†1PUÍf³Ymc—*d±{VYYÙì²²²tÀw÷Ȳ²²+ÊÊÊ^¤Ó|{uå®Rx>ºþ¹øœóÞäÒÔºï_ áÙt¬k÷kU5å|t}þCUï|orijÝ÷/Ù­cƒ‡í K6CD6åYqàó‘ÛÐ`ýØÕYno‘…$ySDþ_ž ˆÈ»îëé9Bô`æÜ®O$æîŸH$öñª“c¼Ý!"ùÒ$‰ßÿ⟣d29¢K„aáQBû.üs€G„û>͉®àKìÕn¿Bä_Š)à(àrà<·†’ÏÁ<<+¦ß3VƒgÅ>ÏWØ·GÁ³x>³)`3ð)^„„¿þÏšúmàC<ßåÏ€ x=^ÙÑÌE¤ÓüEäE¤ÓüEä/vû† Þ¼jÓ‰Ò;…òýjUurÅîinADÖŠÈ÷»ÈU½°‹Ÿ».ð»§ÖÏñ+‘Hü¨‹ýïë"ïÃÜoüw"‘èÊ2rƒï–ÑUY Ãf'ô^Æs%8HâYß ˆÝUxF&¼`Ýå? 8¯+ý=¼pd{nH²¥x®]÷ú嬫«ÛTWW·/°µ¬¬ìqÚ¢G4g®ÿðƒÀžÀW¯;Aÿf`ûÿ:;˜ìã®C(ˆH‡üÝ`d{»üEdyÕnÃ0ÁÛî¢Ñù8&·Û9ãUµRU+"êDâ„nÒ‹Åþ¯‹$ùéTuF`¿£Üï/ë¦Ñ|,ßúd2ùå@šGºúªªªŒªntçáp«^†QÎÆŸê8=Ì¥/Dþëœè=ø'×Þ^Nø³­Íq"ú à躺ºÊÊÊŽtb?8öà|'¾—¸Ana‘Ƴ¢¾‡ç²ËF·í£(*€ˆ¤EäCy/7RÛ¾ÑmûÈnÜlk7ûÊÅóNˆŠˆ|5‹eº»"²¦›O]:h¸mÅÞÝβM X®¿ŸL&gÓyt‹À·„ïeÕË0 ÊZ<ëâðºó'kð]†7D!ó÷#Ì›a(^D'…ºººÅ´…I|Á‰Ýà÷xVÏ#i›òø1<«÷¥uuu«2†a˜àÍ#: iT«ª> TöÀo7Èú¤©Åë2ÂþÇî¦ö]ÑÉúñN˜+^wYOÙݪ—a”Yx>³ xî·79Á{S/äkc÷¤y¯K?RêêêšËÊʶ⠨»o:ãT]]]CYYÙ”ºººf«.†a˜àl6û÷ªz¼ˆtë·›#Z{3¸Ýı¾Ý‡ìŽAT5•óÓ2Ýw¯†>~øÃ{ióiLù9o’œŸâYôŰUÃ0LðFˆªŽÆ‹—¨x]ÿ¶a÷žÌû^áéº\Á¬ªÃ»Ù·3„¦€hÞ-‘H4ôô€“ɤTUUY2Ã0 N]]ÝSx¡Ð ÄAb†aý‘X!3SÕÕN€ Ð¥ßnöÜÖ|ÜçœÕ·ª›}:‹,ñ~À7ø°m)¯‰]Ã0 Ã0ŒA$xUõiê,¥=õÛ R¬ªE]üþñî/"²(°É£¶GW?."ßÊç® "üî™Ýd2™|*™LÞV]]}†U/Ã0 Ã0ŒA"x}¿]' ×nOŒZ'”ÿ_I~èû꺸ŒþçÛüý«««ÔÅÏ×7‘Hl‘F—æÜ®Ž±ººzðuW¾¯Zõ2 Ã0 ÂWULjÈÝ:n~ëU-Ï#¨ÏVÕƒÜï/Ȭoá"<¨êµÕÕÕÇçª~ȳÎ\®p¿MuuuW¡žk®8„»a†a†Q 1hí­ÀçÙlö?E¤² á‘"rmp¥ˆdT5ÔªêyÀªz p .’Â)¹?("_VÕ=½©O'“ÉW'ðÜ.KÞ ‰Dâ¶êêê©êH`ïd2Y#"çâMÄ¡ª:¸Ów¹PÕ_UUUÕäÃ2_ð&“ÉåÀ½"²,‘HÉdr´ˆí¾¾•H$–[54 Ã0 ÃèEÁÛß©®®>YU§K«ªªï"ÝTõg"RœH$ÒV= Ã0 Ã0ú?±APÆ}~—L&÷ï"ÝœØÅÄ®a†a† ÞþÄ Ïÿ/A2™¼TUÇ;ÿÝ_[µ0 Ã0 Ã8È`(d2™Ü ‘Ï€SÕ5x>ÀßösÛH$bÕÂ0 Ã0 càP4Hʹ;°@UG7w’΢&†a†a 0•53™LÞ\ "~l_ÿï‰DâB«†a†a&xÕÕÕ”©D"asý†a†a†a†a†a†a†a†aÆÿoù[°BÀ¹iOiE˜ÁIEND®B`‚nzbget-19.1/webui/util.js0000644000175000017500000003634513130203062015200 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2016 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, see . */ /* * In this module: * 1) Common utilitiy functions (format time, size, etc); * 2) Slideable tab dialog extension; * 3) Communication via JSON-RPC. */ /*** UTILITY FUNCTIONS *********************************************************/ var Util = (new function($) { 'use strict'; this.formatTimeHMS = function(sec) { var hms = ''; var days = Math.floor(sec / 86400); if (days > 0) { hms = days + 'd '; } var hours = Math.floor((sec % 86400) / 3600); hms = hms + hours + ':'; var minutes = Math.floor((sec / 60) % 60); if (minutes < 10) { hms = hms + '0'; } hms = hms + minutes + ':'; var seconds = Math.floor(sec % 60); if (seconds < 10) { hms = hms + '0'; } hms = hms + seconds; return hms; } this.formatTimeLeft = function(sec) { var hms = ''; var days = Math.floor(sec / 86400); var hours = Math.floor((sec % 86400) / 3600); var minutes = Math.floor((sec / 60) % 60); var seconds = Math.floor(sec % 60); if (days > 10) { return days + 'd'; } if (days > 0) { return days + 'd ' + hours + 'h'; } if (hours > 0) { return hours + 'h ' + (minutes < 10 ? '0' : '') + minutes + 'm'; } if (minutes > 0) { return minutes + 'm ' + (seconds < 10 ? '0' : '') + seconds + 's'; } return seconds + 's'; } this.formatDateTime = function(unixTime) { var dt = new Date(unixTime * 1000); var h = dt.getHours(); var m = dt.getMinutes(); var s = dt.getSeconds(); return dt.toDateString() + ' ' + (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s; } this.formatSizeMB = function(sizeMB, sizeLo) { if (sizeLo !== undefined && sizeMB < 1024) { sizeMB = sizeLo / 1024.0 / 1024.0; } if (sizeMB >= 1024 * 1024 * 100) { return this.round0(sizeMB / 1024.0 / 1024.0) + ' TB'; } else if (sizeMB >= 1024 * 1024 * 10) { return this.round1(sizeMB / 1024.0 / 1024.0) + ' TB'; } else if (sizeMB >= 1024 * 1000) { return this.round2(sizeMB / 1024.0 / 1024.0) + ' TB'; } else if (sizeMB >= 1024 * 100) { return this.round0(sizeMB / 1024.0) + ' GB'; } else if (sizeMB >= 1024 * 10) { return this.round1(sizeMB / 1024.0) + ' GB'; } else if (sizeMB >= 1000) { return this.round2(sizeMB / 1024.0) + ' GB'; } else if (sizeMB >= 100) { return this.round0(sizeMB) + ' MB'; } else if (sizeMB >= 10) { return this.round1(sizeMB) + ' MB'; } else { return this.round2(sizeMB) + ' MB'; } } this.formatSpeed = function(bytesPerSec) { if (bytesPerSec >= 100 * 1024 * 1024) { return Util.round0(bytesPerSec / 1024.0 / 1024.0) + ' MB/s'; } else if (bytesPerSec >= 10 * 1024 * 1024) { return Util.round1(bytesPerSec / 1024.0 / 1024.0) + ' MB/s'; } else if (bytesPerSec >= 1024 * 1000) { return Util.round2(bytesPerSec / 1024.0 / 1024.0) + ' MB/s'; } else { return Util.round0(bytesPerSec / 1024.0) + ' KB/s'; } } this.formatAge = function(time) { if (time == 0) { return ''; } var diff = new Date().getTime() / 1000 - time; if (diff > 60*60*24) { return this.round0(diff / (60*60*24)) +' d'; } else if (diff > 60*60) { return this.round0(diff / (60*60)) +' h'; } else { return this.round0(diff / (60)) +' m'; } } this.round0 = function(arg) { return Math.round(arg); } this.round1 = function(arg) { return arg.toFixed(1); } this.round2 = function(arg) { return arg.toFixed(2); } this.formatNZBName = function(NZBName) { return NZBName.replace(/\./g, ' ') .replace(/_/g, ' '); } this.textToHtml = function(str) { return str.replace(/&/g, '&') .replace(//g, '>'); } this.textToAttr = function(str) { return str.replace(/&/g, '&') .replace(/ 0 ? top : 0; $elem.css({ top: top}); } else { $elem.css({ top: '' }); } } this.parseCommaList = function(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; } this.saveToLocalFile = function(content, type, filename) { if (!window.Blob) { return false; } var blob = new Blob([content], {type: type}); if (navigator.msSaveBlob) { navigator.msSaveBlob(blob, filename); } else { var URL = window.URL || window.webkitURL || window; var object_url = URL.createObjectURL(blob); var save_link = document.createElement('a'); save_link.href = object_url; save_link.download = filename; var event = document.createEvent('MouseEvents'); event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); save_link.dispatchEvent(event); } return true; } var keyMap = { 8:'Backspace', 9:'Tab', 13:'Enter', 27:'Escape', 33:'PgUp', 34:'PgDn', 35:'End', 36:'Home', 37:'Left', 38:'Up', 39:'Right', 40:'Down', 45:'Insert', 46:'Delete', 48:'0', 49:'1', 50:'2', 51:'3', 52:'4', 53:'5', 54:'6', 55:'7', 56:'8', 59:'9', 65:'A', 66:'B', 67:'C', 68:'D', 69:'E', 70:'F', 71:'G', 72:'H', 73:'I', 74:'J', 75:'K', 76:'L', 77:'M', 78:'N', 79:'O', 80:'P', 81:'Q', 82:'R', 83:'S', 84:'T', 85:'U', 86:'V', 87:'W', 88:'X', 89:'Y', 90:'Z'}; this.keyName = function(keyEvent) { return (keyEvent.metaKey ? 'Meta+' : '') + (keyEvent.ctrlKey ? 'Ctrl+' : '') + (keyEvent.altKey ? 'Alt+' : '') + (keyEvent.shiftKey ? 'Shift+' : '') + keyMap[keyEvent.keyCode]; } this.isInputControl = function(target) { return target.tagName == 'INPUT' || target.tagName == 'SELECT' || target.tagName == 'TEXTAREA' || target.isContentEditable; } this.wantsReturn = function(target) { return target.tagName == 'TEXTAREA'; } this.endsWith = function(text, substr) { return text.substring(text.length - substr.length, text.length) === substr; } }(jQuery)); /*** MODAL DIALOG WITH SLIDEABLE TABS *********************************************************/ var TabDialog = (new function($) { 'use strict'; this.extend = function(dialog) { dialog.restoreTab = restoreTab; dialog.switchTab = switchTab; dialog.maximize = maximize; } function maximize(options) { var bodyPadding = 15; var dialog = this; var body = $('.modal-body', dialog); var footer = $('.modal-footer', dialog); var header = $('.modal-header', dialog); body.css({top: header.outerHeight(), bottom: footer.outerHeight()}); if (options.mini) { var scrollheader = $('.modal-scrollheader', dialog); var scroll = $('.modal-inner-scroll', dialog); scroll.css('min-height', dialog.height() - header.outerHeight() - footer.outerHeight() - scrollheader.height() - bodyPadding*2); } } function restoreTab() { var dialog = this; var body = $('.modal-body', dialog); var footer = $('.modal-footer', dialog); var header = $('.modal-header', dialog); dialog.css({margin: '', left: '', top: '', bottom: '', right: '', width: '', height: ''}); body.css({position: '', height: '', left: '', right: '', top: '', bottom: '', 'max-height': ''}); footer.css({position: '', left: '', right: '', bottom: ''}); } function switchTab(fromTab, toTab, duration, options) { var dialog = this; var sign = options.back ? -1 : 1; var fullscreen = options.fullscreen && !options.back; var bodyPadding = 15; var dialogMargin = options.mini ? 0 : 15; var dialogBorder = 2; var toggleClass = options.toggleClass ? options.toggleClass : ''; var body = $('.modal-body', dialog); var footer = $('.modal-footer', dialog); var header = $('.modal-header', dialog); var oldBodyHeight = body.height(); var oldWinHeight = dialog.height(); var windowWidth = $(window).width(); var windowHeight = $(window).height(); var oldTabWidth = fromTab.width(); var dialogStyleFS, bodyStyleFS, footerStyleFS; if (options.fullscreen && options.back) { // save fullscreen state for later use dialogStyleFS = dialog.attr('style'); bodyStyleFS = body.attr('style'); footerStyleFS = footer.attr('style'); // restore non-fullscreen state to calculate proper destination sizes dialog.restoreTab(); } fromTab.hide(); toTab.show(); dialog.toggleClass(toggleClass); // CONTROL POINT: at this point the destination dialog size is active // store destination positions and sizes var newBodyHeight = fullscreen ? windowHeight - header.outerHeight() - footer.outerHeight() - dialogMargin*2 - bodyPadding*2 : body.height(); var newTabWidth = fullscreen ? windowWidth - dialogMargin*2 - dialogBorder - bodyPadding*2 : toTab.width(); var leftPos = toTab.position().left; var newDialogPosition = dialog.position(); var newDialogWidth = dialog.width(); var newDialogHeight = dialog.height(); var newDialogMarginLeft = dialog.css('margin-left'); var newDialogMarginTop = dialog.css('margin-top'); // restore source dialog size if (options.fullscreen && options.back) { // restore fullscreen state dialog.attr('style', dialogStyleFS); body.attr('style', bodyStyleFS); footer.attr('style', footerStyleFS); } body.css({position: '', height: oldBodyHeight}); dialog.css('overflow', 'hidden'); fromTab.css({position: 'absolute', left: leftPos, width: oldTabWidth, height: oldBodyHeight}); toTab.css({position: 'absolute', width: newTabWidth, height: oldBodyHeight, left: sign * ((options.back ? newTabWidth : oldTabWidth) + bodyPadding*2)}); fromTab.show(); dialog.toggleClass(toggleClass); // animate dialog to destination position and sizes if (options.fullscreen && options.back) { body.css({position: 'absolute'}); dialog.animate({ 'margin-left': newDialogMarginLeft, 'margin-top': newDialogMarginTop, left: newDialogPosition.left, top: newDialogPosition.top, right: newDialogPosition.left + newDialogWidth, bottom: newDialogPosition.top + newDialogHeight, width: newDialogWidth, height: newDialogHeight }, duration); body.animate({height: newBodyHeight, 'max-height': newBodyHeight}, duration); } else if (options.fullscreen) { dialog.css({height: dialog.height()}); footer.css({position: 'absolute', left: 0, right: 0, bottom: 0}); dialog.animate({ margin: dialogMargin, left: '0%', top: '0%', bottom: '0%', right: '0%', width: windowWidth - dialogMargin*2, height: windowHeight - dialogMargin*2 }, duration); body.animate({height: newBodyHeight, 'max-height': newBodyHeight}, duration); } else { body.animate({height: newBodyHeight}, duration); dialog.animate({width: newDialogWidth, 'margin-left': newDialogMarginLeft}, duration); } fromTab.animate({left: sign * -((options.back ? newTabWidth : oldTabWidth) + bodyPadding*2), height: newBodyHeight + bodyPadding}, duration); toTab.animate({left: leftPos, height: newBodyHeight + bodyPadding}, duration, function() { fromTab.hide(); fromTab.css({position: '', width: '', height: '', left: ''}); toTab.css({position: '', width: '', height: '', left: ''}); dialog.css({overflow: '', width: (fullscreen ? 'auto' : ''), height: (fullscreen ? 'auto' : ''), 'margin-left': (fullscreen ? dialogMargin : '')}); dialog.toggleClass(toggleClass); if (fullscreen) { body.css({position: 'absolute', height: '', left: 0, right: 0, top: header.outerHeight(), bottom: footer.outerHeight(), 'max-height': 'inherit'}); } else { body.css({position: '', height: ''}); } if (options.fullscreen && options.back) { // restore non-fullscreen state dialog.restoreTab(); } if (options.complete) { options.complete(); } }); } }(jQuery)); /*** REMOTE PROCEDURE CALLS VIA JSON-RPC *************************************************/ var RPC = (new function($) { 'use strict'; // Properties this.rpcUrl; this.defaultFailureCallback; this.connectErrorMessage = 'Cannot establish connection'; this.call = function(method, params, completed_callback, failure_callback, timeout, custom_headers) { var _this = this; var request = JSON.stringify({nocache: new Date().getTime(), method: method, params: params}); var xhr = new XMLHttpRequest(); xhr.open('post', this.rpcUrl); if (timeout) { xhr.timeout = timeout; } for (var i = 0; i < (custom_headers ? custom_headers.length : 0); i++) { xhr.setRequestHeader(custom_headers[i].name, custom_headers[i].value); } xhr.onreadystatechange = function() { if (xhr.readyState === 4) { var res = 'Unknown error'; var result; if (xhr.status === 200) { if (xhr.responseText != '') { try { result = JSON.parse(xhr.responseText); } catch (e) { res = e; } if (result) { if (result.error == null) { res = result.result; completed_callback(res); return; } else { res = result.error.message; if (result.error.message != 'Access denied') { res = res + '

Request: ' + request; } } } } else { res = 'No response received.'; } } else if (xhr.status === 0) { res = _this.connectErrorMessage; } else { res = 'Invalid Status: ' + xhr.status; } if (failure_callback) { failure_callback(res, result); } else { _this.defaultFailureCallback(res, result); } } }; xhr.send(request); } }(jQuery)); nzbget-19.1/webui/history.js0000644000175000017500000005470513130203062015724 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2017 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, see . */ /* * In this module: * 1) History tab; * 2) Functions for html generation for history, also used from other modules (edit dialog); * 3) Popup menus in history list. */ /*** HISTORY TAB AND EDIT HISTORY DIALOG **********************************************/ var History = (new function($) { 'use strict'; // Controls var $HistoryTable; var $HistoryTabBadge; var $HistoryTabBadgeEmpty; var $HistoryRecordsPerPage; var $CategoryMenu; // State var history; var notification = null; var updateTabInfo; var curFilter = 'ALL'; var activeTab = false; var showDup = false; this.init = function(options) { updateTabInfo = options.updateTabInfo; $HistoryTable = $('#HistoryTable'); $HistoryTabBadge = $('#HistoryTabBadge'); $HistoryTabBadgeEmpty = $('#HistoryTabBadgeEmpty'); $HistoryRecordsPerPage = $('#HistoryRecordsPerPage'); $CategoryMenu = $('#HistoryCategoryMenu'); var recordsPerPage = UISettings.read('HistoryRecordsPerPage', 10); $HistoryRecordsPerPage.val(recordsPerPage); $('#HistoryTable_filter').val(''); $HistoryTable.fasttable( { filterInput: '#HistoryTable_filter', filterClearButton: '#HistoryTable_clearfilter', pagerContainer: '#HistoryTable_pager', infoContainer: '#HistoryTable_info', rowSelect: UISettings.rowSelect, pageSize: recordsPerPage, maxPages: UISettings.miniTheme ? 1 : 5, pageDots: !UISettings.miniTheme, shortcuts: true, fillFieldsCallback: fillFieldsCallback, filterCallback: filterCallback, renderCellCallback: renderCellCallback, updateInfoCallback: updateInfo }); $HistoryTable.on('click', 'a', editClick); $HistoryTable.on('click', 'td:nth-child(2).dropdown-cell > div', statusClick); $HistoryTable.on('click', 'td:nth-child(5).dropdown-cell > div:not(.dropdown-disabled)', categoryClick); $CategoryMenu.on('click', 'a', categoryMenuClick); HistoryActionsMenu.init(); } this.applyTheme = function() { $HistoryTable.fasttable('setPageSize', UISettings.read('HistoryRecordsPerPage', 10), UISettings.miniTheme ? 1 : 5, !UISettings.miniTheme); } this.show = function() { activeTab = true; this.redraw(); } this.hide = function() { activeTab = false; } this.update = function() { if (!history) { $('#HistoryTable_Category').css('width', DownloadsUI.calcCategoryColumnWidth()); initFilterButtons(); } RPC.call('history', [showDup], loaded); } function loaded(curHistory) { history = curHistory; prepare(); RPC.next(); } function prepare() { for (var j=0, jl=history.length; j < jl; j++) { var hist = history[j]; if (hist.Status === 'DELETED/MANUAL' || hist.Status === 'DELETED/GOOD' || hist.Status === 'DELETED/SUCCESS' || hist.Status === 'DELETED/COPY') { hist.FilterKind = 'DELETED'; } else if (hist.Status === 'DELETED/DUPE') { hist.FilterKind = 'DUPE'; } else if (hist.Status.substring(0, 7) === 'SUCCESS') { hist.FilterKind = 'SUCCESS'; } else { hist.FilterKind = 'FAILURE'; } } } var SEARCH_FIELDS = ['name', 'status', 'priority', 'category', 'age', 'size', 'time']; this.redraw = function() { var data = []; for (var i=0; i < history.length; i++) { var hist = history[i]; var kind = hist.Kind; hist.status = HistoryUI.buildStatusText(hist); hist.name = hist.Name; hist.size = kind === 'URL' ? '' : Util.formatSizeMB(hist.FileSizeMB); hist.sizemb = hist.FileSizeMB; hist.sizegb = hist.FileSizeMB / 1024; hist.time = Util.formatDateTime(hist.HistoryTime + UISettings.timeZoneCorrection*60*60); hist.category = kind !== 'DUP' ? hist.Category : ''; hist.dupe = DownloadsUI.buildDupeText(hist.DupeKey, hist.DupeScore, hist.DupeMode); var age_sec = kind === 'NZB' ? new Date().getTime() / 1000 - (hist.MinPostTime + UISettings.timeZoneCorrection*60*60) : 0; hist.age = kind === 'NZB' ? Util.formatAge(hist.MinPostTime + UISettings.timeZoneCorrection*60*60) : ''; hist.agem = Util.round0(age_sec / 60); hist.ageh = Util.round0(age_sec / (60*60)); hist.aged = Util.round0(age_sec / (60*60*24)); hist._search = SEARCH_FIELDS; var item = { id: hist.ID, data: hist, }; data.push(item); } $HistoryTable.fasttable('update', data); Util.show($HistoryTabBadge, history.length > 0); Util.show($HistoryTabBadgeEmpty, history.length === 0 && UISettings.miniTheme); } function fillFieldsCallback(item) { var hist = item.data; var status = HistoryUI.buildStatus(hist); var name = '' + Util.textToHtml(Util.formatNZBName(hist.Name)) + ''; name += DownloadsUI.buildEncryptedLabel(hist.Kind === 'NZB' ? hist.Parameters : []); var dupe = DownloadsUI.buildDupe(hist.DupeKey, hist.DupeScore, hist.DupeMode); var category = hist.Kind !== 'DUP' ? (hist.Category !== '' ? Util.textToHtml(hist.Category) : 'None') : ''; var backup = hist.Kind === 'NZB' ? DownloadsUI.buildBackupLabel(hist) : ''; if (hist.Kind === 'URL') { name += ' URL'; } else if (hist.Kind === 'DUP') { name += ' hidden'; } if (!UISettings.miniTheme) { status = '
' + status + '
'; category = ''; item.fields = ['
', status, item.data.time, name + dupe + backup, category, item.data.age, item.data.size]; } else { var info = '
' + name + '' + dupe + ' ' + status + backup + ' ' + item.data.time + ''; if (category) { info += ' ' + category + ''; } if (hist.Kind === 'NZB') { info += ' ' + item.data.size + ''; } item.fields = [info]; } } function renderCellCallback(cell, index, item) { if (index === 1 || index === 4) { cell.className = !UISettings.miniTheme ? 'dropdown-cell' : ''; } else if (index === 2) { cell.className = 'text-center' + (!UISettings.miniTheme ? ' dropafter-cell' : ''); } else if (index === 5) { cell.className = 'text-right' + (!UISettings.miniTheme ? ' dropafter-cell' : ''); } else if (index === 6) { cell.className = 'text-right'; } } this.recordsPerPageChange = function() { var val = $HistoryRecordsPerPage.val(); UISettings.write('HistoryRecordsPerPage', val); $HistoryTable.fasttable('setPageSize', val); } function updateInfo(stat) { updateTabInfo($HistoryTabBadge, stat); if (activeTab) { updateFilterButtons(); } } this.actionClick = function(action) { var checkedRows = $HistoryTable.fasttable('checkedRows'); var checkedCount = $HistoryTable.fasttable('checkedCount'); if (checkedCount === 0) { PopupNotification.show('#Notif_History_Select'); return; } var pageCheckedCount = $HistoryTable.fasttable('pageCheckedCount'); var checkedPercentage = Util.round0(checkedCount / history.length * 100); var hasNzb = false; var hasUrl = false; var hasDup = false; var hasFailed = false; for (var i = 0; i < history.length; i++) { var hist = history[i]; if (checkedRows[hist.ID]) { hasNzb |= hist.Kind === 'NZB'; hasUrl |= hist.Kind === 'URL'; hasDup |= hist.Kind === 'DUP'; hasFailed |= hist.ParStatus === 'FAILURE' || hist.UnpackStatus === 'FAILURE' || hist.DeleteStatus != 'NONE'; } } switch (action) { case 'DELETE': notification = '#Notif_History_Deleted'; HistoryUI.deleteConfirm(historyAction, hasNzb, hasDup, hasFailed, true, checkedCount, pageCheckedCount, checkedPercentage); break; case 'REPROCESS': if (hasUrl || hasDup) { PopupNotification.show('#Notif_History_CantReprocess'); return; } notification = '#Notif_History_Reprocess'; historyAction('HistoryProcess'); break; case 'REDOWNLOAD': if (hasDup) { PopupNotification.show('#Notif_History_CantRedownload'); return; } notification = '#Notif_History_Returned'; ConfirmDialog.showModal('HistoryEditRedownloadConfirmDialog', function () { historyAction('HistoryRedownload') }, function () { HistoryUI.confirmMulti(checkedCount > 1); }, checkedCount); break; case 'MARKSUCCESS': case 'MARKGOOD': case 'MARKBAD': if (hasUrl) { PopupNotification.show('#Notif_History_CantMark'); return; } notification = '#Notif_History_Marked'; ConfirmDialog.showModal(action === 'MARKSUCCESS' ? 'HistoryEditSuccessConfirmDialog' : action === 'MARKGOOD' ? 'HistoryEditGoodConfirmDialog' : 'HistoryEditBadConfirmDialog', function () // action { historyAction(action === 'MARKSUCCESS' ? 'HistoryMarkSuccess' : action === 'MARKGOOD' ? 'HistoryMarkGood' :'HistoryMarkBad'); }, function (_dialog) // init { HistoryUI.confirmMulti(checkedCount > 1); }, checkedCount ); break; } } function historyAction(command) { Refresher.pause(); var ids = buildContextIdList(); RPC.call('editqueue', [command, '', ids], function() { editCompleted(); }); } function editCompleted() { Refresher.update(); if (notification) { PopupNotification.show(notification); notification = null; } } function editClick(e) { e.preventDefault(); e.stopPropagation(); var histid = $(this).attr('data-nzbid'); var area = $(this).attr('data-area'); $(this).blur(); var hist = findHist(histid); if (hist == null) { return; } HistoryEditDialog.showModal(hist, area); } function findHist(nzbid) { for (var i=0; i' + statusText + ''; } this.deleteConfirm = function(actionCallback, hasNzb, hasDup, hasFailed, multi, selCount, pageSelCount, selPercentage) { var dupeCheck = Options.option('DupeCheck') === 'yes'; var dialog = null; function init(_dialog) { dialog = _dialog; HistoryUI.confirmMulti(multi); $('#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); 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'; if (selCount - pageSelCount > 0 && selCount >= 50) { PurgeHistoryDialog.showModal(function(){actionCallback(command);}, selCount, selPercentage); } else { actionCallback(command); } } ConfirmDialog.showModal('HistoryDeleteConfirmDialog', action, init, selCount); } this.confirmMulti = function(multi) { if (multi === undefined || !multi) { var html = $('#ConfirmDialog_Text').html(); html = html.replace(/records/g, 'record'); html = html.replace(/nzbs/g, 'nzb'); $('#ConfirmDialog_Text').html(html); } } }(jQuery)); /*** HISTORY ACTION MENU *************************************************************************/ var HistoryActionsMenu = (new function() { 'use strict' var $ActionsMenu; var curHist; var beforeCallback; var completedCallback; var editIds; this.init = function() { $ActionsMenu = $('#HistoryActionsMenu'); $('#HistoryActions_Delete').click(itemDelete); $('#HistoryActions_Return, #HistoryActions_ReturnURL').click(itemReturn); $('#HistoryActions_Reprocess').click(itemReprocess); $('#HistoryActions_Redownload').click(itemRedownload); $('#HistoryActions_RetryFailed').click(itemRetryFailed); $('#HistoryActions_MarkSuccess').click(itemSuccess); $('#HistoryActions_MarkGood').click(itemGood); $('#HistoryActions_MarkBad').click(itemBad); } this.showPopupMenu = function(hist, anchor, rect, before, completed) { curHist = hist; beforeCallback = before; completedCallback = completed; editIds = History.buildContextIdList(hist); // setup menu items Util.show('#HistoryActions_Return', hist.RemainingFileCount > 0); Util.show('#HistoryActions_ReturnURL', hist.Kind === 'URL'); Util.show('#HistoryActions_Redownload, #HistoryActions_Reprocess', hist.Kind === 'NZB'); Util.show('#HistoryActions_RetryFailed', hist.Kind === 'NZB' && hist.FailedArticles > 0 && hist.RetryData); var dupeCheck = Options.option('DupeCheck') === 'yes'; Util.show('#HistoryActions_MarkSuccess', dupeCheck && ((hist.Kind === 'NZB' && hist.MarkStatus !== 'SUCCESS') || (hist.Kind === 'DUP' && hist.DupStatus !== 'SUCCESS')) && hist.Status.substr(0, 7) !== 'SUCCESS'); Util.show('#HistoryActions_MarkGood', dupeCheck && ((hist.Kind === 'NZB' && hist.MarkStatus !== 'GOOD') || (hist.Kind === 'DUP' && hist.DupStatus !== 'GOOD'))); Util.show('#HistoryActions_MarkBad', dupeCheck && hist.Kind !== 'URL'); DownloadsUI.updateContextWarning($ActionsMenu, editIds); DownloadsUI.buildDNZBLinks(hist.Parameters ? hist.Parameters : [], 'HistoryActions_DNZB'); Frontend.showPopupMenu($ActionsMenu, anchor, rect); } function itemDelete(e) { e.preventDefault(); HistoryUI.deleteConfirm(doItemDelete, curHist.Kind === 'NZB', curHist.Kind === 'DUP', curHist.ParStatus === 'FAILURE' || curHist.UnpackStatus === 'FAILURE' || curHist.DeleteStatus != 'NONE', false); } function execAction(command, notification) { function performAction() { RPC.call('editqueue', [command, '', editIds], completedCallback); } var async = beforeCallback(notification, performAction); if (!async) { performAction(); } } function doItemDelete(command) { execAction(command, '#Notif_History_Deleted'); } function itemReturn(e) { e.preventDefault(); execAction('HistoryReturn', '#Notif_History_Returned'); } function itemRedownload(e) { e.preventDefault(); if (curHist.SuccessArticles > 0) { ConfirmDialog.showModal('HistoryEditRedownloadConfirmDialog', doItemRedownload, function () { HistoryUI.confirmMulti(false); }); } else { doItemRedownload(); } } function doItemRedownload() { execAction('HistoryRedownload', '#Notif_History_Returned'); } function itemReprocess(e) { e.preventDefault(); execAction('HistoryProcess', '#Notif_History_Reprocess'); } function itemRetryFailed(e) { e.preventDefault(); execAction('HistoryRetryFailed', '#Notif_History_RetryFailed'); } function itemSuccess(e) { e.preventDefault(); ConfirmDialog.showModal('HistoryEditSuccessConfirmDialog', doItemSuccess, function () { HistoryUI.confirmMulti(editIds.length > 1); }); } function doItemSuccess() { execAction('HistoryMarkSuccess', '#Notif_History_Marked'); } function itemGood(e) { e.preventDefault(); ConfirmDialog.showModal('HistoryEditGoodConfirmDialog', doItemGood, function () { HistoryUI.confirmMulti(editIds.length > 1); }); } function doItemGood() { execAction('HistoryMarkGood', '#Notif_History_Marked'); } function itemBad(e) { e.preventDefault(); ConfirmDialog.showModal('HistoryEditBadConfirmDialog', doItemBad, function () { HistoryUI.confirmMulti(editIds.length > 1); }); } function doItemBad() { execAction('HistoryMarkBad', '#Notif_History_Marked'); } }(jQuery)); /*** PURGE HISTORY DIALOG *****************************************************/ var PurgeHistoryDialog = (new function($) { 'use strict'; // Controls var $PurgeHistoryDialog; // State var actionCallback; this.init = function() { $PurgeHistoryDialog = $('#PurgeHistoryDialog'); } this.showModal = function(_actionCallback, count, percentage) { actionCallback = _actionCallback; $('#PurgeHistoryDialog_count,#PurgeHistoryDialog_count2').text(count); $('#PurgeHistoryDialog_percentage').text(percentage); Util.centerDialog($PurgeHistoryDialog, true); $PurgeHistoryDialog.modal({backdrop: 'static'}); } this.delete = function(event) { event.preventDefault(); // avoid scrolling $PurgeHistoryDialog.modal('hide'); actionCallback(); } }(jQuery)); nzbget-19.1/webui/edit.js0000644000175000017500000015341213130203062015143 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2017 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, see . */ /* * In this module: * 1) Download edit dialog; * 2) Download multi edit dialog (edit multiple items); * 3) Download merge dialog; * 4) Download split dialog; * 5) History edit dialog. */ /*** DOWNLOAD EDIT DIALOG ************************************************************/ var DownloadsEditDialog = (new function($) { 'use strict'; // Controls var $DownloadsEditDialog; var $DownloadsFileTable; var $DownloadsEdit_ParamData; var $ServStatsTable; // State var curGroup; var notification = null; var postParams = []; var lastPage; var lastFullscreen; var logFilled; var files; var refreshTimer = 0; var showing; var oldCategory; this.init = function() { $DownloadsEditDialog = $('#DownloadsEditDialog'); $DownloadsEdit_ParamData = $('#DownloadsEdit_ParamData'); $('#DownloadsEdit_Save').click(saveChanges); $('#DownloadsEdit_Actions').click(itemActions); $('#DownloadsEdit_Param, #DownloadsEdit_Log, #DownloadsEdit_File, #DownloadsEdit_Dupe').click(tabClick); $('#DownloadsEdit_Back').click(backClick); $('#DownloadsEdit_Category').change(categoryChange); LogTab.init('Downloads'); $DownloadsFileTable = $('#DownloadsEdit_FileTable'); $DownloadsFileTable.fasttable( { filterInput: '#DownloadsEdit_FileTable_filter', pagerContainer: '#DownloadsEdit_FileTable_pager', rowSelect: UISettings.rowSelect, pageSize: 10000, renderCellCallback: fileTableRenderCellCallback }); $ServStatsTable = $('#DownloadsEdit_ServStatsTable'); $ServStatsTable.fasttable( { filterInput: '#DownloadsEdit_ServStatsTable_filter', pagerContainer: '#DownloadsEdit_ServStatsTable_pager', pageSize: 100, maxPages: 3, renderCellCallback: EditUI.servStatsTableRenderCellCallback }); $DownloadsEditDialog.on('hidden', function() { // cleanup LogTab.reset('Downloads'); $DownloadsFileTable.fasttable('update', []); $DownloadsEdit_ParamData.empty(); clearTimeout(refreshTimer); // resume updates Refresher.resume(); }); TabDialog.extend($DownloadsEditDialog); if (UISettings.setFocus) { $DownloadsEditDialog.on('shown', function() { if ($('#DownloadsEdit_NZBName').is(":visible")) { $('#DownloadsEdit_NZBName').focus(); } }); } } this.showModal = function(nzbid, allGroups, area) { var group = null; // find Group object for (var i=0; i 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 += 'Total' + size + ''; 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 += '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_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'); DownloadsUI.fillPriorityCombo(v); v.val(group.MaxPriority); if (v.val() != group.MaxPriority) { v.append(''); } v.attr('disabled', 'disabled'); // Category v = $('#DownloadsEdit_Category'); DownloadsUI.fillCategoryCombo(v); v.val(group.Category); if (v.val() != group.Category) { v.append($('').text(group.Category)); } // duplicate settings $('#DownloadsEdit_DupeKey').val(group.DupeKey); $('#DownloadsEdit_DupeScore').val(group.DupeScore); $('#DownloadsEdit_DupeMode').val(group.DupeMode); $DownloadsFileTable.fasttable('update', []); var postParamConfig = ParamTab.createPostParamConfig(); Util.show('#DownloadsEdit_NZBNameReadonly', group.postprocess); Util.show('#DownloadsEdit_Save', !group.postprocess); 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.MessageCount > 0; Util.show('#DownloadsEdit_Param', postParam); Util.show('#DownloadsEdit_Log', postLog); if (group.postprocess) { $('#DownloadsEdit_NZBName').attr('disabled', 'disabled'); $('#DownloadsEdit_Priority').attr('disabled', 'disabled'); $('#DownloadsEdit_Category').attr('disabled', 'disabled'); $('#DownloadsEdit_Close').addClass('btn-primary'); $('#DownloadsEdit_Close').text('Close'); } else { $('#DownloadsEdit_NZBName').removeAttr('disabled'); $('#DownloadsEdit_Priority').removeAttr('disabled'); $('#DownloadsEdit_Category').removeAttr('disabled'); $('#DownloadsEdit_Close').removeClass('btn-primary'); $('#DownloadsEdit_Close').text('Cancel'); } if (postParam) { postParams = ParamTab.buildPostParamTab($DownloadsEdit_ParamData, postParamConfig, curGroup.Parameters); } 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(''); $DownloadsFileTable.fasttable('setCurPage', 1); $DownloadsFileTable.fasttable('applyFilter', ''); LogTab.reset('Downloads'); files = null; logFilled = false; notification = null; oldCategory = curGroup.Category; if (area === 'backup') { showing = true; $('#DownloadsEdit_ServStats').trigger('click'); } showing = false; $DownloadsEditDialog.modal({backdrop: 'static'}); } function completed() { $DownloadsEditDialog.modal('hide'); Refresher.update(); if (notification) { PopupNotification.show(notification); notification = null; } } function tabClick(e) { e.preventDefault(); $('#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()); } if (UISettings.miniTheme && !logFilled) { $('#DownloadsEdit_LogBlock').css('min-height', $DownloadsEditDialog.height()); } $DownloadsEditDialog.switchTab($('#DownloadsEdit_GeneralTab'), lastPage, e.shiftKey || !UISettings.slideAnimation || showing ? 0 : 500, {fullscreen: lastFullscreen, mini: UISettings.miniTheme, complete: function() { if (!UISettings.miniTheme) { $('#DownloadsEdit_FileBlock').css('top', $('#DownloadsEdit_FileBlock').position().top); $('#DownloadsEdit_FileBlock').addClass('modal-inner-scroll'); } else { $('#DownloadsEdit_FileBlock').css('min-height', ''); $('#DownloadsEdit_LogBlock').css('min-height', ''); } }}); if (tab === '#DownloadsEdit_LogTab' && !logFilled && (curGroup.postprocess || curGroup.MessageCount > 0)) { LogTab.fill('Downloads', curGroup); logFilled = true; } if (tab === '#DownloadsEdit_FileTab' && files === null) { fillFiles(); } if (tab === '#DownloadsEdit_ServStatsTab') { scheduleRefresh(); } } function backClick(e) { e.preventDefault(); $('#DownloadsEdit_Back').fadeOut(500, function() { $('#DownloadsEdit_BackSpace').show(); }); $('#DownloadsEdit_FileBlock').removeClass('modal-inner-scroll'); $('#DownloadsEdit_FileBlock').css('top', ''); $DownloadsEditDialog.switchTab(lastPage, $('#DownloadsEdit_GeneralTab'), e.shiftKey || !UISettings.slideAnimation ? 0 : 500, {fullscreen: lastFullscreen, mini: UISettings.miniTheme, back: true}); clearTimeout(refreshTimer); } function disableAllButtons() { $('#DownloadsEditDialog .modal-footer .btn').attr('disabled', 'disabled'); setTimeout(function() { $('#DownloadsEdit_Transmit').show(); }, 500); } function enableAllButtons() { $('#DownloadsEditDialog .modal-footer .btn').removeAttr('disabled'); $('#DownloadsEdit_Transmit').hide(); } function saveChanges(e) { e.preventDefault(); disableAllButtons(); notification = null; saveName(); } function saveName() { var name = $('#DownloadsEdit_NZBName').val(); name !== curGroup.NZBName && !curGroup.postprocess ? RPC.call('editqueue', ['GroupSetName', name, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; savePriority(); }) :savePriority(); } function savePriority() { var priority = parseInt($('#DownloadsEdit_Priority').val()); priority !== curGroup.MaxPriority ? RPC.call('editqueue', ['GroupSetPriority', '' + priority, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; saveCategory(); }) : saveCategory(); } function saveCategory() { var category = $('#DownloadsEdit_Category').val(); category !== curGroup.Category ? RPC.call('editqueue', ['GroupSetCategory', category, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; saveDupeKey(); }) : saveDupeKey(); } function itemActions(e) { e.preventDefault(); e.stopPropagation(); var elem = $('#DownloadsEdit_Actions').parent(); DownloadsActionsMenu.showPopupMenu(curGroup, 'top', { left: elem.offset().left, top: elem.offset().top - 1, width: elem.width(), height: elem.height() + 2 }, function(_notification) { disableAllButtons(); notification = _notification; }, 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); } function saveNextParam(paramList) { if (paramList.length > 0) { RPC.call('editqueue', ['GroupSetParameter', paramList[0], [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; paramList.shift(); saveNextParam(paramList); }) } else { saveFiles(); } } /*** TAB: DUPLICATE SETTINGS **************************************************/ function saveDupeKey() { var value = $('#DownloadsEdit_DupeKey').val(); value !== curGroup.DupeKey ? RPC.call('editqueue', ['GroupSetDupeKey', value, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; saveDupeScore(); }) :saveDupeScore(); } function saveDupeScore() { var value = $('#DownloadsEdit_DupeScore').val(); value != curGroup.DupeScore ? RPC.call('editqueue', ['GroupSetDupeScore', value, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; saveDupeMode(); }) :saveDupeMode(); } function saveDupeMode() { var value = $('#DownloadsEdit_DupeMode').val(); value !== curGroup.DupeMode ? RPC.call('editqueue', ['GroupSetDupeMode', value, [curGroup.NZBID]], function() { notification = '#Notif_Downloads_Saved'; saveParam(); }) :saveParam(); } /*** TAB: FILES *************************************************************************/ function fillFiles() { $('.loading-block', $DownloadsEditDialog).show(); RPC.call('listfiles', [0, 0, curGroup.NZBID], filesLoaded); } function filesLoaded(fileArr) { $('.loading-block', $DownloadsEditDialog).hide(); files = fileArr; var data = []; for (var i=0; i < files.length; i++) { var file = files[i]; if (!file.status) { file.status = file.Paused ? (file.ActiveDownloads > 0 ? 'pausing' : 'paused') : (file.ActiveDownloads > 0 ? 'downloading' : 'queued'); } var FileSizeMB = (file.FileSizeHi * 4096) + (file.FileSizeLo / 1024 / 1024); var RemainingSizeMB = (file.RemainingSizeHi * 4096) + (file.RemainingSizeLo / 1024 / 1024); var age = Util.formatAge(file.PostTime + UISettings.timeZoneCorrection*60*60); var size = Util.formatSizeMB(FileSizeMB, file.FileSizeLo); if (FileSizeMB !== RemainingSizeMB || file.FileSizeLo !== file.RemainingSizeLo) { size = '(' + Util.round0((file.FileSizeHi > 0 ? RemainingSizeMB / FileSizeMB : file.RemainingSizeLo / file.FileSizeLo) * 100) + '%) ' + size; } var status; switch (file.status) { case 'downloading': case 'pausing': status = '' + file.status + ''; break; case 'paused': status = 'paused'; break; case 'queued': status = 'queued'; break; case 'deleted': status = 'deleted'; break; default: status = 'internal error(' + file.status + ')'; } var name = Util.textToHtml(file.Filename); var fields; if (!UISettings.miniTheme) { var info = name; fields = ['
', status, info, age, size]; } else { var info = '
' + name + '' + ' ' + (file.status === 'queued' ? '' : status); fields = [info]; } var item = { id: file.ID, file: file, fields: fields, data: { status: file.status, name: file.Filename, age: age, size: size, _search: true } }; data.push(item); } $DownloadsFileTable.fasttable('update', data); } function fileTableRenderCellCallback(cell, index, item) { if (index > 2) { cell.className = 'text-right'; } } this.editActionClick = function(action) { if (files.length == 0) { return; } var checkedRows = $DownloadsFileTable.fasttable('checkedRows'); var checkedCount = $DownloadsFileTable.fasttable('checkedCount'); if (checkedCount === 0) { PopupNotification.show('#Notif_Edit_Select'); return; } for (var i = 0; i < files.length; i++) { var file = files[i]; file.moved = false; } var editIDList = []; var splitError = false; for (var i = 0; i < files.length; i++) { var n = i; if (action === 'down' || action === 'top') { // iterate backwards in the file list n = files.length-1-i; } var file = files[n]; if (checkedRows[file.ID]) { editIDList.push(file.ID); switch (action) { case 'pause': file.status = 'paused'; file.editAction = action; break; case 'resume': file.status = 'queued'; file.editAction = action; break; case 'delete': file.status = 'deleted'; file.editAction = action; break; case 'top': if (!file.moved) { files.splice(n, 1); files.unshift(file); file.moved = true; file.editMoved = true; i--; } break; case 'up': if (!file.moved && i > 0) { files.splice(i, 1); files.splice(i-1, 0, file); file.moved = true; file.editMoved = true; } break; case 'down': if (!file.moved && i > 0) { files.splice(n, 1); files.splice(n+1, 0, file); file.moved = true; file.editMoved = true; } break; case 'bottom': if (!file.moved) { files.splice(i, 1); files.push(file); file.moved = true; file.editMoved = true; i--; } break; case 'split': if (file.ActiveDownloads > 0 || file.Progress > 0) { splitError = true; } break; } } } if (action === 'split') { if (splitError) { PopupNotification.show('#Notif_Downloads_SplitNotPossible'); } else { DownloadsSplitDialog.showModal(curGroup, editIDList); } } filesLoaded(files); } function saveFilesActions(actions, commands) { if (actions.length === 0 || !files || files.length === 0) { saveFileOrder(); return; } var action = actions.shift(); var command = commands.shift(); var IDs = []; for (var i = 0; i < files.length; i++) { var file = files[i]; if (file.editAction === action) { IDs.push(file.ID); } } if (IDs.length > 0) { RPC.call('editqueue', [command, '', IDs], function() { notification = '#Notif_Downloads_Saved'; saveFilesActions(actions, commands); }) } else { saveFilesActions(actions, commands); } } function saveFiles() { saveFilesActions(['pause', 'resume', 'delete'], ['FilePause', 'FileResume', 'FileDelete']); } function saveFileOrder() { if (!files || files.length === 0) { completed(); return; } var IDs = []; var hasMovedFiles = false; for (var i = 0; i < files.length; i++) { var file = files[i]; IDs.push(file.ID); hasMovedFiles |= file.editMoved; } if (hasMovedFiles) { RPC.call('editqueue', ['FileReorder', '', IDs], function() { notification = '#Notif_Downloads_Saved'; completed(); }) } else { 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' /*** 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%'; } success = '' + success + ''; failures = '' + failures + ''; break; } } var fields = [server.ID + '. ' + name, articles, artquota, success, failures]; var item = { id: server.ID, fields: fields, }; data.push(item); } table.fasttable('update', data); } this.servStatsTableRenderCellCallback = function (cell, index, item) { if (index > 0) { cell.className = 'text-right'; } } }(jQuery)); /*** PARAM TAB FOR EDIT DIALOGS ************************************************************/ var ParamTab = (new function($) { 'use strict' this.buildPostParamTab = function(configData, postParamConfig, parameters) { var postParams = $.extend(true, [], postParamConfig); Options.mergeValues(postParams, parameters); var content = Config.buildOptionsContent(postParams[0]); configData.empty(); configData.append(content); configData.addClass('retain-margin'); var lastClass = ''; var lastDiv = null; for (var i=0; i < configData.children().length; i++) { var div = $(configData.children()[i]); var divClass = div.attr('class'); if (divClass != lastClass && lastClass != '') { lastDiv.addClass('wants-divider'); } lastDiv = div; lastClass = divClass; } return postParams; } this.createPostParamConfig = function() { var postParamConfig = Options.postParamConfig; defineBuiltinParams(postParamConfig); return postParamConfig; } function defineBuiltinParams(postParamConfig) { 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 = []; 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) { var oldValue = option.value; var newValue = Config.getOptionValue(option); if (oldValue != newValue && !((oldValue === null || oldValue === '') && newValue === option.defvalue)) { var opt = option.name + '=' + newValue; request.push(opt); } } } } 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) + '.Extensions')); if (scriptList.length === 0) { scriptList = Util.parseCommaList(Options.option('Extensions')); } if (Options.option('Category' + (i + 1) + '.Unpack') === 'yes') { scriptList.push('*Unpack'); } return scriptList; } } // empty category or category not found scriptList = Util.parseCommaList(Options.option('Extensions')); 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) === ':') { 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)); /*** LOG TAB FOR EDIT DIALOGS ************************************************************/ var LogTab = (new function($) { 'use strict' var curLog; var curItem; this.init = function(name) { var recordsPerPage = UISettings.read('ItemLogRecordsPerPage', 10); $('#' + name + 'LogRecordsPerPage').val(recordsPerPage); var $LogTable = $('#' + name + 'Edit_LogTable'); $LogTable.fasttable( { filterInput: '#' + name + 'Edit_LogTable_filter', pagerContainer: '#' + name + 'Edit_LogTable_pager', pageSize: recordsPerPage, maxPages: 3, renderCellCallback: logTableRenderCellCallback }); } this.reset = function(name) { var $LogTable = $('#' + name + 'Edit_LogTable'); $LogTable.fasttable('update', []); $LogTable.fasttable('setCurPage', 1); $LogTable.fasttable('applyFilter', ''); $('#' + name + 'Edit_LogTable_filter').val(''); } this.fill = function(name, item) { curItem = item; function logLoaded(log) { curLog = log; $('#' + name + 'EditDialog .loading-block').hide(); var $LogTable = $('#' + name + 'Edit_LogTable'); var data = []; for (var i=0; i < log.length; i++) { var message = log[i]; var kind; switch (message.Kind) { case 'INFO': kind = 'info'; break; case 'DETAIL': kind = 'detail'; break; case 'WARNING': kind = 'warning'; break; case 'ERROR': kind = 'error'; break; case 'DEBUG': kind = 'debug'; break; } var text = Util.textToHtml(message.Text); var time = Util.formatDateTime(message.Time + UISettings.timeZoneCorrection*60*60); var fields; if (!UISettings.miniTheme) { fields = [kind, time, text]; } else { var info = kind + ' ' + time + ' ' + text; fields = [info]; } var item = { id: message, fields: fields, data: { kind: message.Kind, time: time, text: message.Text, _search: true } }; data.unshift(item); } $LogTable.fasttable('update', data); } var recordsPerPage = UISettings.read('ItemLogRecordsPerPage', 10); $('#' + name + 'LogRecordsPerPage').val(recordsPerPage); $('#' + name + 'EditDialog .loading-block').show(); RPC.call('loadlog', [item.NZBID, 0, 10000], logLoaded); } function logTableRenderCellCallback(cell, index, item) { if (index === 0) { cell.width = '65px'; } } this.recordsPerPageChange = function(name) { var val = $('#' + name + 'LogRecordsPerPage').val(); UISettings.write('ItemLogRecordsPerPage', val); var $LogTable = $('#' + name + 'Edit_LogTable'); $LogTable.fasttable('setPageSize', val); } this.export = function() { var filename = curItem.NZBName + '.log'; var logstr = ''; for (var i=0; i < curLog.length; i++) { var message = curLog[i]; var time = Util.formatDateTime(message.Time + UISettings.timeZoneCorrection*60*60); logstr += time + '\t' + message.Kind + '\t' + message.Text + '\n'; } if (!Util.saveToLocalFile(logstr, "text/plain;charset=utf-8", filename)) { var queueDir = Options.option('QueueDir'); var pathSeparator = queueDir.indexOf('\\') > -1 ? '\\' : '/'; alert('Unfortunately your browser doesn\'t support access to local file system.\n\n' + 'The log of this nzb can be found in file "' + queueDir + pathSeparator + 'n' + curItem.NZBID + '.log"'); } } }(jQuery)); /*** DOWNLOAD MULTI EDIT DIALOG ************************************************************/ var DownloadsMultiDialog = (new function($) { 'use strict' // 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 () { Refresher.resume(); }); if (UISettings.setFocus) { $DownloadsMultiDialog.on('shown', function () { if ($('#DownloadsMulti_Priority').is(":visible")) { $('#DownloadsMulti_Priority').focus(); } }); } } this.showModal = function(nzbIdList, allGroups) { var groups = []; multiIDList = []; for (var i=0; i -1) { groups.push(gr); multiIDList.push(gr.NZBID); } } if (groups.length == 0) { return; } Refresher.pause(); var FileSizeMB = 0, FileSizeLo = 0; var RemainingSizeMB = 0, RemainingSizeLo = 0; var PausedSizeMB = 0, PausedSizeLo = 0; var FileCount = 0, RemainingFileCount = 0, RemainingParCount = 0; var paused = true; var Priority = groups[0].MaxPriority; var PriorityDiff = false; var Category = groups[0].Category; var CategoryDiff = false; for (var i=0; i 0 ? Util.formatTimeHMS((RemainingSizeMB-PausedSizeMB)*1024/(Status.status.DownloadRate/1024)) : ''); var table = ''; table += 'Total' + size + ''; table += 'Paused' + unpausedSize + ''; table += 'Unpaused' + remaining + ''; table += 'Estimated time' + estimated + ''; table += 'Files (total/remaining/pars)' + FileCount + ' / ' + RemainingFileCount + ' / ' + RemainingParCount + ''; $('#DownloadsMulti_Statistics').html(table); $('#DownloadsMulti_Title').text('Multiple records (' + groups.length + ')'); // Priority var v = $('#DownloadsMulti_Priority'); DownloadsUI.fillPriorityCombo(v); v.val(Priority); if (v.val() != Priority) { v.append(''); v.val(Priority); } if (PriorityDiff) { v.append(''); } oldPriority = v.val(); $('#DownloadsMulti_Priority').removeAttr('disabled'); // Category var v = $('#DownloadsMulti_Category'); DownloadsUI.fillCategoryCombo(v); v.val(Category); if (v.val() != Category) { v.append($('').text(Category)); v.val(Category); } if (CategoryDiff) { v.append(''); } oldCategory = v.val(); enableAllButtons(); $('#DownloadsMulti_GeneralTabLink').tab('show'); notification = null; $DownloadsMultiDialog.modal({backdrop: 'static'}); } function enableAllButtons() { $('#DownloadsMulti .modal-footer .btn').removeAttr('disabled'); $('#DownloadsMulti_Transmit').hide(); } function disableAllButtons() { $('#DownloadsMulti .modal-footer .btn').attr('disabled', 'disabled'); setTimeout(function() { $('#DownloadsMulti_Transmit').show(); }, 500); } function saveChanges(e) { e.preventDefault(); disableAllButtons(); savePriority(); } function savePriority() { var priority = $('#DownloadsMulti_Priority').val(); (priority !== oldPriority && priority !== '') ? RPC.call('editqueue', ['GroupSetPriority', priority, multiIDList], function() { notification = '#Notif_Downloads_Saved'; saveCategory(); }) : saveCategory(); } function saveCategory() { var category = $('#DownloadsMulti_Category').val(); (category !== oldCategory && category !== '') ? RPC.call('editqueue', ['GroupApplyCategory', category, multiIDList], function() { notification = '#Notif_Downloads_Saved'; completed(); }) : completed(); } function completed() { $DownloadsMultiDialog.modal('hide'); Refresher.update(); if (notification) { PopupNotification.show(notification); } } }(jQuery)); /*** DOWNLOAD MERGE DIALOG ************************************************************/ var DownloadsMergeDialog = (new function($) { 'use strict' // Controls var $DownloadsMergeDialog; // State var mergeEditIDList; this.init = function() { $DownloadsMergeDialog = $('#DownloadsMergeDialog'); $('#DownloadsMerge_Merge').click(merge); $DownloadsMergeDialog.on('hidden', function () { Refresher.resume(); }); if (UISettings.setFocus) { $DownloadsMergeDialog.on('shown', function () { $('#DownloadsMerge_Merge').focus(); }); } } this.showModal = function(nzbIdList, allGroups) { Refresher.pause(); mergeEditIDList = []; $('#DownloadsMerge_Files').empty(); for (var i = 0; i < allGroups.length; i++) { var group = allGroups[i]; if (nzbIdList.indexOf(group.NZBID) > -1) { mergeEditIDList.push(group.NZBID); var html = '
' + Util.formatNZBName(group.NZBName) + '
'; $('#DownloadsMerge_Files').append(html); } } $DownloadsMergeDialog.modal({backdrop: 'static'}); } function merge() { RPC.call('editqueue', ['GroupMerge', '', mergeEditIDList], completed); } function completed() { $DownloadsMergeDialog.modal('hide'); Refresher.update(); PopupNotification.show('#Notif_Downloads_Merged'); } }(jQuery)); /*** DOWNLOAD SPLIT DIALOG ************************************************************/ var DownloadsSplitDialog = (new function($) { 'use strict' // Controls var $DownloadsSplitDialog; // State var splitEditIDList; this.init = function() { $DownloadsSplitDialog = $('#DownloadsSplitDialog'); $('#DownloadsSplit_Split').click(split); $DownloadsSplitDialog.on('hidden', function () { Refresher.resume(); }); if (UISettings.setFocus) { $DownloadsSplitDialog.on('shown', function () { $('#DownloadsSplit_Merge').focus(); }); } } this.showModal = function(group, editIDList) { Refresher.pause(); splitEditIDList = editIDList; var groupName = group.NZBName + ' (' + editIDList[0] + (editIDList.length > 1 ? '-' + editIDList[editIDList.length-1] : '') + ')'; $('#DownloadsSplit_NZBName').attr('value', groupName); $DownloadsSplitDialog.modal({backdrop: 'static'}); } function split() { var groupName = $('#DownloadsSplit_NZBName').val(); RPC.call('editqueue', ['FileSplit', groupName, splitEditIDList], completed); } function completed(result) { $('#DownloadsEditDialog').modal('hide'); $DownloadsSplitDialog.modal('hide'); Refresher.update(); PopupNotification.show(result ? '#Notif_Downloads_Splitted' : '#Notif_Downloads_SplitError'); } }(jQuery)); /*** EDIT HISTORY DIALOG *************************************************************************/ var HistoryEditDialog = (new function() { 'use strict' // Controls var $HistoryEditDialog; var $HistoryEdit_ParamData; var $ServStatsTable; // State var curHist; var notification = null; var postParams = []; var lastPage; var lastFullscreen; var saveCompleted; var logFilled; var showing; this.init = function() { $HistoryEditDialog = $('#HistoryEditDialog'); $HistoryEdit_ParamData = $('#HistoryEdit_ParamData'); $('#HistoryEdit_Save').click(saveChanges); $('#HistoryEdit_Actions').click(itemActions); $('#HistoryEdit_Param, #HistoryEdit_Dupe, #HistoryEdit_Log').click(tabClick); $('#HistoryEdit_Back').click(backClick); LogTab.init('History'); $ServStatsTable = $('#HistoryEdit_ServStatsTable'); $ServStatsTable.fasttable( { filterInput: '#HistoryEdit_ServStatsTable_filter', pagerContainer: '#HistoryEdit_ServStatsTable_pager', pageSize: 100, maxPages: 3, renderCellCallback: EditUI.servStatsTableRenderCellCallback }); $HistoryEditDialog.on('hidden', function () { $HistoryEdit_ParamData.empty(); LogTab.reset('History'); // resume updates Refresher.resume(); }); TabDialog.extend($HistoryEditDialog); } this.showModal = function(hist, area) { Refresher.pause(); curHist = hist; var status = ''; if (hist.Kind === 'NZB') { if (hist.DeleteStatus === '' || hist.DeleteStatus === 'HEALTH') { status = 'health: ' + Math.floor(hist.Health / 10) + '%'; } if (hist.MarkStatus !== 'NONE') { status += ' ' + buildStatus(hist.MarkStatus, 'Mark: '); } else if (hist.DeleteStatus === 'NONE') { var exParStatus = hist.ExParStatus === 'RECIPIENT' ? ' ' + '' + buildStatus(hist.ExParStatus, 'ExPar: ') + '' : hist.ExParStatus === 'DONOR' ? ' ' + '' + buildStatus(hist.ExParStatus, 'ExPar: ') + '' : ''; status += ' ' + buildStatus(hist.ParStatus, 'Par: ') + exParStatus + ' ' + (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; i' + (hist.Kind === 'DUP' ? 'hidden' : hist.Kind) + ''); } $('#HistoryEdit_NZBName').val(hist.Name); if (hist.Kind !== 'DUP') { // Category var v = $('#HistoryEdit_Category'); DownloadsUI.fillCategoryCombo(v); v.val(hist.Category); if (v.val() != hist.Category) { v.append($('').text(hist.Category)); } } if (hist.Kind === 'NZB') { $('#HistoryEdit_Path').val(hist.FinalDir !== '' ? hist.FinalDir : hist.DestDir); 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(); } $('#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_PathGroup, #HistoryEdit_StatisticsGroup', 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_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); if (postParam) { postParams = ParamTab.buildPostParamTab($HistoryEdit_ParamData, postParamConfig, curHist.Parameters); } var postLog = hist.MessageCount > 0; Util.show('#HistoryEdit_Log', postLog); enableAllButtons(); $('#HistoryEdit_GeneralTab').show(); $('#HistoryEdit_ParamTab').hide(); $('#HistoryEdit_ServStatsTab').hide(); $('#HistoryEdit_TimeStatsTab').hide(); $('#HistoryEdit_DupeTab').hide(); $('#HistoryEdit_LogTab').hide(); $('#HistoryEdit_Back').hide(); $('#HistoryEdit_BackSpace').show(); $HistoryEditDialog.restoreTab(); LogTab.reset('History'); logFilled = false; notification = null; if (area === 'backup') { showing = true; $('#HistoryEdit_ServStats').trigger('click'); } showing = false; $HistoryEditDialog.modal({backdrop: 'static'}); } function buildStatus(status, prefix) { switch (status) { case 'SUCCESS': case 'GOOD': case 'RECIPIENT': case 'DONOR': 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': case 'DELETED-MANUAL': case 'DELETED-COPY': case 'DELETED-GOOD': case 'DELETED-SUCCESS': return '' + prefix + status.substr(8).toLowerCase() + ''; case 'DELETED-HEALTH': return '' + prefix + 'health'; case 'DELETED-BAD': return '' + prefix + 'bad'; case 'DELETED-SCAN': return '' + prefix + 'scan'; 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) + ''; table += hist.ExtraParBlocks > 0 ? 'Received extra par-blocks' + hist.ExtraParBlocks + '' : hist.ExtraParBlocks < 0 ? 'Donated par-blocks' + - hist.ExtraParBlocks + '' : ''; $('#HistoryEdit_TimeStatsTable tbody').html(table); } function tabClick(e) { e.preventDefault(); $('#HistoryEdit_Back').fadeIn(showing ? 0 : 500); $('#HistoryEdit_BackSpace').hide(); 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 || showing ? 0 : 500, {fullscreen: lastFullscreen, mini: UISettings.miniTheme}); if (tab === '#HistoryEdit_LogTab' && !logFilled && curHist.MessageCount > 0) { LogTab.fill('History', curHist); logFilled = true; } } function backClick(e) { e.preventDefault(); $('#HistoryEdit_Back').fadeOut(500, function() { $('#HistoryEdit_BackSpace').show(); }); $HistoryEditDialog.switchTab(lastPage, $('#HistoryEdit_GeneralTab'), e.shiftKey || !UISettings.slideAnimation ? 0 : 500, {fullscreen: lastFullscreen, mini: UISettings.miniTheme, back: true}); } function disableAllButtons() { $('#HistoryEditDialog .modal-footer .btn').attr('disabled', 'disabled'); setTimeout(function() { $('#HistoryEdit_Transmit').show(); }, 500); } function enableAllButtons() { $('#HistoryEditDialog .modal-footer .btn').removeAttr('disabled'); $('#HistoryEdit_Transmit').hide(); } function completed() { $HistoryEditDialog.modal('hide'); Refresher.update(); if (notification) { PopupNotification.show(notification); notification = null; } } function saveChanges(e) { e.preventDefault(); disableAllButtons(); notification = null; saveCompleted = completed; saveName(); } function saveName() { var name = $('#HistoryEdit_NZBName').val(); name !== curHist.Name && !curHist.postprocess ? RPC.call('editqueue', ['HistorySetName', name, [curHist.ID]], function() { notification = '#Notif_History_Saved'; saveCategory(); }) :saveCategory(); } function saveCategory() { var category = $('#HistoryEdit_Category').val(); category !== curHist.Category && curHist.Kind !== 'DUP' ? RPC.call('editqueue', ['HistorySetCategory', category, [curHist.ID]], function() { notification = '#Notif_History_Saved'; saveDupeKey(); }) : saveDupeKey(); } function itemActions(e) { e.preventDefault(); e.stopPropagation(); var elem = $('#HistoryEdit_Actions').parent(); HistoryActionsMenu.showPopupMenu(curHist, 'top', { left: elem.offset().left, top: elem.offset().top - 1, width: elem.width(), height: elem.height() + 2 }, function(_notification, actionCallback) { disableAllButtons(); notification = _notification; saveCompleted = actionCallback; saveName(); return true; // async }, completed); } /*** TAB: POST-PROCESSING PARAMETERS **************************************************/ function saveParam() { if (curHist.Kind !== 'NZB') { saveCompleted(); return; } var paramList = ParamTab.prepareParamRequest(postParams); saveNextParam(paramList); } function saveNextParam(paramList) { if (paramList.length > 0) { RPC.call('editqueue', ['HistorySetParameter', paramList[0], [curHist.ID]], function() { notification = '#Notif_History_Saved'; paramList.shift(); saveNextParam(paramList); }) } else { saveCompleted(); } } /*** TAB: DUPLICATE SETTINGS **************************************************/ function saveDupeKey() { var value = $('#HistoryEdit_DupeKey').val(); value !== curHist.DupeKey ? RPC.call('editqueue', ['HistorySetDupeKey', value, [curHist.ID]], function() { notification = '#Notif_History_Saved'; saveDupeScore(); }) :saveDupeScore(); } function saveDupeScore() { var value = $('#HistoryEdit_DupeScore').val(); value != curHist.DupeScore ? RPC.call('editqueue', ['HistorySetDupeScore', value, [curHist.ID]], function() { notification = '#Notif_History_Saved'; saveDupeMode(); }) :saveDupeMode(); } function saveDupeMode() { var value = $('#HistoryEdit_DupeMode').val(); value !== curHist.DupeMode ? RPC.call('editqueue', ['HistorySetDupeMode', 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', value ? "YES" : "NO", [curHist.ID]], function() { notification = '#Notif_History_Saved'; saveParam(); }) :saveParam(); } }(jQuery)); nzbget-19.1/webui/lib/0000755000175000017500000000000013130203062014420 5ustar andreasandreasnzbget-19.1/webui/lib/jquery.js0000644000175000017500000075572113130203062016316 0ustar andreasandreas/*! * jQuery JavaScript Library v1.7.2 * http://jquery.com/ * * Copyright 2011, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Includes Sizzle.js * http://sizzlejs.com/ * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * * Date: Wed Mar 21 12:46:34 2012 -0700 */ (function( window, undefined ) { // Use the correct document accordingly with window argument (sandbox) var document = window.document, navigator = window.navigator, location = window.location; var jQuery = (function() { // Define a local copy of jQuery var jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' return new jQuery.fn.init( selector, context, rootjQuery ); }, // Map over jQuery in case of overwrite _jQuery = window.jQuery, // Map over the $ in case of overwrite _$ = window.$, // A central reference to the root jQuery(document) rootjQuery, // A simple way to check for HTML strings or ID strings // Prioritize #id over to avoid XSS via location.hash (#9521) quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, // Check if a string has a non-whitespace character in it rnotwhite = /\S/, // Used for trimming whitespace trimLeft = /^\s+/, trimRight = /\s+$/, // Match a standalone tag rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, // JSON RegExp rvalidchars = /^[\],:{}\s]*$/, rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, // Useragent RegExp rwebkit = /(webkit)[ \/]([\w.]+)/, ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, rmsie = /(msie) ([\w.]+)/, rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, // Matches dashed string for camelizing rdashAlpha = /-([a-z]|[0-9])/ig, rmsPrefix = /^-ms-/, // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return ( letter + "" ).toUpperCase(); }, // Keep a UserAgent string for use with jQuery.browser userAgent = navigator.userAgent, // For matching the engine and version of the browser browserMatch, // The deferred used on DOM ready readyList, // The ready event handler DOMContentLoaded, // Save a reference to some core methods toString = Object.prototype.toString, hasOwn = Object.prototype.hasOwnProperty, push = Array.prototype.push, slice = Array.prototype.slice, trim = String.prototype.trim, indexOf = Array.prototype.indexOf, // [[Class]] -> type pairs class2type = {}; jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function( selector, context, rootjQuery ) { var match, elem, ret, doc; // Handle $(""), $(null), or $(undefined) if ( !selector ) { return this; } // Handle $(DOMElement) if ( selector.nodeType ) { this.context = this[0] = selector; this.length = 1; return this; } // The body element only exists once, optimize finding it if ( selector === "body" && !context && document.body ) { this.context = document; this[0] = document.body; this.selector = selector; this.length = 1; return this; } // Handle HTML strings if ( typeof selector === "string" ) { // Are we dealing with HTML string or an ID? if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; } else { match = quickExpr.exec( selector ); } // Verify a match, and that no context was specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) if ( match[1] ) { context = context instanceof jQuery ? context[0] : context; doc = ( context ? context.ownerDocument || context : document ); // If a single string is passed in and it's a single tag // just do a createElement and skip the rest ret = rsingleTag.exec( selector ); if ( ret ) { if ( jQuery.isPlainObject( context ) ) { selector = [ document.createElement( ret[1] ) ]; jQuery.fn.attr.call( selector, context, true ); } else { selector = [ doc.createElement( ret[1] ) ]; } } else { ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; } return jQuery.merge( this, selector ); // HANDLE: $("#id") } else { elem = document.getElementById( match[2] ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 if ( elem && elem.parentNode ) { // Handle the case where IE and Opera return items // by name instead of ID if ( elem.id !== match[2] ) { return rootjQuery.find( selector ); } // Otherwise, we inject the element directly into the jQuery object this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this; } // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { return this.constructor( context ).find( selector ); } // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { return rootjQuery.ready( selector ); } if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } return jQuery.makeArray( selector, this ); }, // Start with an empty selector selector: "", // The current version of jQuery being used jquery: "1.7.2", // The default length of a jQuery object is 0 length: 0, // The number of elements contained in the matched element set size: function() { return this.length; }, toArray: function() { return slice.call( this, 0 ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { return num == null ? // Return a 'clean' array this.toArray() : // Return just the object ( num < 0 ? this[ this.length + num ] : this[ num ] ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) pushStack: function( elems, name, selector ) { // Build a new jQuery matched element set var ret = this.constructor(); if ( jQuery.isArray( elems ) ) { push.apply( ret, elems ); } else { jQuery.merge( ret, elems ); } // Add the old object onto the stack (as a reference) ret.prevObject = this; ret.context = this.context; if ( name === "find" ) { ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; } else if ( name ) { ret.selector = this.selector + "." + name + "(" + selector + ")"; } // Return the newly-formed element set return ret; }, // Execute a callback for every element in the matched set. // (You can seed the arguments with an array of args, but this is // only used internally.) each: function( callback, args ) { return jQuery.each( this, callback, args ); }, ready: function( fn ) { // Attach the listeners jQuery.bindReady(); // Add the callback readyList.add( fn ); return this; }, eq: function( i ) { i = +i; return i === -1 ? this.slice( i ) : this.slice( i, i + 1 ); }, first: function() { return this.eq( 0 ); }, last: function() { return this.eq( -1 ); }, slice: function() { return this.pushStack( slice.apply( this, arguments ), "slice", slice.call(arguments).join(",") ); }, map: function( callback ) { return this.pushStack( jQuery.map(this, function( elem, i ) { return callback.call( elem, i, elem ); })); }, end: function() { return this.prevObject || this.constructor(null); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. push: push, sort: [].sort, splice: [].splice }; // Give the init function the jQuery prototype for later instantiation jQuery.fn.init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction(target) ) { target = {}; } // extend jQuery itself if only one argument is passed if ( length === i ) { target = this; --i; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( (options = arguments[ i ]) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; }; jQuery.extend({ noConflict: function( deep ) { if ( window.$ === jQuery ) { window.$ = _$; } if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; }, // Is the DOM ready to be used? Set to true once it occurs. isReady: false, // A counter to track how many items to wait for before // the ready event fires. See #6781 readyWait: 1, // Hold (or release) the ready event holdReady: function( hold ) { if ( hold ) { jQuery.readyWait++; } else { jQuery.ready( true ); } }, // Handle when the DOM is ready ready: function( wait ) { // Either a released hold or an DOMready/load event and not yet ready if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). if ( !document.body ) { return setTimeout( jQuery.ready, 1 ); } // Remember that the DOM is ready jQuery.isReady = true; // If a normal DOM Ready event fired, decrement, and wait if need be if ( wait !== true && --jQuery.readyWait > 0 ) { return; } // If there are functions bound, to execute readyList.fireWith( document, [ jQuery ] ); // Trigger any bound ready events if ( jQuery.fn.trigger ) { jQuery( document ).trigger( "ready" ).off( "ready" ); } } }, bindReady: function() { if ( readyList ) { return; } readyList = jQuery.Callbacks( "once memory" ); // Catch cases where $(document).ready() is called after the // browser event has already occurred. if ( document.readyState === "complete" ) { // Handle it asynchronously to allow scripts the opportunity to delay ready return setTimeout( jQuery.ready, 1 ); } // Mozilla, Opera and webkit nightlies currently support this event if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", jQuery.ready, false ); // If IE event model is used } else if ( document.attachEvent ) { // ensure firing before onload, // maybe late but safe also for iframes document.attachEvent( "onreadystatechange", DOMContentLoaded ); // A fallback to window.onload, that will always work window.attachEvent( "onload", jQuery.ready ); // If IE and not a frame // continually check to see if the document is ready var toplevel = false; try { toplevel = window.frameElement == null; } catch(e) {} if ( document.documentElement.doScroll && toplevel ) { doScrollCheck(); } } }, // See test/unit/core.js for details concerning isFunction. // Since version 1.3, DOM methods and functions like alert // aren't supported. They return false on IE (#2968). isFunction: function( obj ) { return jQuery.type(obj) === "function"; }, isArray: Array.isArray || function( obj ) { return jQuery.type(obj) === "array"; }, isWindow: function( obj ) { return obj != null && obj == obj.window; }, isNumeric: function( obj ) { return !isNaN( parseFloat(obj) ) && isFinite( obj ); }, type: function( obj ) { return obj == null ? String( obj ) : class2type[ toString.call(obj) ] || "object"; }, isPlainObject: function( obj ) { // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } try { // Not own constructor property must be Object if ( obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { return false; } } catch ( e ) { // IE8,9 Will throw exceptions on certain host objects #9897 return false; } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key; for ( key in obj ) {} return key === undefined || hasOwn.call( obj, key ); }, isEmptyObject: function( obj ) { for ( var name in obj ) { return false; } return true; }, error: function( msg ) { throw new Error( msg ); }, parseJSON: function( data ) { if ( typeof data !== "string" || !data ) { return null; } // Make sure leading/trailing whitespace is removed (IE can't handle it) data = jQuery.trim( data ); // Attempt to parse using the native JSON parser first if ( window.JSON && window.JSON.parse ) { return window.JSON.parse( data ); } // Make sure the incoming data is actual JSON // Logic borrowed from http://json.org/json2.js if ( rvalidchars.test( data.replace( rvalidescape, "@" ) .replace( rvalidtokens, "]" ) .replace( rvalidbraces, "")) ) { return ( new Function( "return " + data ) )(); } jQuery.error( "Invalid JSON: " + data ); }, // Cross-browser xml parsing parseXML: function( data ) { if ( typeof data !== "string" || !data ) { return null; } var xml, tmp; try { if ( window.DOMParser ) { // Standard tmp = new DOMParser(); xml = tmp.parseFromString( data , "text/xml" ); } else { // IE xml = new ActiveXObject( "Microsoft.XMLDOM" ); xml.async = "false"; xml.loadXML( data ); } } catch( e ) { xml = undefined; } if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { jQuery.error( "Invalid XML: " + data ); } return xml; }, noop: function() {}, // Evaluates a script in a global context // Workarounds based on findings by Jim Driscoll // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context globalEval: function( data ) { if ( data && rnotwhite.test( data ) ) { // We use execScript on Internet Explorer // We use an anonymous function so that context is window // rather than jQuery in Firefox ( window.execScript || function( data ) { window[ "eval" ].call( window, data ); } )( data ); } }, // Convert dashed to camelCase; used by the css and data modules // Microsoft forgot to hump their vendor prefix (#9572) camelCase: function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); }, nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); }, // args is for internal usage only each: function( object, callback, args ) { var name, i = 0, length = object.length, isObj = length === undefined || jQuery.isFunction( object ); if ( args ) { if ( isObj ) { for ( name in object ) { if ( callback.apply( object[ name ], args ) === false ) { break; } } } else { for ( ; i < length; ) { if ( callback.apply( object[ i++ ], args ) === false ) { break; } } } // A special, fast, case for the most common use of each } else { if ( isObj ) { for ( name in object ) { if ( callback.call( object[ name ], name, object[ name ] ) === false ) { break; } } } else { for ( ; i < length; ) { if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { break; } } } } return object; }, // Use native String.trim function wherever possible trim: trim ? function( text ) { return text == null ? "" : trim.call( text ); } : // Otherwise use our own trimming functionality function( text ) { return text == null ? "" : text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); }, // results is for internal usage only makeArray: function( array, results ) { var ret = results || []; if ( array != null ) { // The window, strings (and functions) also have 'length' // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 var type = jQuery.type( array ); if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { push.call( ret, array ); } else { jQuery.merge( ret, array ); } } return ret; }, inArray: function( elem, array, i ) { var len; if ( array ) { if ( indexOf ) { return indexOf.call( array, elem, i ); } len = array.length; i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; for ( ; i < len; i++ ) { // Skip accessing in sparse arrays if ( i in array && array[ i ] === elem ) { return i; } } } return -1; }, merge: function( first, second ) { var i = first.length, j = 0; if ( typeof second.length === "number" ) { for ( var l = second.length; j < l; j++ ) { first[ i++ ] = second[ j ]; } } else { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; } } first.length = i; return first; }, grep: function( elems, callback, inv ) { var ret = [], retVal; inv = !!inv; // Go through the array, only saving the items // that pass the validator function for ( var i = 0, length = elems.length; i < length; i++ ) { retVal = !!callback( elems[ i ], i ); if ( inv !== retVal ) { ret.push( elems[ i ] ); } } return ret; }, // arg is for internal usage only map: function( elems, callback, arg ) { var value, key, ret = [], i = 0, length = elems.length, // jquery objects are treated as arrays isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; // Go through the array, translating each of the items to their if ( isArray ) { for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret[ ret.length ] = value; } } // Go through every key on the object, } else { for ( key in elems ) { value = callback( elems[ key ], key, arg ); if ( value != null ) { ret[ ret.length ] = value; } } } // Flatten any nested arrays return ret.concat.apply( [], ret ); }, // A global GUID counter for objects guid: 1, // Bind a function to a context, optionally partially applying any // arguments. proxy: function( fn, context ) { if ( typeof context === "string" ) { var tmp = fn[ context ]; context = fn; fn = tmp; } // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. if ( !jQuery.isFunction( fn ) ) { return undefined; } // Simulated bind var args = slice.call( arguments, 2 ), proxy = function() { return fn.apply( context, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; return proxy; }, // Mutifunctional method to get and set values to a collection // The value/s can optionally be executed if it's a function access: function( elems, fn, key, value, chainable, emptyGet, pass ) { var exec, bulk = key == null, i = 0, length = elems.length; // Sets many values if ( key && typeof key === "object" ) { for ( i in key ) { jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); } chainable = 1; // Sets one value } else if ( value !== undefined ) { // Optionally, function values get executed if exec is true exec = pass === undefined && jQuery.isFunction( value ); if ( bulk ) { // Bulk operations only iterate when executing function values if ( exec ) { exec = fn; fn = function( elem, key, value ) { return exec.call( jQuery( elem ), value ); }; // Otherwise they run against the entire set } else { fn.call( elems, value ); fn = null; } } if ( fn ) { for (; i < length; i++ ) { fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); } } chainable = 1; } return chainable ? elems : // Gets bulk ? fn.call( elems ) : length ? fn( elems[0], key ) : emptyGet; }, now: function() { return ( new Date() ).getTime(); }, // Use of jQuery.browser is frowned upon. // More details: http://docs.jquery.com/Utilities/jQuery.browser uaMatch: function( ua ) { ua = ua.toLowerCase(); var match = rwebkit.exec( ua ) || ropera.exec( ua ) || rmsie.exec( ua ) || ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || []; return { browser: match[1] || "", version: match[2] || "0" }; }, sub: function() { function jQuerySub( selector, context ) { return new jQuerySub.fn.init( selector, context ); } jQuery.extend( true, jQuerySub, this ); jQuerySub.superclass = this; jQuerySub.fn = jQuerySub.prototype = this(); jQuerySub.fn.constructor = jQuerySub; jQuerySub.sub = this.sub; jQuerySub.fn.init = function init( selector, context ) { if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { context = jQuerySub( context ); } return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); }; jQuerySub.fn.init.prototype = jQuerySub.fn; var rootjQuerySub = jQuerySub(document); return jQuerySub; }, browser: {} }); // Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); browserMatch = jQuery.uaMatch( userAgent ); if ( browserMatch.browser ) { jQuery.browser[ browserMatch.browser ] = true; jQuery.browser.version = browserMatch.version; } // Deprecated, use jQuery.browser.webkit instead if ( jQuery.browser.webkit ) { jQuery.browser.safari = true; } // IE doesn't match non-breaking spaces with \s if ( rnotwhite.test( "\xA0" ) ) { trimLeft = /^[\s\xA0]+/; trimRight = /[\s\xA0]+$/; } // All jQuery objects should point back to these rootjQuery = jQuery(document); // Cleanup functions for the document ready method if ( document.addEventListener ) { DOMContentLoaded = function() { document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); jQuery.ready(); }; } else if ( document.attachEvent ) { DOMContentLoaded = function() { // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). if ( document.readyState === "complete" ) { document.detachEvent( "onreadystatechange", DOMContentLoaded ); jQuery.ready(); } }; } // The DOM ready check for Internet Explorer function doScrollCheck() { if ( jQuery.isReady ) { return; } try { // If IE is used, use the trick by Diego Perini // http://javascript.nwbox.com/IEContentLoaded/ document.documentElement.doScroll("left"); } catch(e) { setTimeout( doScrollCheck, 1 ); return; } // and execute any waiting functions jQuery.ready(); } return jQuery; })(); // String to Object flags format cache var flagsCache = {}; // Convert String-formatted flags into Object-formatted ones and store in cache function createFlags( flags ) { var object = flagsCache[ flags ] = {}, i, length; flags = flags.split( /\s+/ ); for ( i = 0, length = flags.length; i < length; i++ ) { object[ flags[i] ] = true; } return object; } /* * Create a callback list using the following parameters: * * flags: an optional list of space-separated flags that will change how * the callback list behaves * * By default a callback list will act like an event callback list and can be * "fired" multiple times. * * Possible flags: * * once: will ensure the callback list can only be fired once (like a Deferred) * * memory: will keep track of previous values and will call any callback added * after the list has been fired right away with the latest "memorized" * values (like a Deferred) * * unique: will ensure a callback can only be added once (no duplicate in the list) * * stopOnFalse: interrupt callings when a callback returns false * */ jQuery.Callbacks = function( flags ) { // Convert flags from String-formatted to Object-formatted // (we check in cache first) flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; var // Actual callback list list = [], // Stack of fire calls for repeatable lists stack = [], // Last fire value (for non-forgettable lists) memory, // Flag to know if list was already fired fired, // Flag to know if list is currently firing firing, // First callback to fire (used internally by add and fireWith) firingStart, // End of the loop when firing firingLength, // Index of currently firing callback (modified by remove if needed) firingIndex, // Add one or several callbacks to the list add = function( args ) { var i, length, elem, type, actual; for ( i = 0, length = args.length; i < length; i++ ) { elem = args[ i ]; type = jQuery.type( elem ); if ( type === "array" ) { // Inspect recursively add( elem ); } else if ( type === "function" ) { // Add if not in unique mode and callback is not in if ( !flags.unique || !self.has( elem ) ) { list.push( elem ); } } } }, // Fire callbacks fire = function( context, args ) { args = args || []; memory = !flags.memory || [ context, args ]; fired = true; firing = true; firingIndex = firingStart || 0; firingStart = 0; firingLength = list.length; for ( ; list && firingIndex < firingLength; firingIndex++ ) { if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { memory = true; // Mark as halted break; } } firing = false; if ( list ) { if ( !flags.once ) { if ( stack && stack.length ) { memory = stack.shift(); self.fireWith( memory[ 0 ], memory[ 1 ] ); } } else if ( memory === true ) { self.disable(); } else { list = []; } } }, // Actual Callbacks object self = { // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { var length = list.length; add( arguments ); // Do we need to add the callbacks to the // current firing batch? if ( firing ) { firingLength = list.length; // With memory, if we're not firing then // we should call right away, unless previous // firing was halted (stopOnFalse) } else if ( memory && memory !== true ) { firingStart = length; fire( memory[ 0 ], memory[ 1 ] ); } } return this; }, // Remove a callback from the list remove: function() { if ( list ) { var args = arguments, argIndex = 0, argLength = args.length; for ( ; argIndex < argLength ; argIndex++ ) { for ( var i = 0; i < list.length; i++ ) { if ( args[ argIndex ] === list[ i ] ) { // Handle firingIndex and firingLength if ( firing ) { if ( i <= firingLength ) { firingLength--; if ( i <= firingIndex ) { firingIndex--; } } } // Remove the element list.splice( i--, 1 ); // If we have some unicity property then // we only need to do this once if ( flags.unique ) { break; } } } } } return this; }, // Control if a given callback is in the list has: function( fn ) { if ( list ) { var i = 0, length = list.length; for ( ; i < length; i++ ) { if ( fn === list[ i ] ) { return true; } } } return false; }, // Remove all callbacks from the list empty: function() { list = []; return this; }, // Have the list do nothing anymore disable: function() { list = stack = memory = undefined; return this; }, // Is it disabled? disabled: function() { return !list; }, // Lock the list in its current state lock: function() { stack = undefined; if ( !memory || memory === true ) { self.disable(); } return this; }, // Is it locked? locked: function() { return !stack; }, // Call all callbacks with the given context and arguments fireWith: function( context, args ) { if ( stack ) { if ( firing ) { if ( !flags.once ) { stack.push( [ context, args ] ); } } else if ( !( flags.once && memory ) ) { fire( context, args ); } } return this; }, // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, // To know if the callbacks have already been called at least once fired: function() { return !!fired; } }; return self; }; var // Static reference to slice sliceDeferred = [].slice; jQuery.extend({ Deferred: function( func ) { var doneList = jQuery.Callbacks( "once memory" ), failList = jQuery.Callbacks( "once memory" ), progressList = jQuery.Callbacks( "memory" ), state = "pending", lists = { resolve: doneList, reject: failList, notify: progressList }, promise = { done: doneList.add, fail: failList.add, progress: progressList.add, state: function() { return state; }, // Deprecated isResolved: doneList.fired, isRejected: failList.fired, then: function( doneCallbacks, failCallbacks, progressCallbacks ) { deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); return this; }, always: function() { deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); return this; }, pipe: function( fnDone, fnFail, fnProgress ) { return jQuery.Deferred(function( newDefer ) { jQuery.each( { done: [ fnDone, "resolve" ], fail: [ fnFail, "reject" ], progress: [ fnProgress, "notify" ] }, function( handler, data ) { var fn = data[ 0 ], action = data[ 1 ], returned; if ( jQuery.isFunction( fn ) ) { deferred[ handler ](function() { returned = fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); } else { newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); } }); } else { deferred[ handler ]( newDefer[ action ] ); } }); }).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { if ( obj == null ) { obj = promise; } else { for ( var key in promise ) { obj[ key ] = promise[ key ]; } } return obj; } }, deferred = promise.promise({}), key; for ( key in lists ) { deferred[ key ] = lists[ key ].fire; deferred[ key + "With" ] = lists[ key ].fireWith; } // Handle state deferred.done( function() { state = "resolved"; }, failList.disable, progressList.lock ).fail( function() { state = "rejected"; }, doneList.disable, progressList.lock ); // Call given func if any if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; }, // Deferred helper when: function( firstParam ) { var args = sliceDeferred.call( arguments, 0 ), i = 0, length = args.length, pValues = new Array( length ), count = length, pCount = length, deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? firstParam : jQuery.Deferred(), promise = deferred.promise(); function resolveFunc( i ) { return function( value ) { args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; if ( !( --count ) ) { deferred.resolveWith( deferred, args ); } }; } function progressFunc( i ) { return function( value ) { pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; deferred.notifyWith( promise, pValues ); }; } if ( length > 1 ) { for ( ; i < length; i++ ) { if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); } else { --count; } } if ( !count ) { deferred.resolveWith( deferred, args ); } } else if ( deferred !== firstParam ) { deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); } return promise; } }); jQuery.support = (function() { var support, all, a, select, opt, input, fragment, tds, events, eventName, i, isSupported, div = document.createElement( "div" ), documentElement = document.documentElement; // Preliminary tests div.setAttribute("className", "t"); div.innerHTML = "
a"; all = div.getElementsByTagName( "*" ); a = div.getElementsByTagName( "a" )[ 0 ]; // Can't get basic test support if ( !all || !all.length || !a ) { return {}; } // First batch of supports tests select = document.createElement( "select" ); opt = select.appendChild( document.createElement("option") ); input = div.getElementsByTagName( "input" )[ 0 ]; support = { // IE strips leading whitespace when .innerHTML is used leadingWhitespace: ( div.firstChild.nodeType === 3 ), // Make sure that tbody elements aren't automatically inserted // IE will insert them into empty tables tbody: !div.getElementsByTagName("tbody").length, // Make sure that link elements get serialized correctly by innerHTML // This requires a wrapper element in IE htmlSerialize: !!div.getElementsByTagName("link").length, // Get the style information from getAttribute // (IE uses .cssText instead) style: /top/.test( a.getAttribute("style") ), // Make sure that URLs aren't manipulated // (IE normalizes it by default) hrefNormalized: ( a.getAttribute("href") === "/a" ), // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 opacity: /^0.55/.test( a.style.opacity ), // Verify style float existence // (IE uses styleFloat instead of cssFloat) cssFloat: !!a.style.cssFloat, // Make sure that if no value is specified for a checkbox // that it defaults to "on". // (WebKit defaults to "" instead) checkOn: ( input.value === "on" ), // Make sure that a selected-by-default option has a working selected property. // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: opt.selected, // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) getSetAttribute: div.className !== "t", // Tests for enctype support on a form(#6743) enctype: !!document.createElement("form").enctype, // Makes sure cloning an html5 element does not cause problems // Where outerHTML is undefined, this still works html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", // Will be defined later submitBubbles: true, changeBubbles: true, focusinBubbles: false, deleteExpando: true, noCloneEvent: true, inlineBlockNeedsLayout: false, shrinkWrapBlocks: false, reliableMarginRight: true, pixelMargin: true }; // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat"); // Make sure checked status is properly cloned input.checked = true; support.noCloneChecked = input.cloneNode( true ).checked; // Make sure that the options inside disabled selects aren't marked as disabled // (WebKit marks them as disabled) select.disabled = true; support.optDisabled = !opt.disabled; // Test to see if it's possible to delete an expando from an element // Fails in Internet Explorer try { delete div.test; } catch( e ) { support.deleteExpando = false; } if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { div.attachEvent( "onclick", function() { // Cloning a node shouldn't copy over any // bound event handlers (IE does this) support.noCloneEvent = false; }); div.cloneNode( true ).fireEvent( "onclick" ); } // Check if a radio maintains its value // after being appended to the DOM input = document.createElement("input"); input.value = "t"; input.setAttribute("type", "radio"); support.radioValue = input.value === "t"; input.setAttribute("checked", "checked"); // #11217 - WebKit loses check when the name is after the checked attribute input.setAttribute( "name", "t" ); div.appendChild( input ); fragment = document.createDocumentFragment(); fragment.appendChild( div.lastChild ); // WebKit doesn't clone checked state correctly in fragments support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; // Check if a disconnected checkbox will retain its checked // value of true after appended to the DOM (IE6/7) support.appendChecked = input.checked; fragment.removeChild( input ); fragment.appendChild( div ); // Technique from Juriy Zaytsev // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ // We only care about the case where non-standard event systems // are used, namely in IE. Short-circuiting here helps us to // avoid an eval call (in setAttribute) which can cause CSP // to go haywire. See: https://developer.mozilla.org/en/Security/CSP if ( div.attachEvent ) { for ( i in { submit: 1, change: 1, focusin: 1 }) { eventName = "on" + i; isSupported = ( eventName in div ); if ( !isSupported ) { div.setAttribute( eventName, "return;" ); isSupported = ( typeof div[ eventName ] === "function" ); } support[ i + "Bubbles" ] = isSupported; } } fragment.removeChild( div ); // Null elements to avoid leaks in IE fragment = select = opt = div = input = null; // Run tests that need a body at doc ready jQuery(function() { var container, outer, inner, table, td, offsetSupport, marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight, paddingMarginBorderVisibility, paddingMarginBorder, body = document.getElementsByTagName("body")[0]; if ( !body ) { // Return for frameset docs that don't have a body return; } conMarginTop = 1; paddingMarginBorder = "padding:0;margin:0;border:"; positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;"; paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;"; style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;"; html = "
" + "" + "
"; container = document.createElement("div"); container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; body.insertBefore( container, body.firstChild ); // Construct the test element div = document.createElement("div"); container.appendChild( div ); // Check if table cells still have offsetWidth/Height when they are set // to display:none and there are still other visible table cells in a // table row; if so, offsetWidth/Height are not reliable for use when // determining if an element has been hidden directly using // display:none (it is still safe to use offsets if a parent element is // hidden; don safety goggles and see bug #4512 for more information). // (only IE 8 fails this test) div.innerHTML = "
t
"; tds = div.getElementsByTagName( "td" ); isSupported = ( tds[ 0 ].offsetHeight === 0 ); tds[ 0 ].style.display = ""; tds[ 1 ].style.display = "none"; // Check if empty table cells still have offsetWidth/Height // (IE <= 8 fail this test) support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); // Check if div with explicit width and no margin-right incorrectly // gets computed margin-right based on width of container. For more // info see bug #3333 // Fails in WebKit before Feb 2011 nightlies // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right if ( window.getComputedStyle ) { div.innerHTML = ""; marginDiv = document.createElement( "div" ); marginDiv.style.width = "0"; marginDiv.style.marginRight = "0"; div.style.width = "2px"; div.appendChild( marginDiv ); support.reliableMarginRight = ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; } if ( typeof div.style.zoom !== "undefined" ) { // Check if natively block-level elements act like inline-block // elements when setting their display to 'inline' and giving // them layout // (IE < 8 does this) div.innerHTML = ""; div.style.width = div.style.padding = "1px"; div.style.border = 0; div.style.overflow = "hidden"; div.style.display = "inline"; div.style.zoom = 1; support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); // Check if elements with layout shrink-wrap their children // (IE 6 does this) div.style.display = "block"; div.style.overflow = "visible"; div.innerHTML = "
"; support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); } div.style.cssText = positionTopLeftWidthHeight + paddingMarginBorderVisibility; div.innerHTML = html; outer = div.firstChild; inner = outer.firstChild; td = outer.nextSibling.firstChild.firstChild; offsetSupport = { doesNotAddBorder: ( inner.offsetTop !== 5 ), doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) }; inner.style.position = "fixed"; inner.style.top = "20px"; // safari subtracts parent border width here which is 5px offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); inner.style.position = inner.style.top = ""; outer.style.overflow = "hidden"; outer.style.position = "relative"; offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); if ( window.getComputedStyle ) { div.style.marginTop = "1%"; support.pixelMargin = ( window.getComputedStyle( div, null ) || { marginTop: 0 } ).marginTop !== "1%"; } if ( typeof container.style.zoom !== "undefined" ) { container.style.zoom = 1; } body.removeChild( container ); marginDiv = div = container = null; jQuery.extend( support, offsetSupport ); }); return support; })(); var rbrace = /^(?:\{.*\}|\[.*\])$/, rmultiDash = /([A-Z])/g; jQuery.extend({ cache: {}, // Please use with caution uuid: 0, // Unique for each copy of jQuery on the page // Non-digits removed to match rinlinejQuery expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), // The following elements throw uncatchable exceptions if you // attempt to add expando properties to them. noData: { "embed": true, // Ban all objects except for Flash (which handle expandos) "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", "applet": true }, hasData: function( elem ) { elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; return !!elem && !isEmptyDataObject( elem ); }, data: function( elem, name, data, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } var privateCache, thisCache, ret, internalKey = jQuery.expando, getByName = typeof name === "string", // We have to handle DOM nodes and JS objects differently because IE6-7 // can't GC object references properly across the DOM-JS boundary isNode = elem.nodeType, // Only DOM nodes need the global jQuery cache; JS object data is // attached directly to the object so GC can occur automatically cache = isNode ? jQuery.cache : elem, // Only defining an ID for JS objects if its cache already exists allows // the code to shortcut on the same path as a DOM node with no cache id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, isEvents = name === "events"; // Avoid doing any more work than we need to when trying to get data on an // object that has no data at all if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { return; } if ( !id ) { // Only DOM nodes need a new unique ID for each element since their data // ends up in the global cache if ( isNode ) { elem[ internalKey ] = id = ++jQuery.uuid; } else { id = internalKey; } } if ( !cache[ id ] ) { cache[ id ] = {}; // Avoids exposing jQuery metadata on plain JS objects when the object // is serialized using JSON.stringify if ( !isNode ) { cache[ id ].toJSON = jQuery.noop; } } // An object can be passed to jQuery.data instead of a key/value pair; this gets // shallow copied over onto the existing cache if ( typeof name === "object" || typeof name === "function" ) { if ( pvt ) { cache[ id ] = jQuery.extend( cache[ id ], name ); } else { cache[ id ].data = jQuery.extend( cache[ id ].data, name ); } } privateCache = thisCache = cache[ id ]; // jQuery data() is stored in a separate object inside the object's internal data // cache in order to avoid key collisions between internal data and user-defined // data. if ( !pvt ) { if ( !thisCache.data ) { thisCache.data = {}; } thisCache = thisCache.data; } if ( data !== undefined ) { thisCache[ jQuery.camelCase( name ) ] = data; } // Users should not attempt to inspect the internal events object using jQuery.data, // it is undocumented and subject to change. But does anyone listen? No. if ( isEvents && !thisCache[ name ] ) { return privateCache.events; } // Check for both converted-to-camel and non-converted data property names // If a data property was specified if ( getByName ) { // First Try to find as-is property data ret = thisCache[ name ]; // Test for null|undefined property data if ( ret == null ) { // Try to find the camelCased property ret = thisCache[ jQuery.camelCase( name ) ]; } } else { ret = thisCache; } return ret; }, removeData: function( elem, name, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } var thisCache, i, l, // Reference to internal data cache key internalKey = jQuery.expando, isNode = elem.nodeType, // See jQuery.data for more information cache = isNode ? jQuery.cache : elem, // See jQuery.data for more information id = isNode ? elem[ internalKey ] : internalKey; // If there is already no cache entry for this object, there is no // purpose in continuing if ( !cache[ id ] ) { return; } if ( name ) { thisCache = pvt ? cache[ id ] : cache[ id ].data; if ( thisCache ) { // Support array or space separated string names for data keys if ( !jQuery.isArray( name ) ) { // try the string as a key before any manipulation if ( name in thisCache ) { name = [ name ]; } else { // split the camel cased version by spaces unless a key with the spaces exists name = jQuery.camelCase( name ); if ( name in thisCache ) { name = [ name ]; } else { name = name.split( " " ); } } } for ( i = 0, l = name.length; i < l; i++ ) { delete thisCache[ name[i] ]; } // If there is no data left in the cache, we want to continue // and let the cache object itself get destroyed if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { return; } } } // See jQuery.data for more information if ( !pvt ) { delete cache[ id ].data; // Don't destroy the parent cache unless the internal data object // had been the only thing left in it if ( !isEmptyDataObject(cache[ id ]) ) { return; } } // Browsers that fail expando deletion also refuse to delete expandos on // the window, but it will allow it on all other JS objects; other browsers // don't care // Ensure that `cache` is not a window object #10080 if ( jQuery.support.deleteExpando || !cache.setInterval ) { delete cache[ id ]; } else { cache[ id ] = null; } // We destroyed the cache and need to eliminate the expando on the node to avoid // false lookups in the cache for entries that no longer exist if ( isNode ) { // IE does not allow us to delete expando properties from nodes, // nor does it have a removeAttribute function on Document nodes; // we must handle all of these cases if ( jQuery.support.deleteExpando ) { delete elem[ internalKey ]; } else if ( elem.removeAttribute ) { elem.removeAttribute( internalKey ); } else { elem[ internalKey ] = null; } } }, // For internal use only. _data: function( elem, name, data ) { return jQuery.data( elem, name, data, true ); }, // A method for determining if a DOM node can handle the data expando acceptData: function( elem ) { if ( elem.nodeName ) { var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; if ( match ) { return !(match === true || elem.getAttribute("classid") !== match); } } return true; } }); jQuery.fn.extend({ data: function( key, value ) { var parts, part, attr, name, l, elem = this[0], i = 0, data = null; // Gets all values if ( key === undefined ) { if ( this.length ) { data = jQuery.data( elem ); if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { attr = elem.attributes; for ( l = attr.length; i < l; i++ ) { name = attr[i].name; if ( name.indexOf( "data-" ) === 0 ) { name = jQuery.camelCase( name.substring(5) ); dataAttr( elem, name, data[ name ] ); } } jQuery._data( elem, "parsedAttrs", true ); } } return data; } // Sets multiple values if ( typeof key === "object" ) { return this.each(function() { jQuery.data( this, key ); }); } parts = key.split( ".", 2 ); parts[1] = parts[1] ? "." + parts[1] : ""; part = parts[1] + "!"; return jQuery.access( this, function( value ) { if ( value === undefined ) { data = this.triggerHandler( "getData" + part, [ parts[0] ] ); // Try to fetch any internally stored data first if ( data === undefined && elem ) { data = jQuery.data( elem, key ); data = dataAttr( elem, key, data ); } return data === undefined && parts[1] ? this.data( parts[0] ) : data; } parts[1] = value; this.each(function() { var self = jQuery( this ); self.triggerHandler( "setData" + part, parts ); jQuery.data( this, key, value ); self.triggerHandler( "changeData" + part, parts ); }); }, null, value, arguments.length > 1, null, false ); }, removeData: function( key ) { return this.each(function() { jQuery.removeData( this, key ); }); } }); function dataAttr( elem, key, data ) { // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute if ( data === undefined && elem.nodeType === 1 ) { var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); data = elem.getAttribute( name ); if ( typeof data === "string" ) { try { data = data === "true" ? true : data === "false" ? false : data === "null" ? null : jQuery.isNumeric( data ) ? +data : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {} // Make sure we set the data so it isn't changed later jQuery.data( elem, key, data ); } else { data = undefined; } } return data; } // checks a cache object for emptiness function isEmptyDataObject( obj ) { for ( var name in obj ) { // if the public data object is empty, the private is still empty if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { continue; } if ( name !== "toJSON" ) { return false; } } return true; } function handleQueueMarkDefer( elem, type, src ) { var deferDataKey = type + "defer", queueDataKey = type + "queue", markDataKey = type + "mark", defer = jQuery._data( elem, deferDataKey ); if ( defer && ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { // Give room for hard-coded callbacks to fire first // and eventually mark/queue something else on the element setTimeout( function() { if ( !jQuery._data( elem, queueDataKey ) && !jQuery._data( elem, markDataKey ) ) { jQuery.removeData( elem, deferDataKey, true ); defer.fire(); } }, 0 ); } } jQuery.extend({ _mark: function( elem, type ) { if ( elem ) { type = ( type || "fx" ) + "mark"; jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); } }, _unmark: function( force, elem, type ) { if ( force !== true ) { type = elem; elem = force; force = false; } if ( elem ) { type = type || "fx"; var key = type + "mark", count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); if ( count ) { jQuery._data( elem, key, count ); } else { jQuery.removeData( elem, key, true ); handleQueueMarkDefer( elem, type, "mark" ); } } }, queue: function( elem, type, data ) { var q; if ( elem ) { type = ( type || "fx" ) + "queue"; q = jQuery._data( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { if ( !q || jQuery.isArray(data) ) { q = jQuery._data( elem, type, jQuery.makeArray(data) ); } else { q.push( data ); } } return q || []; } }, dequeue: function( elem, type ) { type = type || "fx"; var queue = jQuery.queue( elem, type ), fn = queue.shift(), hooks = {}; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { fn = queue.shift(); } if ( fn ) { // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { queue.unshift( "inprogress" ); } jQuery._data( elem, type + ".run", hooks ); fn.call( elem, function() { jQuery.dequeue( elem, type ); }, hooks ); } if ( !queue.length ) { jQuery.removeData( elem, type + "queue " + type + ".run", true ); handleQueueMarkDefer( elem, type, "queue" ); } } }); jQuery.fn.extend({ queue: function( type, data ) { var setter = 2; if ( typeof type !== "string" ) { data = type; type = "fx"; setter--; } if ( arguments.length < setter ) { return jQuery.queue( this[0], type ); } return data === undefined ? this : this.each(function() { var queue = jQuery.queue( this, type, data ); if ( type === "fx" && queue[0] !== "inprogress" ) { jQuery.dequeue( this, type ); } }); }, dequeue: function( type ) { return this.each(function() { jQuery.dequeue( this, type ); }); }, // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ delay: function( time, type ) { time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; type = type || "fx"; return this.queue( type, function( next, hooks ) { var timeout = setTimeout( next, time ); hooks.stop = function() { clearTimeout( timeout ); }; }); }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, // Get a promise resolved when queues of a certain type // are emptied (fx is the type by default) promise: function( type, object ) { if ( typeof type !== "string" ) { object = type; type = undefined; } type = type || "fx"; var defer = jQuery.Deferred(), elements = this, i = elements.length, count = 1, deferDataKey = type + "defer", queueDataKey = type + "queue", markDataKey = type + "mark", tmp; function resolve() { if ( !( --count ) ) { defer.resolveWith( elements, [ elements ] ); } } while( i-- ) { if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { count++; tmp.add( resolve ); } } resolve(); return defer.promise( object ); } }); var rclass = /[\n\t\r]/g, rspace = /\s+/, rreturn = /\r/g, rtype = /^(?:button|input)$/i, rfocusable = /^(?:button|input|object|select|textarea)$/i, rclickable = /^a(?:rea)?$/i, rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, getSetAttribute = jQuery.support.getSetAttribute, nodeHook, boolHook, fixSpecified; jQuery.fn.extend({ attr: function( name, value ) { return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); }, removeAttr: function( name ) { return this.each(function() { jQuery.removeAttr( this, name ); }); }, prop: function( name, value ) { return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); }, removeProp: function( name ) { name = jQuery.propFix[ name ] || name; return this.each(function() { // try/catch handles cases where IE balks (such as removing a property on window) try { this[ name ] = undefined; delete this[ name ]; } catch( e ) {} }); }, addClass: function( value ) { var classNames, i, l, elem, setClass, c, cl; if ( jQuery.isFunction( value ) ) { return this.each(function( j ) { jQuery( this ).addClass( value.call(this, j, this.className) ); }); } if ( value && typeof value === "string" ) { classNames = value.split( rspace ); for ( i = 0, l = this.length; i < l; i++ ) { elem = this[ i ]; if ( elem.nodeType === 1 ) { if ( !elem.className && classNames.length === 1 ) { elem.className = value; } else { setClass = " " + elem.className + " "; for ( c = 0, cl = classNames.length; c < cl; c++ ) { if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { setClass += classNames[ c ] + " "; } } elem.className = jQuery.trim( setClass ); } } } } return this; }, removeClass: function( value ) { var classNames, i, l, elem, className, c, cl; if ( jQuery.isFunction( value ) ) { return this.each(function( j ) { jQuery( this ).removeClass( value.call(this, j, this.className) ); }); } if ( (value && typeof value === "string") || value === undefined ) { classNames = ( value || "" ).split( rspace ); for ( i = 0, l = this.length; i < l; i++ ) { elem = this[ i ]; if ( elem.nodeType === 1 && elem.className ) { if ( value ) { className = (" " + elem.className + " ").replace( rclass, " " ); for ( c = 0, cl = classNames.length; c < cl; c++ ) { className = className.replace(" " + classNames[ c ] + " ", " "); } elem.className = jQuery.trim( className ); } else { elem.className = ""; } } } } return this; }, toggleClass: function( value, stateVal ) { var type = typeof value, isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { return this.each(function( i ) { jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); }); } return this.each(function() { if ( type === "string" ) { // toggle individual class names var className, i = 0, self = jQuery( this ), state = stateVal, classNames = value.split( rspace ); while ( (className = classNames[ i++ ]) ) { // check each className given, space seperated list state = isBool ? state : !self.hasClass( className ); self[ state ? "addClass" : "removeClass" ]( className ); } } else if ( type === "undefined" || type === "boolean" ) { if ( this.className ) { // store className if set jQuery._data( this, "__className__", this.className ); } // toggle whole className this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } }); }, hasClass: function( selector ) { var className = " " + selector + " ", i = 0, l = this.length; for ( ; i < l; i++ ) { if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { return true; } } return false; }, val: function( value ) { var hooks, ret, isFunction, elem = this[0]; if ( !arguments.length ) { if ( elem ) { hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { return ret; } ret = elem.value; return typeof ret === "string" ? // handle most common string cases ret.replace(rreturn, "") : // handle cases where value is null/undef or number ret == null ? "" : ret; } return; } isFunction = jQuery.isFunction( value ); return this.each(function( i ) { var self = jQuery(this), val; if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { val = value.call( this, i, self.val() ); } else { val = value; } // Treat null/undefined as ""; convert numbers to string if ( val == null ) { val = ""; } else if ( typeof val === "number" ) { val += ""; } else if ( jQuery.isArray( val ) ) { val = jQuery.map(val, function ( value ) { return value == null ? "" : value + ""; }); } hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; // If set returns undefined, fall back to normal setting if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { this.value = val; } }); } }); jQuery.extend({ valHooks: { option: { get: function( elem ) { // attributes.value is undefined in Blackberry 4.7 but // uses .value. See #6932 var val = elem.attributes.value; return !val || val.specified ? elem.value : elem.text; } }, select: { get: function( elem ) { var value, i, max, option, index = elem.selectedIndex, values = [], options = elem.options, one = elem.type === "select-one"; // Nothing was selected if ( index < 0 ) { return null; } // Loop through all the selected options i = one ? index : 0; max = one ? index + 1 : options.length; for ( ; i < max; i++ ) { option = options[ i ]; // Don't return options that are disabled or in a disabled optgroup if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { // Get the specific value for the option value = jQuery( option ).val(); // We don't need an array for one selects if ( one ) { return value; } // Multi-Selects return an array values.push( value ); } } // Fixes Bug #2551 -- select.val() broken in IE after form.reset() if ( one && !values.length && options.length ) { return jQuery( options[ index ] ).val(); } return values; }, set: function( elem, value ) { var values = jQuery.makeArray( value ); jQuery(elem).find("option").each(function() { this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; }); if ( !values.length ) { elem.selectedIndex = -1; } return values; } } }, attrFn: { val: true, css: true, html: true, text: true, data: true, width: true, height: true, offset: true }, attr: function( elem, name, value, pass ) { var ret, hooks, notxml, nType = elem.nodeType; // don't get/set attributes on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } if ( pass && name in jQuery.attrFn ) { return jQuery( elem )[ name ]( value ); } // Fallback to prop when attributes are not supported if ( typeof elem.getAttribute === "undefined" ) { return jQuery.prop( elem, name, value ); } notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); // All attributes are lowercase // Grab necessary hook if one is defined if ( notxml ) { name = name.toLowerCase(); hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); } if ( value !== undefined ) { if ( value === null ) { jQuery.removeAttr( elem, name ); return; } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { elem.setAttribute( name, "" + value ); return value; } } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { ret = elem.getAttribute( name ); // Non-existent attributes return null, we normalize to undefined return ret === null ? undefined : ret; } }, removeAttr: function( elem, value ) { var propName, attrNames, name, l, isBool, i = 0; if ( value && elem.nodeType === 1 ) { attrNames = value.toLowerCase().split( rspace ); l = attrNames.length; for ( ; i < l; i++ ) { name = attrNames[ i ]; if ( name ) { propName = jQuery.propFix[ name ] || name; isBool = rboolean.test( name ); // See #9699 for explanation of this approach (setting first, then removal) // Do not do this for boolean attributes (see #10870) if ( !isBool ) { jQuery.attr( elem, name, "" ); } elem.removeAttribute( getSetAttribute ? name : propName ); // Set corresponding property to false for boolean attributes if ( isBool && propName in elem ) { elem[ propName ] = false; } } } } }, attrHooks: { type: { set: function( elem, value ) { // We can't allow the type property to be changed (since it causes problems in IE) if ( rtype.test( elem.nodeName ) && elem.parentNode ) { jQuery.error( "type property can't be changed" ); } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { // Setting the type on a radio button after the value resets the value in IE6-9 // Reset value to it's default in case type is set after value // This is for element creation var val = elem.value; elem.setAttribute( "type", value ); if ( val ) { elem.value = val; } return value; } } }, // Use the value property for back compat // Use the nodeHook for button elements in IE6/7 (#1954) value: { get: function( elem, name ) { if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { return nodeHook.get( elem, name ); } return name in elem ? elem.value : null; }, set: function( elem, value, name ) { if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { return nodeHook.set( elem, value, name ); } // Does not return so that setAttribute is also used elem.value = value; } } }, propFix: { tabindex: "tabIndex", readonly: "readOnly", "for": "htmlFor", "class": "className", maxlength: "maxLength", cellspacing: "cellSpacing", cellpadding: "cellPadding", rowspan: "rowSpan", colspan: "colSpan", usemap: "useMap", frameborder: "frameBorder", contenteditable: "contentEditable" }, prop: function( elem, name, value ) { var ret, hooks, notxml, nType = elem.nodeType; // don't get/set properties on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); if ( notxml ) { // Fix name and attach hooks name = jQuery.propFix[ name ] || name; hooks = jQuery.propHooks[ name ]; } if ( value !== undefined ) { if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { return ( elem[ name ] = value ); } } else { if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { return elem[ name ]; } } }, propHooks: { tabIndex: { get: function( elem ) { // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ var attributeNode = elem.getAttributeNode("tabindex"); return attributeNode && attributeNode.specified ? parseInt( attributeNode.value, 10 ) : rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? 0 : undefined; } } } }); // Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; // Hook for boolean attributes boolHook = { get: function( elem, name ) { // Align boolean attributes with corresponding properties // Fall back to attribute presence where some booleans are not supported var attrNode, property = jQuery.prop( elem, name ); return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? name.toLowerCase() : undefined; }, set: function( elem, value, name ) { var propName; if ( value === false ) { // Remove boolean attributes when set to false jQuery.removeAttr( elem, name ); } else { // value is true since we know at this point it's type boolean and not false // Set boolean attributes to the same name and set the DOM property propName = jQuery.propFix[ name ] || name; if ( propName in elem ) { // Only set the IDL specifically if it already exists on the element elem[ propName ] = true; } elem.setAttribute( name, name.toLowerCase() ); } return name; } }; // IE6/7 do not support getting/setting some attributes with get/setAttribute if ( !getSetAttribute ) { fixSpecified = { name: true, id: true, coords: true }; // Use this for any attribute in IE6/7 // This fixes almost every IE6/7 issue nodeHook = jQuery.valHooks.button = { get: function( elem, name ) { var ret; ret = elem.getAttributeNode( name ); return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? ret.nodeValue : undefined; }, set: function( elem, value, name ) { // Set the existing or create a new attribute node var ret = elem.getAttributeNode( name ); if ( !ret ) { ret = document.createAttribute( name ); elem.setAttributeNode( ret ); } return ( ret.nodeValue = value + "" ); } }; // Apply the nodeHook to tabindex jQuery.attrHooks.tabindex.set = nodeHook.set; // Set width and height to auto instead of 0 on empty string( Bug #8150 ) // This is for removals jQuery.each([ "width", "height" ], function( i, name ) { jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { set: function( elem, value ) { if ( value === "" ) { elem.setAttribute( name, "auto" ); return value; } } }); }); // Set contenteditable to false on removals(#10429) // Setting to empty string throws an error as an invalid value jQuery.attrHooks.contenteditable = { get: nodeHook.get, set: function( elem, value, name ) { if ( value === "" ) { value = "false"; } nodeHook.set( elem, value, name ); } }; } // Some attributes require a special call on IE if ( !jQuery.support.hrefNormalized ) { jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { get: function( elem ) { var ret = elem.getAttribute( name, 2 ); return ret === null ? undefined : ret; } }); }); } if ( !jQuery.support.style ) { jQuery.attrHooks.style = { get: function( elem ) { // Return undefined in the case of empty string // Normalize to lowercase since IE uppercases css property names return elem.style.cssText.toLowerCase() || undefined; }, set: function( elem, value ) { return ( elem.style.cssText = "" + value ); } }; } // Safari mis-reports the default selected property of an option // Accessing the parent's selectedIndex property fixes it if ( !jQuery.support.optSelected ) { jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { get: function( elem ) { var parent = elem.parentNode; if ( parent ) { parent.selectedIndex; // Make sure that it also works with optgroups, see #5701 if ( parent.parentNode ) { parent.parentNode.selectedIndex; } } return null; } }); } // IE6/7 call enctype encoding if ( !jQuery.support.enctype ) { jQuery.propFix.enctype = "encoding"; } // Radios and checkboxes getter/setter if ( !jQuery.support.checkOn ) { jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = { get: function( elem ) { // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified return elem.getAttribute("value") === null ? "on" : elem.value; } }; }); } jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { set: function( elem, value ) { if ( jQuery.isArray( value ) ) { return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); } } }); }); var rformElems = /^(?:textarea|input|select)$/i, rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, rhoverHack = /(?:^|\s)hover(\.\S+)?\b/, rkeyEvent = /^key/, rmouseEvent = /^(?:mouse|contextmenu)|click/, rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, quickParse = function( selector ) { var quick = rquickIs.exec( selector ); if ( quick ) { // 0 1 2 3 // [ _, tag, id, class ] quick[1] = ( quick[1] || "" ).toLowerCase(); quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); } return quick; }, quickIs = function( elem, m ) { var attrs = elem.attributes || {}; return ( (!m[1] || elem.nodeName.toLowerCase() === m[1]) && (!m[2] || (attrs.id || {}).value === m[2]) && (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) ); }, hoverHack = function( events ) { return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); }; /* * Helper functions for managing events -- not part of the public interface. * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { add: function( elem, types, handler, data, selector ) { var elemData, eventHandle, events, t, tns, type, namespaces, handleObj, handleObjIn, quick, handlers, special; // Don't attach events to noData or text/comment nodes (allow plain objects tho) if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { return; } // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; selector = handleObjIn.selector; } // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } // Init the element's event structure and main handler, if this is the first events = elemData.events; if ( !events ) { elemData.events = events = {}; } eventHandle = elemData.handle; if ( !eventHandle ) { elemData.handle = eventHandle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events eventHandle.elem = elem; } // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); types = jQuery.trim( hoverHack(types) ).split( " " ); for ( t = 0; t < types.length; t++ ) { tns = rtypenamespace.exec( types[t] ) || []; type = tns[1]; namespaces = ( tns[2] || "" ).split( "." ).sort(); // If event changes its type, use the special event handlers for the changed type special = jQuery.event.special[ type ] || {}; // If selector defined, determine special event api type, otherwise given type type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers handleObj = jQuery.extend({ type: type, origType: tns[1], data: data, handler: handler, guid: handler.guid, selector: selector, quick: selector && quickParse( selector ), namespace: namespaces.join(".") }, handleObjIn ); // Init the event handler queue if we're the first handlers = events[ type ]; if ( !handlers ) { handlers = events[ type ] = []; handlers.delegateCount = 0; // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); } } } if ( special.add ) { special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add to the element's handler list, delegates in front if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } // Nullify elem to prevent memory leaks in IE elem = null; }, global: {}, // Detach an event or set of events from an element remove: function( elem, types, handler, selector, mappedTypes ) { var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), t, tns, type, origType, namespaces, origCount, j, events, special, handle, eventType, handleObj; if ( !elemData || !(events = elemData.events) ) { return; } // Once for each type.namespace in types; type may be omitted types = jQuery.trim( hoverHack( types || "" ) ).split(" "); for ( t = 0; t < types.length; t++ ) { tns = rtypenamespace.exec( types[t] ) || []; type = origType = tns[1]; namespaces = tns[2]; // Unbind all events (on this namespace, if provided) for the element if ( !type ) { for ( type in events ) { jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } continue; } special = jQuery.event.special[ type ] || {}; type = ( selector? special.delegateType : special.bindType ) || type; eventType = events[ type ] || []; origCount = eventType.length; namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; // Remove matching events for ( j = 0; j < eventType.length; j++ ) { handleObj = eventType[ j ]; if ( ( mappedTypes || origType === handleObj.origType ) && ( !handler || handler.guid === handleObj.guid ) && ( !namespaces || namespaces.test( handleObj.namespace ) ) && ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { eventType.splice( j--, 1 ); if ( handleObj.selector ) { eventType.delegateCount--; } if ( special.remove ) { special.remove.call( elem, handleObj ); } } } // Remove generic event handler if we removed something and no more handlers exist // (avoids potential for endless recursion during removal of special event handlers) if ( eventType.length === 0 && origCount !== eventType.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { handle = elemData.handle; if ( handle ) { handle.elem = null; } // removeData also checks for emptiness and clears the expando if empty // so use it instead of delete jQuery.removeData( elem, [ "events", "handle" ], true ); } }, // Events that are safe to short-circuit if no handlers are attached. // Native DOM events should not be added, they may have inline handlers. customEvent: { "getData": true, "setData": true, "changeData": true }, trigger: function( event, data, elem, onlyHandlers ) { // Don't do events on text and comment nodes if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { return; } // Event object or event type var type = event.type || event, namespaces = [], cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; // focus/blur morphs to focusin/out; ensure we're not firing them right now if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { return; } if ( type.indexOf( "!" ) >= 0 ) { // Exclusive events trigger only for the exact event (no namespaces) type = type.slice(0, -1); exclusive = true; } if ( type.indexOf( "." ) >= 0 ) { // Namespaced trigger; create a regexp to match event type in handle() namespaces = type.split("."); type = namespaces.shift(); namespaces.sort(); } if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { // No jQuery handlers for this event type, and it can't have inline handlers return; } // Caller can pass in an Event, Object, or just an event type string event = typeof event === "object" ? // jQuery.Event object event[ jQuery.expando ] ? event : // Object literal new jQuery.Event( type, event ) : // Just the event type (string) new jQuery.Event( type ); event.type = type; event.isTrigger = true; event.exclusive = exclusive; event.namespace = namespaces.join( "." ); event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; // Handle a global trigger if ( !elem ) { // TODO: Stop taunting the data cache; remove global events and always attach to document cache = jQuery.cache; for ( i in cache ) { if ( cache[ i ].events && cache[ i ].events[ type ] ) { jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); } } return; } // Clean up the event in case it is being reused event.result = undefined; if ( !event.target ) { event.target = elem; } // Clone any incoming data and prepend the event, creating the handler arg list data = data != null ? jQuery.makeArray( data ) : []; data.unshift( event ); // Allow special events to draw outside the lines special = jQuery.event.special[ type ] || {}; if ( special.trigger && special.trigger.apply( elem, data ) === false ) { return; } // Determine event propagation path in advance, per W3C events spec (#9951) // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) eventPath = [[ elem, special.bindType || type ]]; if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { bubbleType = special.delegateType || type; cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; old = null; for ( ; cur; cur = cur.parentNode ) { eventPath.push([ cur, bubbleType ]); old = cur; } // Only add window if we got to document (e.g., not plain obj or detached DOM) if ( old && old === elem.ownerDocument ) { eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); } } // Fire handlers on the event path for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { cur = eventPath[i][0]; event.type = eventPath[i][1]; handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); } // Note that this is a bare JS function and not a jQuery handler handle = ontype && cur[ ontype ]; if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { event.preventDefault(); } } event.type = type; // If nobody prevented the default action, do it now if ( !onlyHandlers && !event.isDefaultPrevented() ) { if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { // Call a native DOM method on the target with the same name name as the event. // Can't use an .isFunction() check here because IE6/7 fails that test. // Don't do default actions on window, that's where global variables be (#6170) // IE<9 dies on focus/blur to hidden element (#1486) if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { // Don't re-trigger an onFOO event when we call its FOO() method old = elem[ ontype ]; if ( old ) { elem[ ontype ] = null; } // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; elem[ type ](); jQuery.event.triggered = undefined; if ( old ) { elem[ ontype ] = old; } } } } return event.result; }, dispatch: function( event ) { // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event || window.event ); var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), delegateCount = handlers.delegateCount, args = [].slice.call( arguments, 0 ), run_all = !event.exclusive && !event.namespace, special = jQuery.event.special[ event.type ] || {}, handlerQueue = [], i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; // Use the fix-ed jQuery.Event rather than the (read-only) native event args[0] = event; event.delegateTarget = this; // Call the preDispatch hook for the mapped type, and let it bail if desired if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } // Determine handlers that should run if there are delegated events // Avoid non-left-click bubbling in Firefox (#3861) if ( delegateCount && !(event.button && event.type === "click") ) { // Pregenerate a single jQuery object for reuse with .is() jqcur = jQuery(this); jqcur.context = this.ownerDocument || this; for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { // Don't process events on disabled elements (#6911, #8165) if ( cur.disabled !== true ) { selMatch = {}; matches = []; jqcur[0] = cur; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; sel = handleObj.selector; if ( selMatch[ sel ] === undefined ) { selMatch[ sel ] = ( handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) ); } if ( selMatch[ sel ] ) { matches.push( handleObj ); } } if ( matches.length ) { handlerQueue.push({ elem: cur, matches: matches }); } } } } // Add the remaining (directly-bound) handlers if ( handlers.length > delegateCount ) { handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); } // Run delegates first; they may want to stop propagation beneath us for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { matched = handlerQueue[ i ]; event.currentTarget = matched.elem; for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { handleObj = matched.matches[ j ]; // Triggered event must either 1) be non-exclusive and have no namespace, or // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { event.data = handleObj.data; event.handleObj = handleObj; ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { event.result = ret; if ( ret === false ) { event.preventDefault(); event.stopPropagation(); } } } } } // Call the postDispatch hook for the mapped type if ( special.postDispatch ) { special.postDispatch.call( this, event ); } return event.result; }, // Includes some event props shared by KeyEvent and MouseEvent // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), fixHooks: {}, keyHooks: { props: "char charCode key keyCode".split(" "), filter: function( event, original ) { // Add which for key events if ( event.which == null ) { event.which = original.charCode != null ? original.charCode : original.keyCode; } return event; } }, mouseHooks: { props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), filter: function( event, original ) { var eventDoc, doc, body, button = original.button, fromElement = original.fromElement; // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && original.clientX != null ) { eventDoc = event.target.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } // Add relatedTarget, if necessary if ( !event.relatedTarget && fromElement ) { event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; } // Add which for click: 1 === left; 2 === middle; 3 === right // Note: button is not normalized, so don't use it if ( !event.which && button !== undefined ) { event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); } return event; } }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } // Create a writable copy of the event object and normalize some properties var i, prop, originalEvent = event, fixHook = jQuery.event.fixHooks[ event.type ] || {}, copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; event = jQuery.Event( originalEvent ); for ( i = copy.length; i; ) { prop = copy[ --i ]; event[ prop ] = originalEvent[ prop ]; } // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) if ( !event.target ) { event.target = originalEvent.srcElement || document; } // Target should not be a text node (#504, Safari) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) if ( event.metaKey === undefined ) { event.metaKey = event.ctrlKey; } return fixHook.filter? fixHook.filter( event, originalEvent ) : event; }, special: { ready: { // Make sure the ready event is setup setup: jQuery.bindReady }, load: { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus: { delegateType: "focusin" }, blur: { delegateType: "focusout" }, beforeunload: { setup: function( data, namespaces, eventHandle ) { // We only want to do this special case on windows if ( jQuery.isWindow( this ) ) { this.onbeforeunload = eventHandle; } }, teardown: function( namespaces, eventHandle ) { if ( this.onbeforeunload === eventHandle ) { this.onbeforeunload = null; } } } }, simulate: function( type, elem, event, bubble ) { // Piggyback on a donor event to simulate a different one. // Fake originalEvent to avoid donor's stopPropagation, but if the // simulated event prevents default then we do the same on the donor. var e = jQuery.extend( new jQuery.Event(), event, { type: type, isSimulated: true, originalEvent: {} } ); if ( bubble ) { jQuery.event.trigger( e, null, elem ); } else { jQuery.event.dispatch.call( elem, e ); } if ( e.isDefaultPrevented() ) { event.preventDefault(); } } }; // Some plugins are using, but it's undocumented/deprecated and will be removed. // The 1.7 special event interface should provide all the hooks needed now. jQuery.event.handle = jQuery.event.dispatch; jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { if ( elem.removeEventListener ) { elem.removeEventListener( type, handle, false ); } } : function( elem, type, handle ) { if ( elem.detachEvent ) { elem.detachEvent( "on" + type, handle ); } }; jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword if ( !(this instanceof jQuery.Event) ) { return new jQuery.Event( src, props ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; // Event type } else { this.type = src; } // Put explicitly provided properties onto the event object if ( props ) { jQuery.extend( this, props ); } // Create a timestamp if incoming event doesn't have one this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; }; function returnFalse() { return false; } function returnTrue() { return true; } // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { preventDefault: function() { this.isDefaultPrevented = returnTrue; var e = this.originalEvent; if ( !e ) { return; } // if preventDefault exists run it on the original event if ( e.preventDefault ) { e.preventDefault(); // otherwise set the returnValue property of the original event to false (IE) } else { e.returnValue = false; } }, stopPropagation: function() { this.isPropagationStopped = returnTrue; var e = this.originalEvent; if ( !e ) { return; } // if stopPropagation exists run it on the original event if ( e.stopPropagation ) { e.stopPropagation(); } // otherwise set the cancelBubble property of the original event to true (IE) e.cancelBubble = true; }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); }, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse }; // Create mouseenter/leave events using mouseover/out and event-time checks jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { delegateType: fix, bindType: fix, handle: function( event ) { var target = this, related = event.relatedTarget, handleObj = event.handleObj, selector = handleObj.selector, ret; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || (related !== target && !jQuery.contains( target, related )) ) { event.type = handleObj.origType; ret = handleObj.handler.apply( this, arguments ); event.type = fix; } return ret; } }; }); // IE submit delegation if ( !jQuery.support.submitBubbles ) { jQuery.event.special.submit = { setup: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Lazy-add a submit handler when a descendant form may potentially be submitted jQuery.event.add( this, "click._submit keypress._submit", function( e ) { // Node name check avoids a VML-related crash in IE (#9807) var elem = e.target, form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; if ( form && !form._submit_attached ) { jQuery.event.add( form, "submit._submit", function( event ) { event._submit_bubble = true; }); form._submit_attached = true; } }); // return undefined since we don't need an event listener }, postDispatch: function( event ) { // If form was submitted by the user, bubble the event up the tree if ( event._submit_bubble ) { delete event._submit_bubble; if ( this.parentNode && !event.isTrigger ) { jQuery.event.simulate( "submit", this.parentNode, event, true ); } } }, teardown: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Remove delegated handlers; cleanData eventually reaps submit handlers attached above jQuery.event.remove( this, "._submit" ); } }; } // IE change delegation and checkbox/radio fix if ( !jQuery.support.changeBubbles ) { jQuery.event.special.change = { setup: function() { if ( rformElems.test( this.nodeName ) ) { // IE doesn't fire change on a check/radio until blur; trigger it on click // after a propertychange. Eat the blur-change in special.change.handle. // This still fires onchange a second time for check/radio after blur. if ( this.type === "checkbox" || this.type === "radio" ) { jQuery.event.add( this, "propertychange._change", function( event ) { if ( event.originalEvent.propertyName === "checked" ) { this._just_changed = true; } }); jQuery.event.add( this, "click._change", function( event ) { if ( this._just_changed && !event.isTrigger ) { this._just_changed = false; jQuery.event.simulate( "change", this, event, true ); } }); } return false; } // Delegated event; lazy-add a change handler on descendant inputs jQuery.event.add( this, "beforeactivate._change", function( e ) { var elem = e.target; if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { jQuery.event.add( elem, "change._change", function( event ) { if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { jQuery.event.simulate( "change", this.parentNode, event, true ); } }); elem._change_attached = true; } }); }, handle: function( event ) { var elem = event.target; // Swallow native change events from checkbox/radio, we already triggered them above if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { return event.handleObj.handler.apply( this, arguments ); } }, teardown: function() { jQuery.event.remove( this, "._change" ); return rformElems.test( this.nodeName ); } }; } // Create "bubbling" focus and blur events if ( !jQuery.support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { // Attach a single capturing handler while someone wants focusin/focusout var attaches = 0, handler = function( event ) { jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); }; jQuery.event.special[ fix ] = { setup: function() { if ( attaches++ === 0 ) { document.addEventListener( orig, handler, true ); } }, teardown: function() { if ( --attaches === 0 ) { document.removeEventListener( orig, handler, true ); } } }; }); } jQuery.fn.extend({ on: function( types, selector, data, fn, /*INTERNAL*/ one ) { var origFn, type; // Types can be a map of types/handlers if ( typeof types === "object" ) { // ( types-Object, selector, data ) if ( typeof selector !== "string" ) { // && selector != null // ( types-Object, data ) data = data || selector; selector = undefined; } for ( type in types ) { this.on( type, selector, data, types[ type ], one ); } return this; } if ( data == null && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } else if ( fn == null ) { if ( typeof selector === "string" ) { // ( types, selector, fn ) fn = data; data = undefined; } else { // ( types, data, fn ) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { fn = returnFalse; } else if ( !fn ) { return this; } if ( one === 1 ) { origFn = fn; fn = function( event ) { // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } return this.each( function() { jQuery.event.add( this, types, fn, data, selector ); }); }, one: function( types, selector, data, fn ) { return this.on( types, selector, data, fn, 1 ); }, off: function( types, selector, fn ) { if ( types && types.preventDefault && types.handleObj ) { // ( event ) dispatched jQuery.Event var handleObj = types.handleObj; jQuery( types.delegateTarget ).off( handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, handleObj.selector, handleObj.handler ); return this; } if ( typeof types === "object" ) { // ( types-object [, selector] ) for ( var type in types ) { this.off( type, selector, types[ type ] ); } return this; } if ( selector === false || typeof selector === "function" ) { // ( types [, fn] ) fn = selector; selector = undefined; } if ( fn === false ) { fn = returnFalse; } return this.each(function() { jQuery.event.remove( this, types, fn, selector ); }); }, bind: function( types, data, fn ) { return this.on( types, null, data, fn ); }, unbind: function( types, fn ) { return this.off( types, null, fn ); }, live: function( types, data, fn ) { jQuery( this.context ).on( types, this.selector, data, fn ); return this; }, die: function( types, fn ) { jQuery( this.context ).off( types, this.selector || "**", fn ); return this; }, delegate: function( selector, types, data, fn ) { return this.on( types, selector, data, fn ); }, undelegate: function( selector, types, fn ) { // ( namespace ) or ( selector, types [, fn] ) return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); }, trigger: function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); }); }, triggerHandler: function( type, data ) { if ( this[0] ) { return jQuery.event.trigger( type, data, this[0], true ); } }, toggle: function( fn ) { // Save reference to arguments for access in closure var args = arguments, guid = fn.guid || jQuery.guid++, i = 0, toggler = function( event ) { // Figure out which function to execute var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); // Make sure that clicks stop event.preventDefault(); // and execute the function return args[ lastToggle ].apply( this, arguments ) || false; }; // link all the functions, so any of them can unbind this click handler toggler.guid = guid; while ( i < args.length ) { args[ i++ ].guid = guid; } return this.click( toggler ); }, hover: function( fnOver, fnOut ) { return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); } }); jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { // Handle event binding jQuery.fn[ name ] = function( data, fn ) { if ( fn == null ) { fn = data; data = null; } return arguments.length > 0 ? this.on( name, null, data, fn ) : this.trigger( name ); }; if ( jQuery.attrFn ) { jQuery.attrFn[ name ] = true; } if ( rkeyEvent.test( name ) ) { jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; } if ( rmouseEvent.test( name ) ) { jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; } }); /*! * Sizzle CSS Selector Engine * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * More information: http://sizzlejs.com/ */ (function(){ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, expando = "sizcache" + (Math.random() + '').replace('.', ''), done = 0, toString = Object.prototype.toString, hasDuplicate = false, baseHasDuplicate = true, rBackslash = /\\/g, rReturn = /\r\n/g, rNonWord = /\W/; // Here we check if the JavaScript engine is using some sort of // optimization where it does not always call our comparision // function. If that is the case, discard the hasDuplicate value. // Thus far that includes Google Chrome. [0, 0].sort(function() { baseHasDuplicate = false; return 0; }); var Sizzle = function( selector, context, results, seed ) { results = results || []; context = context || document; var origContext = context; if ( context.nodeType !== 1 && context.nodeType !== 9 ) { return []; } if ( !selector || typeof selector !== "string" ) { return results; } var m, set, checkSet, extra, ret, cur, pop, i, prune = true, contextXML = Sizzle.isXML( context ), parts = [], soFar = selector; // Reset the position of the chunker regexp (start from head) do { chunker.exec( "" ); m = chunker.exec( soFar ); if ( m ) { soFar = m[3]; parts.push( m[1] ); if ( m[2] ) { extra = m[3]; break; } } } while ( m ); if ( parts.length > 1 && origPOS.exec( selector ) ) { if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { set = posProcess( parts[0] + parts[1], context, seed ); } else { set = Expr.relative[ parts[0] ] ? [ context ] : Sizzle( parts.shift(), context ); while ( parts.length ) { selector = parts.shift(); if ( Expr.relative[ selector ] ) { selector += parts.shift(); } set = posProcess( selector, set, seed ); } } } else { // Take a shortcut and set the context if the root selector is an ID // (but not if it'll be faster if the inner selector is an ID) if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { ret = Sizzle.find( parts.shift(), context, contextXML ); context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; } if ( context ) { ret = seed ? { expr: parts.pop(), set: makeArray(seed) } : Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; if ( parts.length > 0 ) { checkSet = makeArray( set ); } else { prune = false; } while ( parts.length ) { cur = parts.pop(); pop = cur; if ( !Expr.relative[ cur ] ) { cur = ""; } else { pop = parts.pop(); } if ( pop == null ) { pop = context; } Expr.relative[ cur ]( checkSet, pop, contextXML ); } } else { checkSet = parts = []; } } if ( !checkSet ) { checkSet = set; } if ( !checkSet ) { Sizzle.error( cur || selector ); } if ( toString.call(checkSet) === "[object Array]" ) { if ( !prune ) { results.push.apply( results, checkSet ); } else if ( context && context.nodeType === 1 ) { for ( i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { results.push( set[i] ); } } } else { for ( i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && checkSet[i].nodeType === 1 ) { results.push( set[i] ); } } } } else { makeArray( checkSet, results ); } if ( extra ) { Sizzle( extra, origContext, results, seed ); Sizzle.uniqueSort( results ); } return results; }; Sizzle.uniqueSort = function( results ) { if ( sortOrder ) { hasDuplicate = baseHasDuplicate; results.sort( sortOrder ); if ( hasDuplicate ) { for ( var i = 1; i < results.length; i++ ) { if ( results[i] === results[ i - 1 ] ) { results.splice( i--, 1 ); } } } } return results; }; Sizzle.matches = function( expr, set ) { return Sizzle( expr, null, null, set ); }; Sizzle.matchesSelector = function( node, expr ) { return Sizzle( expr, null, null, [node] ).length > 0; }; Sizzle.find = function( expr, context, isXML ) { var set, i, len, match, type, left; if ( !expr ) { return []; } for ( i = 0, len = Expr.order.length; i < len; i++ ) { type = Expr.order[i]; if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { left = match[1]; match.splice( 1, 1 ); if ( left.substr( left.length - 1 ) !== "\\" ) { match[1] = (match[1] || "").replace( rBackslash, "" ); set = Expr.find[ type ]( match, context, isXML ); if ( set != null ) { expr = expr.replace( Expr.match[ type ], "" ); break; } } } } if ( !set ) { set = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName( "*" ) : []; } return { set: set, expr: expr }; }; Sizzle.filter = function( expr, set, inplace, not ) { var match, anyFound, type, found, item, filter, left, i, pass, old = expr, result = [], curLoop = set, isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); while ( expr && set.length ) { for ( type in Expr.filter ) { if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { filter = Expr.filter[ type ]; left = match[1]; anyFound = false; match.splice(1,1); if ( left.substr( left.length - 1 ) === "\\" ) { continue; } if ( curLoop === result ) { result = []; } if ( Expr.preFilter[ type ] ) { match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); if ( !match ) { anyFound = found = true; } else if ( match === true ) { continue; } } if ( match ) { for ( i = 0; (item = curLoop[i]) != null; i++ ) { if ( item ) { found = filter( item, match, i, curLoop ); pass = not ^ found; if ( inplace && found != null ) { if ( pass ) { anyFound = true; } else { curLoop[i] = false; } } else if ( pass ) { result.push( item ); anyFound = true; } } } } if ( found !== undefined ) { if ( !inplace ) { curLoop = result; } expr = expr.replace( Expr.match[ type ], "" ); if ( !anyFound ) { return []; } break; } } } // Improper expression if ( expr === old ) { if ( anyFound == null ) { Sizzle.error( expr ); } else { break; } } old = expr; } return curLoop; }; Sizzle.error = function( msg ) { throw new Error( "Syntax error, unrecognized expression: " + msg ); }; /** * Utility function for retreiving the text value of an array of DOM nodes * @param {Array|Element} elem */ var getText = Sizzle.getText = function( elem ) { var i, node, nodeType = elem.nodeType, ret = ""; if ( nodeType ) { if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { // Use textContent || innerText for elements if ( typeof elem.textContent === 'string' ) { return elem.textContent; } else if ( typeof elem.innerText === 'string' ) { // Replace IE's carriage returns return elem.innerText.replace( rReturn, '' ); } else { // Traverse it's children for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { ret += getText( elem ); } } } else if ( nodeType === 3 || nodeType === 4 ) { return elem.nodeValue; } } else { // If no nodeType, this is expected to be an array for ( i = 0; (node = elem[i]); i++ ) { // Do not traverse comment nodes if ( node.nodeType !== 8 ) { ret += getText( node ); } } } return ret; }; var Expr = Sizzle.selectors = { order: [ "ID", "NAME", "TAG" ], match: { ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ }, leftMatch: {}, attrMap: { "class": "className", "for": "htmlFor" }, attrHandle: { href: function( elem ) { return elem.getAttribute( "href" ); }, type: function( elem ) { return elem.getAttribute( "type" ); } }, relative: { "+": function(checkSet, part){ var isPartStr = typeof part === "string", isTag = isPartStr && !rNonWord.test( part ), isPartStrNotTag = isPartStr && !isTag; if ( isTag ) { part = part.toLowerCase(); } for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { if ( (elem = checkSet[i]) ) { while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? elem || false : elem === part; } } if ( isPartStrNotTag ) { Sizzle.filter( part, checkSet, true ); } }, ">": function( checkSet, part ) { var elem, isPartStr = typeof part === "string", i = 0, l = checkSet.length; if ( isPartStr && !rNonWord.test( part ) ) { part = part.toLowerCase(); for ( ; i < l; i++ ) { elem = checkSet[i]; if ( elem ) { var parent = elem.parentNode; checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; } } } else { for ( ; i < l; i++ ) { elem = checkSet[i]; if ( elem ) { checkSet[i] = isPartStr ? elem.parentNode : elem.parentNode === part; } } if ( isPartStr ) { Sizzle.filter( part, checkSet, true ); } } }, "": function(checkSet, part, isXML){ var nodeCheck, doneName = done++, checkFn = dirCheck; if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); }, "~": function( checkSet, part, isXML ) { var nodeCheck, doneName = done++, checkFn = dirCheck; if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); } }, find: { ID: function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 return m && m.parentNode ? [m] : []; } }, NAME: function( match, context ) { if ( typeof context.getElementsByName !== "undefined" ) { var ret = [], results = context.getElementsByName( match[1] ); for ( var i = 0, l = results.length; i < l; i++ ) { if ( results[i].getAttribute("name") === match[1] ) { ret.push( results[i] ); } } return ret.length === 0 ? null : ret; } }, TAG: function( match, context ) { if ( typeof context.getElementsByTagName !== "undefined" ) { return context.getElementsByTagName( match[1] ); } } }, preFilter: { CLASS: function( match, curLoop, inplace, result, not, isXML ) { match = " " + match[1].replace( rBackslash, "" ) + " "; if ( isXML ) { return match; } for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { if ( elem ) { if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { if ( !inplace ) { result.push( elem ); } } else if ( inplace ) { curLoop[i] = false; } } } return false; }, ID: function( match ) { return match[1].replace( rBackslash, "" ); }, TAG: function( match, curLoop ) { return match[1].replace( rBackslash, "" ).toLowerCase(); }, CHILD: function( match ) { if ( match[1] === "nth" ) { if ( !match[2] ) { Sizzle.error( match[0] ); } match[2] = match[2].replace(/^\+|\s*/g, ''); // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); // calculate the numbers (first)n+(last) including if they are negative match[2] = (test[1] + (test[2] || 1)) - 0; match[3] = test[3] - 0; } else if ( match[2] ) { Sizzle.error( match[0] ); } // TODO: Move to normal caching system match[0] = done++; return match; }, ATTR: function( match, curLoop, inplace, result, not, isXML ) { var name = match[1] = match[1].replace( rBackslash, "" ); if ( !isXML && Expr.attrMap[name] ) { match[1] = Expr.attrMap[name]; } // Handle if an un-quoted value was used match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); if ( match[2] === "~=" ) { match[4] = " " + match[4] + " "; } return match; }, PSEUDO: function( match, curLoop, inplace, result, not ) { if ( match[1] === "not" ) { // If we're dealing with a complex expression, or a simple one if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { match[3] = Sizzle(match[3], null, null, curLoop); } else { var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); if ( !inplace ) { result.push.apply( result, ret ); } return false; } } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { return true; } return match; }, POS: function( match ) { match.unshift( true ); return match; } }, filters: { enabled: function( elem ) { return elem.disabled === false && elem.type !== "hidden"; }, disabled: function( elem ) { return elem.disabled === true; }, checked: function( elem ) { return elem.checked === true; }, selected: function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly if ( elem.parentNode ) { elem.parentNode.selectedIndex; } return elem.selected === true; }, parent: function( elem ) { return !!elem.firstChild; }, empty: function( elem ) { return !elem.firstChild; }, has: function( elem, i, match ) { return !!Sizzle( match[3], elem ).length; }, header: function( elem ) { return (/h\d/i).test( elem.nodeName ); }, text: function( elem ) { var attr = elem.getAttribute( "type" ), type = elem.type; // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) // use getAttribute instead to test this case return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); }, radio: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; }, checkbox: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; }, file: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; }, password: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; }, submit: function( elem ) { var name = elem.nodeName.toLowerCase(); return (name === "input" || name === "button") && "submit" === elem.type; }, image: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; }, reset: function( elem ) { var name = elem.nodeName.toLowerCase(); return (name === "input" || name === "button") && "reset" === elem.type; }, button: function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && "button" === elem.type || name === "button"; }, input: function( elem ) { return (/input|select|textarea|button/i).test( elem.nodeName ); }, focus: function( elem ) { return elem === elem.ownerDocument.activeElement; } }, setFilters: { first: function( elem, i ) { return i === 0; }, last: function( elem, i, match, array ) { return i === array.length - 1; }, even: function( elem, i ) { return i % 2 === 0; }, odd: function( elem, i ) { return i % 2 === 1; }, lt: function( elem, i, match ) { return i < match[3] - 0; }, gt: function( elem, i, match ) { return i > match[3] - 0; }, nth: function( elem, i, match ) { return match[3] - 0 === i; }, eq: function( elem, i, match ) { return match[3] - 0 === i; } }, filter: { PSEUDO: function( elem, match, i, array ) { var name = match[1], filter = Expr.filters[ name ]; if ( filter ) { return filter( elem, i, match, array ); } else if ( name === "contains" ) { return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; } else if ( name === "not" ) { var not = match[3]; for ( var j = 0, l = not.length; j < l; j++ ) { if ( not[j] === elem ) { return false; } } return true; } else { Sizzle.error( name ); } }, CHILD: function( elem, match ) { var first, last, doneName, parent, cache, count, diff, type = match[1], node = elem; switch ( type ) { case "only": case "first": while ( (node = node.previousSibling) ) { if ( node.nodeType === 1 ) { return false; } } if ( type === "first" ) { return true; } node = elem; /* falls through */ case "last": while ( (node = node.nextSibling) ) { if ( node.nodeType === 1 ) { return false; } } return true; case "nth": first = match[2]; last = match[3]; if ( first === 1 && last === 0 ) { return true; } doneName = match[0]; parent = elem.parentNode; if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { count = 0; for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { node.nodeIndex = ++count; } } parent[ expando ] = doneName; } diff = elem.nodeIndex - last; if ( first === 0 ) { return diff === 0; } else { return ( diff % first === 0 && diff / first >= 0 ); } } }, ID: function( elem, match ) { return elem.nodeType === 1 && elem.getAttribute("id") === match; }, TAG: function( elem, match ) { return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; }, CLASS: function( elem, match ) { return (" " + (elem.className || elem.getAttribute("class")) + " ") .indexOf( match ) > -1; }, ATTR: function( elem, match ) { var name = match[1], result = Sizzle.attr ? Sizzle.attr( elem, name ) : Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : elem[ name ] != null ? elem[ name ] : elem.getAttribute( name ), value = result + "", type = match[2], check = match[4]; return result == null ? type === "!=" : !type && Sizzle.attr ? result != null : type === "=" ? value === check : type === "*=" ? value.indexOf(check) >= 0 : type === "~=" ? (" " + value + " ").indexOf(check) >= 0 : !check ? value && result !== false : type === "!=" ? value !== check : type === "^=" ? value.indexOf(check) === 0 : type === "$=" ? value.substr(value.length - check.length) === check : type === "|=" ? value === check || value.substr(0, check.length + 1) === check + "-" : false; }, POS: function( elem, match, i, array ) { var name = match[2], filter = Expr.setFilters[ name ]; if ( filter ) { return filter( elem, i, match, array ); } } } }; var origPOS = Expr.match.POS, fescape = function(all, num){ return "\\" + (num - 0 + 1); }; for ( var type in Expr.match ) { Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); } // Expose origPOS // "global" as in regardless of relation to brackets/parens Expr.match.globalPOS = origPOS; var makeArray = function( array, results ) { array = Array.prototype.slice.call( array, 0 ); if ( results ) { results.push.apply( results, array ); return results; } return array; }; // Perform a simple check to determine if the browser is capable of // converting a NodeList to an array using builtin methods. // Also verifies that the returned array holds DOM nodes // (which is not the case in the Blackberry browser) try { Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; // Provide a fallback method if it does not work } catch( e ) { makeArray = function( array, results ) { var i = 0, ret = results || []; if ( toString.call(array) === "[object Array]" ) { Array.prototype.push.apply( ret, array ); } else { if ( typeof array.length === "number" ) { for ( var l = array.length; i < l; i++ ) { ret.push( array[i] ); } } else { for ( ; array[i]; i++ ) { ret.push( array[i] ); } } } return ret; }; } var sortOrder, siblingCheck; if ( document.documentElement.compareDocumentPosition ) { sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; return 0; } if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { return a.compareDocumentPosition ? -1 : 1; } return a.compareDocumentPosition(b) & 4 ? -1 : 1; }; } else { sortOrder = function( a, b ) { // The nodes are identical, we can exit early if ( a === b ) { hasDuplicate = true; return 0; // Fallback to using sourceIndex (in IE) if it's available on both nodes } else if ( a.sourceIndex && b.sourceIndex ) { return a.sourceIndex - b.sourceIndex; } var al, bl, ap = [], bp = [], aup = a.parentNode, bup = b.parentNode, cur = aup; // If the nodes are siblings (or identical) we can do a quick check if ( aup === bup ) { return siblingCheck( a, b ); // If no parents were found then the nodes are disconnected } else if ( !aup ) { return -1; } else if ( !bup ) { return 1; } // Otherwise they're somewhere else in the tree so we need // to build up a full list of the parentNodes for comparison while ( cur ) { ap.unshift( cur ); cur = cur.parentNode; } cur = bup; while ( cur ) { bp.unshift( cur ); cur = cur.parentNode; } al = ap.length; bl = bp.length; // Start walking down the tree looking for a discrepancy for ( var i = 0; i < al && i < bl; i++ ) { if ( ap[i] !== bp[i] ) { return siblingCheck( ap[i], bp[i] ); } } // We ended someplace up the tree so do a sibling check return i === al ? siblingCheck( a, bp[i], -1 ) : siblingCheck( ap[i], b, 1 ); }; siblingCheck = function( a, b, ret ) { if ( a === b ) { return ret; } var cur = a.nextSibling; while ( cur ) { if ( cur === b ) { return -1; } cur = cur.nextSibling; } return 1; }; } // Check to see if the browser returns elements by name when // querying by getElementById (and provide a workaround) (function(){ // We're going to inject a fake input element with a specified name var form = document.createElement("div"), id = "script" + (new Date()).getTime(), root = document.documentElement; form.innerHTML = ""; // Inject it into the root element, check its status, and remove it quickly root.insertBefore( form, root.firstChild ); // The workaround has to do additional checks after a getElementById // Which slows things down for other browsers (hence the branching) if ( document.getElementById( id ) ) { Expr.find.ID = function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; } }; Expr.filter.ID = function( elem, match ) { var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); return elem.nodeType === 1 && node && node.nodeValue === match; }; } root.removeChild( form ); // release memory in IE root = form = null; })(); (function(){ // Check to see if the browser returns only elements // when doing getElementsByTagName("*") // Create a fake element var div = document.createElement("div"); div.appendChild( document.createComment("") ); // Make sure no comments are found if ( div.getElementsByTagName("*").length > 0 ) { Expr.find.TAG = function( match, context ) { var results = context.getElementsByTagName( match[1] ); // Filter out possible comments if ( match[1] === "*" ) { var tmp = []; for ( var i = 0; results[i]; i++ ) { if ( results[i].nodeType === 1 ) { tmp.push( results[i] ); } } results = tmp; } return results; }; } // Check to see if an attribute returns normalized href attributes div.innerHTML = ""; if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && div.firstChild.getAttribute("href") !== "#" ) { Expr.attrHandle.href = function( elem ) { return elem.getAttribute( "href", 2 ); }; } // release memory in IE div = null; })(); if ( document.querySelectorAll ) { (function(){ var oldSizzle = Sizzle, div = document.createElement("div"), id = "__sizzle__"; div.innerHTML = "

"; // Safari can't handle uppercase or unicode characters when // in quirks mode. if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { return; } Sizzle = function( query, context, extra, seed ) { context = context || document; // Only use querySelectorAll on non-XML documents // (ID selectors don't work in non-HTML documents) if ( !seed && !Sizzle.isXML(context) ) { // See if we find a selector to speed up var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { // Speed-up: Sizzle("TAG") if ( match[1] ) { return makeArray( context.getElementsByTagName( query ), extra ); // Speed-up: Sizzle(".CLASS") } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { return makeArray( context.getElementsByClassName( match[2] ), extra ); } } if ( context.nodeType === 9 ) { // Speed-up: Sizzle("body") // The body element only exists once, optimize finding it if ( query === "body" && context.body ) { return makeArray( [ context.body ], extra ); // Speed-up: Sizzle("#ID") } else if ( match && match[3] ) { var elem = context.getElementById( match[3] ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 if ( elem && elem.parentNode ) { // Handle the case where IE and Opera return items // by name instead of ID if ( elem.id === match[3] ) { return makeArray( [ elem ], extra ); } } else { return makeArray( [], extra ); } } try { return makeArray( context.querySelectorAll(query), extra ); } catch(qsaError) {} // qSA works strangely on Element-rooted queries // We can work around this by specifying an extra ID on the root // and working up from there (Thanks to Andrew Dupont for the technique) // IE 8 doesn't work on object elements } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { var oldContext = context, old = context.getAttribute( "id" ), nid = old || id, hasParent = context.parentNode, relativeHierarchySelector = /^\s*[+~]/.test( query ); if ( !old ) { context.setAttribute( "id", nid ); } else { nid = nid.replace( /'/g, "\\$&" ); } if ( relativeHierarchySelector && hasParent ) { context = context.parentNode; } try { if ( !relativeHierarchySelector || hasParent ) { return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); } } catch(pseudoError) { } finally { if ( !old ) { oldContext.removeAttribute( "id" ); } } } } return oldSizzle(query, context, extra, seed); }; for ( var prop in oldSizzle ) { Sizzle[ prop ] = oldSizzle[ prop ]; } // release memory in IE div = null; })(); } (function(){ var html = document.documentElement, matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; if ( matches ) { // Check to see if it's possible to do matchesSelector // on a disconnected node (IE 9 fails this) var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), pseudoWorks = false; try { // This should fail with an exception // Gecko does not error, returns false instead matches.call( document.documentElement, "[test!='']:sizzle" ); } catch( pseudoError ) { pseudoWorks = true; } Sizzle.matchesSelector = function( node, expr ) { // Make sure that attribute selectors are quoted expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); if ( !Sizzle.isXML( node ) ) { try { if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { var ret = matches.call( node, expr ); // IE 9's matchesSelector returns false on disconnected nodes if ( ret || !disconnectedMatch || // As well, disconnected nodes are said to be in a document // fragment in IE 9, so check for that node.document && node.document.nodeType !== 11 ) { return ret; } } } catch(e) {} } return Sizzle(expr, null, null, [node]).length > 0; }; } })(); (function(){ var div = document.createElement("div"); div.innerHTML = "
"; // Opera can't find a second classname (in 9.6) // Also, make sure that getElementsByClassName actually exists if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { return; } // Safari caches class attributes, doesn't catch changes (in 3.2) div.lastChild.className = "e"; if ( div.getElementsByClassName("e").length === 1 ) { return; } Expr.order.splice(1, 0, "CLASS"); Expr.find.CLASS = function( match, context, isXML ) { if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { return context.getElementsByClassName(match[1]); } }; // release memory in IE div = null; })(); function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { var match = false; elem = elem[dir]; while ( elem ) { if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 && !isXML ){ elem[ expando ] = doneName; elem.sizset = i; } if ( elem.nodeName.toLowerCase() === cur ) { match = elem; break; } elem = elem[dir]; } checkSet[i] = match; } } } function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { var match = false; elem = elem[dir]; while ( elem ) { if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 ) { if ( !isXML ) { elem[ expando ] = doneName; elem.sizset = i; } if ( typeof cur !== "string" ) { if ( elem === cur ) { match = true; break; } } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { match = elem; break; } } elem = elem[dir]; } checkSet[i] = match; } } } if ( document.documentElement.contains ) { Sizzle.contains = function( a, b ) { return a !== b && (a.contains ? a.contains(b) : true); }; } else if ( document.documentElement.compareDocumentPosition ) { Sizzle.contains = function( a, b ) { return !!(a.compareDocumentPosition(b) & 16); }; } else { Sizzle.contains = function() { return false; }; } Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist // (such as loading iframes in IE - #4833) var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; }; var posProcess = function( selector, context, seed ) { var match, tmpSet = [], later = "", root = context.nodeType ? [context] : context; // Position selectors must be done after the filter // And so must :not(positional) so we move all PSEUDOs to the end while ( (match = Expr.match.PSEUDO.exec( selector )) ) { later += match[0]; selector = selector.replace( Expr.match.PSEUDO, "" ); } selector = Expr.relative[selector] ? selector + "*" : selector; for ( var i = 0, l = root.length; i < l; i++ ) { Sizzle( selector, root[i], tmpSet, seed ); } return Sizzle.filter( later, tmpSet ); }; // EXPOSE // Override sizzle attribute retrieval Sizzle.attr = jQuery.attr; Sizzle.selectors.attrMap = {}; jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.filters; jQuery.unique = Sizzle.uniqueSort; jQuery.text = Sizzle.getText; jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; })(); var runtil = /Until$/, rparentsprev = /^(?:parents|prevUntil|prevAll)/, // Note: This RegExp should be improved, or likely pulled from Sizzle rmultiselector = /,/, isSimple = /^.[^:#\[\.,]*$/, slice = Array.prototype.slice, POS = jQuery.expr.match.globalPOS, // methods guaranteed to produce a unique set when starting from a unique set guaranteedUnique = { children: true, contents: true, next: true, prev: true }; jQuery.fn.extend({ find: function( selector ) { var self = this, i, l; if ( typeof selector !== "string" ) { return jQuery( selector ).filter(function() { for ( i = 0, l = self.length; i < l; i++ ) { if ( jQuery.contains( self[ i ], this ) ) { return true; } } }); } var ret = this.pushStack( "", "find", selector ), length, n, r; for ( i = 0, l = this.length; i < l; i++ ) { length = ret.length; jQuery.find( selector, this[i], ret ); if ( i > 0 ) { // Make sure that the results are unique for ( n = length; n < ret.length; n++ ) { for ( r = 0; r < length; r++ ) { if ( ret[r] === ret[n] ) { ret.splice(n--, 1); break; } } } } } return ret; }, has: function( target ) { var targets = jQuery( target ); return this.filter(function() { for ( var i = 0, l = targets.length; i < l; i++ ) { if ( jQuery.contains( this, targets[i] ) ) { return true; } } }); }, not: function( selector ) { return this.pushStack( winnow(this, selector, false), "not", selector); }, filter: function( selector ) { return this.pushStack( winnow(this, selector, true), "filter", selector ); }, is: function( selector ) { return !!selector && ( typeof selector === "string" ? // If this is a positional selector, check membership in the returned set // so $("p:first").is("p:last") won't return true for a doc with two "p". POS.test( selector ) ? jQuery( selector, this.context ).index( this[0] ) >= 0 : jQuery.filter( selector, this ).length > 0 : this.filter( selector ).length > 0 ); }, closest: function( selectors, context ) { var ret = [], i, l, cur = this[0]; // Array (deprecated as of jQuery 1.7) if ( jQuery.isArray( selectors ) ) { var level = 1; while ( cur && cur.ownerDocument && cur !== context ) { for ( i = 0; i < selectors.length; i++ ) { if ( jQuery( cur ).is( selectors[ i ] ) ) { ret.push({ selector: selectors[ i ], elem: cur, level: level }); } } cur = cur.parentNode; level++; } return ret; } // String var pos = POS.test( selectors ) || typeof selectors !== "string" ? jQuery( selectors, context || this.context ) : 0; for ( i = 0, l = this.length; i < l; i++ ) { cur = this[i]; while ( cur ) { if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { ret.push( cur ); break; } else { cur = cur.parentNode; if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { break; } } } } ret = ret.length > 1 ? jQuery.unique( ret ) : ret; return this.pushStack( ret, "closest", selectors ); }, // Determine the position of an element within // the matched set of elements index: function( elem ) { // No argument, return index in parent if ( !elem ) { return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; } // index in selector if ( typeof elem === "string" ) { return jQuery.inArray( this[0], jQuery( elem ) ); } // Locate the position of the desired element return jQuery.inArray( // If it receives a jQuery object, the first element is used elem.jquery ? elem[0] : elem, this ); }, add: function( selector, context ) { var set = typeof selector === "string" ? jQuery( selector, context ) : jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), all = jQuery.merge( this.get(), set ); return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? all : jQuery.unique( all ) ); }, andSelf: function() { return this.add( this.prevObject ); } }); // A painfully simple check to see if an element is disconnected // from a document (should be improved, where feasible). function isDisconnected( node ) { return !node || !node.parentNode || node.parentNode.nodeType === 11; } jQuery.each({ parent: function( elem ) { var parent = elem.parentNode; return parent && parent.nodeType !== 11 ? parent : null; }, parents: function( elem ) { return jQuery.dir( elem, "parentNode" ); }, parentsUntil: function( elem, i, until ) { return jQuery.dir( elem, "parentNode", until ); }, next: function( elem ) { return jQuery.nth( elem, 2, "nextSibling" ); }, prev: function( elem ) { return jQuery.nth( elem, 2, "previousSibling" ); }, nextAll: function( elem ) { return jQuery.dir( elem, "nextSibling" ); }, prevAll: function( elem ) { return jQuery.dir( elem, "previousSibling" ); }, nextUntil: function( elem, i, until ) { return jQuery.dir( elem, "nextSibling", until ); }, prevUntil: function( elem, i, until ) { return jQuery.dir( elem, "previousSibling", until ); }, siblings: function( elem ) { return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); }, children: function( elem ) { return jQuery.sibling( elem.firstChild ); }, contents: function( elem ) { return jQuery.nodeName( elem, "iframe" ) ? elem.contentDocument || elem.contentWindow.document : jQuery.makeArray( elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); if ( !runtil.test( name ) ) { selector = until; } if ( selector && typeof selector === "string" ) { ret = jQuery.filter( selector, ret ); } ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { ret = ret.reverse(); } return this.pushStack( ret, name, slice.call( arguments ).join(",") ); }; }); jQuery.extend({ filter: function( expr, elems, not ) { if ( not ) { expr = ":not(" + expr + ")"; } return elems.length === 1 ? jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : jQuery.find.matches(expr, elems); }, dir: function( elem, dir, until ) { var matched = [], cur = elem[ dir ]; while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { if ( cur.nodeType === 1 ) { matched.push( cur ); } cur = cur[dir]; } return matched; }, nth: function( cur, result, dir, elem ) { result = result || 1; var num = 0; for ( ; cur; cur = cur[dir] ) { if ( cur.nodeType === 1 && ++num === result ) { break; } } return cur; }, sibling: function( n, elem ) { var r = []; for ( ; n; n = n.nextSibling ) { if ( n.nodeType === 1 && n !== elem ) { r.push( n ); } } return r; } }); // Implement the identical functionality for filter and not function winnow( elements, qualifier, keep ) { // Can't pass null or undefined to indexOf in Firefox 4 // Set to 0 to skip string check qualifier = qualifier || 0; if ( jQuery.isFunction( qualifier ) ) { return jQuery.grep(elements, function( elem, i ) { var retVal = !!qualifier.call( elem, i, elem ); return retVal === keep; }); } else if ( qualifier.nodeType ) { return jQuery.grep(elements, function( elem, i ) { return ( elem === qualifier ) === keep; }); } else if ( typeof qualifier === "string" ) { var filtered = jQuery.grep(elements, function( elem ) { return elem.nodeType === 1; }); if ( isSimple.test( qualifier ) ) { return jQuery.filter(qualifier, filtered, !keep); } else { qualifier = jQuery.filter( qualifier, filtered ); } } return jQuery.grep(elements, function( elem, i ) { return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; }); } function createSafeFragment( document ) { var list = nodeNames.split( "|" ), safeFrag = document.createDocumentFragment(); if ( safeFrag.createElement ) { while ( list.length ) { safeFrag.createElement( list.pop() ); } } return safeFrag; } var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, rleadingWhitespace = /^\s+/, rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, rtagName = /<([\w:]+)/, rtbody = /]", "i"), // checked="checked" or checked rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, rscriptType = /\/(java|ecma)script/i, rcleanScript = /^\s*", "" ], legend: [ 1, "
", "
" ], thead: [ 1, "", "
" ], tr: [ 2, "", "
" ], td: [ 3, "", "
" ], col: [ 2, "", "
" ], area: [ 1, "", "" ], _default: [ 0, "", "" ] }, safeFragment = createSafeFragment( document ); wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; // IE can't serialize and

Loading...

Please wait.
Downloads

Delete selected downloads?

How to delete selected downloads?

Delete
DownloadsDeleteHelp
History

Delete selected history records?

How to delete selected history records?

Selected records will be deleted from history. All files remain on disk.

Selected records will be deleted from history. For failed downloads all downloaded files will be deleted.

Permanent deleting of hidden records may have an impact on duplicate check and is not recommended.

Delete
HistoryDeleteHelp
History

Download selected nzbs again?

All downloaded files will be deleted and the nzbs will be downloaded again from scratch.

Download Again
History

Mark selected history records as success?

Marking has an effect on duplicate handling and RSS.
Records marked as success considered successfully downloaded and processed. This is useful for downloads repaired or processed outside of NZBGet. Duplicates with higher duplicate scores may be downloaded in the future.

Mark Success
History

Mark selected history records as good?

Marking has an effect on duplicate handling and RSS.
For titles marked as good no more duplicates will be downloaded, even with higher duplicate score. Existing dupe-backups will be removed from history.

Mark Good
History

Mark selected history records as Bad?

Marking has an effect on duplicate handling and RSS.
If dupe-backups exist in the history the best of them will be moved to queue for download. Otherwise the title will be watched and downloaded when it becomes available.

Mark Bad
Messages

Clear Messages?

All log records will be deleted from screen buffer. The log-file remains on disk unchanged.

Clear
Configuration

Delete ?

Delete
Reset

Reset custom counter for all news servers?

Last reset on Fri Apr 04 2014 09:32:24.

Reset
Execute Script

Please confrim execution of the script with command "".

This script command is marked as dangerous and requires a confirmation.

Execute Script
Not yet implemented
Reload (soft-restart)

Reload NZBGet?

The configuration will be reloaded and the program will be reinitialized.

Reload

Reloading NZBGet

Stopping all activities and reloading...

Should this take too long:
  • Try to refresh the page in browser manually.
  • If you changed remote control settings (IP, Port, Password) update the URL in browser accordingly.
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
Files Submitted
Scan Completed
Speed Limit Changed
Saved
Changed
Pausing
Paused
Resumed
Deleted
Canceled
Moved
Merged
Splitted
Could not split. Check messages for errors.
Cannot split. Some of selected files are already (partially) downloaded.
Please select records first
Please select at least two records
Post-processing-downloads cannot be edited
URLs cannot be merged or paused
Sorted
Please select records first
Deleted
Cleared
Deleted
Changed
Returned to Queue
Post-Processing
Retrying failed articles
Saved
Please select records first
Cannot mark URL-records
Marked
Cannot post-process URL- or hidden records
Cannot redownload hidden records
Fetching new items
No records selected
Fetching items
Nothing to save
No changes have been made
Could not save configuration in


Please check file permissions

Reload command has been sent

Please wait few seconds, then refresh the page.
Please select at least one section
Restoring settings...
Could not start update script
Debug
Incorrect period
Volume reset
Testing connection...
Connection successful
Please type a search string first
nzbget-19.1/windows/0000755000175000017500000000000013130203062014231 5ustar andreasandreasnzbget-19.1/scripts/0000755000175000017500000000000013130203062014226 5ustar andreasandreasnzbget-19.1/scripts/EMail.py0000755000175000017500000002405613130203062015601 0ustar andreasandreas#!/usr/bin/env python # # E-Mail post-processing script for NZBGet # # Copyright (C) 2013-2017 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, see . # ############################################################################## ### NZBGET POST-PROCESSING SCRIPT ### # Send E-Mail notification. # # This script sends E-Mail notification when the job is done. # # NOTE: This script requires Python to be installed on your system. ############################################################################## ### OPTIONS ### # When to send the message (Always, OnFailure). #SendMail=Always # Email address you want this email to be sent from. #From="NZBGet" # Email address you want this email to be sent to. # # Multiple addresses can be separated with comma. #To=myaccount@gmail.com # SMTP server host. #Server=smtp.gmail.com # SMTP server port (1-65535). #Port=25 # Secure communication using TLS/SSL (yes, no, force). # no - plain text communication (insecure); # yes - switch to secure session using StartTLS command; # force - start secure session on encrypted socket. #Encryption=yes # SMTP server user name, if required. #Username=myaccount # SMTP server password, if required. #Password=mypass # To check connection parameters click the button. #ConnectionTest@Send Test E-Mail # 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). #FileList=yes # Append broken-log to the message (yes, no). # # Add the content of file _brokenlog.txt. This file contains the list of damaged # files and the result of par-check/repair. For successful downloads the broken-log # is usually deleted by cleanup-script and therefore is not sent. #BrokenLog=yes # Append nzb log to the message (Always, Never, OnFailure). # # Add the download and post-processing log of active job. #NzbLog=OnFailure ### NZBGET POST-PROCESSING SCRIPT ### ############################################################################## import os import sys import datetime import smtplib from email.mime.text import MIMEText try: from xmlrpclib import ServerProxy # python 2 except ImportError: from xmlrpc.client import ServerProxy # python 3 # Exit codes used by NZBGet POSTPROCESS_SUCCESS=93 POSTPROCESS_ERROR=94 POSTPROCESS_NONE=95 # Check if the script is called from nzbget 15.0 or later if not 'NZBOP_NZBLOG' in os.environ: print('*** NZBGet post-processing script ***') print('This script is supposed to be called from nzbget (15.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') 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) # Check if the script is executed from settings page with a custom command command = os.environ.get('NZBCP_COMMAND') test_mode = command == 'ConnectionTest' if command != None and not test_mode: print('[ERROR] Invalid command ' + command) sys.exit(POSTPROCESS_ERROR) status = os.environ.get('NZBPP_STATUS') if not test_mode else 'SUCCESS/ALL' total_status = os.environ.get('NZBPP_TOTALSTATUS') if not test_mode else 'SUCCESS' # 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.get('NZBPP_SCRIPTSTATUS') == 'FAILURE': total_status = 'WARNING' status = 'WARNING/SCRIPT' success = total_status == 'SUCCESS' if success and os.environ.get('NZBPO_SENDMAIL') == 'OnFailure' and not test_mode: print('[INFO] Skipping sending of message for successful download') sys.exit(POSTPROCESS_NONE) if success: subject = 'Success for "%s"' % (os.environ.get('NZBPP_NZBNAME', 'Test download')) text = 'Download of "%s" has successfully completed.' % (os.environ.get('NZBPP_NZBNAME', 'Test download')) else: subject = 'Failure for "%s"' % (os.environ['NZBPP_NZBNAME']) text = 'Download of "%s" has failed.' % (os.environ['NZBPP_NZBNAME']) text += '\nStatus: %s' % status if (os.environ.get('NZBPO_STATISTICS') == 'yes' or \ os.environ.get('NZBPO_NZBLOG') == 'Always' or \ (os.environ.get('NZBPO_NZBLOG') == 'OnFailure' and not success)) and \ not test_mode: # To get statistics or the post-processing log we connect to NZBGet via XML-RPC. # For more info visit http://nzbget.net/api # 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. host = os.environ['NZBOP_CONTROLIP']; port = os.environ['NZBOP_CONTROLPORT']; username = os.environ['NZBOP_CONTROLUSERNAME']; password = os.environ['NZBOP_CONTROLPASSWORD']; if host == '0.0.0.0': host = '127.0.0.1' # Build an URL for XML-RPC requests rpcUrl = 'http://%s:%s@%s:%s/xmlrpc' % (username, password, host, port); # Create remote server object server = ServerProxy(rpcUrl) if os.environ.get('NZBPO_STATISTICS') == 'yes' and not test_mode: # 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' and not test_mode: 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' and not test_mode: 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_NZBLOG') == 'Always' or \ (os.environ.get('NZBPO_NZBLOG') == 'OnFailure' and not success)) and \ not test_mode: # To get the item log we connect to NZBGet via XML-RPC and call # method "loadlog", which returns the log for a given nzb item. # For more info visit http://nzbget.net/api # Call remote method 'loadlog' nzbid = int(os.environ['NZBPP_NZBID']) log = server.loadlog(nzbid, 0, 10000) # Now iterate through entries and save them to message text if len(log) > 0: text += '\n\nNzb-log:'; for entry in log: text += '\n%s\t%s\t%s' % (entry['Kind'], datetime.datetime.fromtimestamp(int(entry['Time'])), entry['Text']) # Create message msg = MIMEText(text) 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') sys.stdout.flush() try: if os.environ['NZBPO_ENCRYPTION'] == 'force': smtp = smtplib.SMTP_SSL(os.environ['NZBPO_SERVER'], os.environ['NZBPO_PORT']) else: smtp = smtplib.SMTP(os.environ['NZBPO_SERVER'], os.environ['NZBPO_PORT']) if os.environ['NZBPO_ENCRYPTION'] == 'yes': smtp.starttls() if os.environ['NZBPO_USERNAME'] != '' and os.environ['NZBPO_PASSWORD'] != '': smtp.login(os.environ['NZBPO_USERNAME'], os.environ['NZBPO_PASSWORD']) smtp.sendmail(os.environ['NZBPO_FROM'], os.environ['NZBPO_TO'].split(','), msg.as_string()) smtp.quit() except Exception as err: print('[ERROR] %s' % err) sys.exit(POSTPROCESS_ERROR) # All OK, returning exit status 'POSTPROCESS_SUCCESS' (int <93>) to let NZBGet know # that our script has successfully completed. sys.exit(POSTPROCESS_SUCCESS) nzbget-19.1/scripts/Logger.py0000755000175000017500000000634113130203062016026 0ustar andreasandreas#!/usr/bin/env python # # Logger post-processing script for NZBGet # # Copyright (C) 2013-2016 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, see . # ############################################################################## ### NZBGET POST-PROCESSING SCRIPT ### # Save nzb log into a file. # # This script saves the download and post-processing log of nzb-file # into file _nzblog.txt in the destination directory. # # NOTE: This script requires Python to be installed on your system. ### NZBGET POST-PROCESSING SCRIPT ### ############################################################################## import os import sys import datetime try: from xmlrpclib import ServerProxy # python 2 except ImportError: from xmlrpc.client import ServerProxy # python 3 # Exit codes used by NZBGet POSTPROCESS_SUCCESS=93 POSTPROCESS_NONE=95 POSTPROCESS_ERROR=94 # Check if the script is called from nzbget 15.0 or later if not 'NZBOP_NZBLOG' in os.environ: print('*** NZBGet post-processing script ***') print('This script is supposed to be called from nzbget (15.0 or later).') sys.exit(POSTPROCESS_ERROR) if not os.path.exists(os.environ['NZBPP_DIRECTORY']): print('Destination directory doesn\'t exist, exiting') sys.exit(POSTPROCESS_NONE) # To get the item log we connect to NZBGet via XML-RPC and call # method "loadlog", which returns the log for a given nzb item. # 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. host = os.environ['NZBOP_CONTROLIP']; port = os.environ['NZBOP_CONTROLPORT']; username = os.environ['NZBOP_CONTROLUSERNAME']; password = os.environ['NZBOP_CONTROLPASSWORD']; if host == '0.0.0.0': host = '127.0.0.1' # Build an URL for XML-RPC requests rpcUrl = 'http://%s:%s@%s:%s/xmlrpc' % (username, password, host, port); # Create remote server object server = ServerProxy(rpcUrl) # Call remote method 'loadlog' nzbid = int(os.environ['NZBPP_NZBID']) log = server.loadlog(nzbid, 0, 10000) # Now iterate through entries and save them to the output file if len(log) > 0: f = open('%s/_nzblog.txt' % os.environ['NZBPP_DIRECTORY'], 'wb') for entry in log: f.write((u'%s\t%s\t%s\n' % (entry['Kind'], datetime.datetime.fromtimestamp(int(entry['Time'])), entry['Text'])).encode('utf8')) f.close() # All OK, returning exit status 'POSTPROCESS_SUCCESS' (int <93>) to let NZBGet know # that our script has successfully completed. sys.exit(POSTPROCESS_SUCCESS) nzbget-19.1/configure.ac0000644000175000017500000004615313130203062015036 0ustar andreasandreas# # This file is part of nzbget. See . # # Copyright (C) 2008-2017 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, see . # # -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ(2.59) AC_INIT(nzbget, 19.1, hugbug@users.sourceforge.net) AC_CONFIG_AUX_DIR(posix) AC_CANONICAL_TARGET AM_INIT_AUTOMAKE([foreign]) AC_CONFIG_SRCDIR([daemon/main/nzbget.cpp]) AC_CONFIG_HEADERS([config.h]) AM_MAINTAINER_MODE m4_include([posix/ax_cxx_compile_stdcxx.m4]) dnl dnl Check for programs. dnl AC_PROG_CXX AC_PATH_PROG(TAR, tar, $FALSE) AC_PATH_PROG(MAKE, make, $FALSE) AC_PROG_INSTALL dnl dnl Do all tests with c++ compiler. dnl AC_LANG(C++) dnl dnl Determine compiler switches to support C++14 standard. dnl AC_MSG_CHECKING(whether to test compiler features) AC_ARG_ENABLE(cpp-check, [AS_HELP_STRING([--disable-cpp-check], [disable check for C++14 compiler features])], [ ENABLECPPCHECK=$enableval ], [ ENABLECPPCHECK=yes] ) AC_MSG_RESULT($ENABLECPPCHECK) if test "$ENABLECPPCHECK" = "yes"; then AX_CXX_COMPILE_STDCXX(14,,[optional]) if test "$HAVE_CXX14" != "1"; then AC_MSG_ERROR("A compiler with support for C++14 language features is required. For details visit http://nzbget.net/cpp14") fi fi dnl dnl Checks for header files. dnl AC_CHECK_HEADERS(sys/prctl.h) AC_CHECK_HEADERS(regex.h) dnl dnl Check for libs dnl AC_SEARCH_LIBS([pthread_create], [pthread]) AC_SEARCH_LIBS([socket], [socket]) AC_SEARCH_LIBS([inet_addr], [nsl]) AC_SEARCH_LIBS([hstrerror], [resolv]) dnl dnl Getopt dnl AC_CHECK_FUNC(getopt_long, [AC_DEFINE([HAVE_GETOPT_LONG], 1, [Define to 1 if getopt_long is supported])],) dnl dnl fsync dnl AC_CHECK_FUNC(fdatasync, [AC_DEFINE([HAVE_FDATASYNC], 1, [Define to 1 if fdatasync is supported])],) AC_CHECK_DECL(F_FULLFSYNC, [AC_DEFINE([HAVE_FULLFSYNC], 1, [Define to 1 if F_FULLFSYNC is supported])],,[#include ]) dnl dnl use 64-Bits for file sizes dnl AC_SYS_LARGEFILE dnl dnl check ctime_r dnl AC_MSG_CHECKING(for ctime_r) AC_TRY_COMPILE( [#include ], [ time_t clock; char buf[26]; ctime_r(&clock, buf, 26); ], AC_MSG_RESULT([[yes, and it takes 3 arguments]]) FOUND="yes" AC_DEFINE([HAVE_CTIME_R_3], 1, [Define to 1 if ctime_r takes 3 arguments]), FOUND="no") if test "$FOUND" = "no"; then AC_TRY_COMPILE( [#include ], [ time_t clock; char buf[26]; ctime_r(&clock, buf); ], AC_MSG_RESULT([[yes, and it takes 2 arguments]]) FOUND="yes" AC_DEFINE([HAVE_CTIME_R_2], 1, [Define to 1 if ctime_r takes 2 arguments]), FOUND="no") fi if test "$FOUND" = "no"; then AC_MSG_RESULT([no]) AC_MSG_ERROR("function ctime_r not found") fi dnl dnl check getaddrinfo dnl AC_CHECK_FUNC(getaddrinfo, FOUND="yes" [AC_DEFINE([HAVE_GETADDRINFO], 1, [Define to 1 if getaddrinfo is supported])] AC_SEARCH_LIBS([getaddrinfo], [nsl]), FOUND="no") dnl dnl check gethostbyname_r, if getaddrinfo is not available dnl if test "$FOUND" = "no"; then AC_MSG_CHECKING(for gethostbyname_r) AC_TRY_COMPILE( [#include ], [ char* szHost; struct hostent hinfobuf; char* strbuf; int h_errnop; struct hostent* hinfo = gethostbyname_r(szHost, &hinfobuf, strbuf, 1024, &h_errnop); ], AC_MSG_RESULT([[yes, and it takes 5 arguments]]) FOUND="yes" AC_DEFINE([HAVE_GETHOSTBYNAME_R_5], 1, [Define to 1 if gethostbyname_r takes 5 arguments]), FOUND="no") if test "$FOUND" = "no"; then AC_TRY_COMPILE( [#include ], [ char* szHost; struct hostent* hinfo; struct hostent hinfobuf; char* strbuf; int h_errnop; int err = gethostbyname_r(szHost, &hinfobuf, strbuf, 1024, &hinfo, &h_errnop); ], AC_MSG_RESULT([[yes, and it takes 6 arguments]]) FOUND="yes" AC_DEFINE([HAVE_GETHOSTBYNAME_R_6], 1, [Define to 1 if gethostbyname_r takes 6 arguments]), FOUND="no") fi if test "$FOUND" = "no"; then AC_TRY_COMPILE( [#include ], [ char* szHost; struct hostent hinfo; struct hostent_data hinfobuf; int err = gethostbyname_r(szHost, &hinfo, &hinfobuf); ], AC_MSG_RESULT([[yes, and it takes 3 arguments]]) FOUND="yes" AC_DEFINE([HAVE_GETHOSTBYNAME_R_3], 1, [Define to 1 if gethostbyname_r takes 3 arguments]), AC_MSG_RESULT([[no]]) FOUND="no") fi if test "$FOUND" = "yes"; then AC_DEFINE([HAVE_GETHOSTBYNAME_R], 1, [Define to 1 if gethostbyname_r is supported]) AC_SEARCH_LIBS([gethostbyname_r], [nsl]) fi fi dnl dnl Determine what socket length (socklen_t) data type is dnl AC_MSG_CHECKING([for type of socket length (socklen_t)]) AC_TRY_COMPILE([ #include #include #include ],[ (void)getsockopt (1, 1, 1, NULL, (socklen_t*)NULL)],[ AC_MSG_RESULT(socklen_t) SOCKLEN_T=socklen_t],[ AC_TRY_COMPILE([ #include #include #include ],[ (void)getsockopt (1, 1, 1, NULL, (size_t*)NULL)],[ AC_MSG_RESULT(size_t) SOCKLEN_T=size_t],[ AC_TRY_COMPILE([ #include #include #include ],[ (void)getsockopt (1, 1, 1, NULL, (int*)NULL)],[ AC_MSG_RESULT(int) SOCKLEN_T=int],[ AC_MSG_WARN(could not determine) SOCKLEN_T=int])])]) AC_DEFINE_UNQUOTED(SOCKLEN_T, $SOCKLEN_T, [Determine what socket length (socklen_t) data type is]) dnl dnl Dir-browser's snapshot dnl AC_MSG_CHECKING(whether dir-browser snapshot workaround is needed) if test "$target_vendor" == "apple"; then AC_MSG_RESULT([[yes]]) AC_DEFINE([DIRBROWSER_SNAPSHOT], 1, [Define to 1 if deleting of files during reading of directory is not properly supported by OS]) else AC_MSG_RESULT([[no]]) fi dnl dnl check cpu cores via sysconf dnl AC_MSG_CHECKING(for cpu cores via sysconf) AC_TRY_COMPILE( [#include ], [ int a = _SC_NPROCESSORS_ONLN; ], FOUND="yes" AC_MSG_RESULT([[yes]]) AC_DEFINE([HAVE_SC_NPROCESSORS_ONLN], 1, [Define to 1 if _SC_NPROCESSORS_ONLN is present in unistd.h]), FOUND="no") dnl dnl checks for libxml2 includes and libraries. dnl AC_ARG_WITH(libxml2_includes, [AS_HELP_STRING([--with-libxml2-includes=DIR], [libxml2 include directory])], [CPPFLAGS="${CPPFLAGS} -I${withval}"] [INCVAL="yes"], [INCVAL="no"]) AC_ARG_WITH(libxml2_libraries, [AS_HELP_STRING([--with-libxml2-libraries=DIR], [libxml2 library directory])], [LDFLAGS="${LDFLAGS} -L${withval}"] [LIBVAL="yes"], [LIBVAL="no"]) if test "$INCVAL" = "no" -o "$LIBVAL" = "no"; then PKG_CHECK_MODULES(libxml2, libxml-2.0, [LIBS="${LIBS} $libxml2_LIBS"] [CPPFLAGS="${CPPFLAGS} $libxml2_CFLAGS"], AC_MSG_ERROR("libxml2 library not found")) fi AC_CHECK_HEADER(libxml/tree.h,, AC_MSG_ERROR("libxml2 header files not found")) AC_SEARCH_LIBS([xmlNewNode], [xml2], , AC_MSG_ERROR("libxml2 library not found")) dnl dnl Use curses. Deafult: yes dnl AC_MSG_CHECKING(whether to use curses) AC_ARG_ENABLE(curses, [AS_HELP_STRING([--disable-curses], [do not use curses (removes dependency from curses-library)])], [USECURSES=$enableval], [USECURSES=yes] ) AC_MSG_RESULT($USECURSES) if test "$USECURSES" = "yes"; then AC_ARG_WITH(libcurses_includes, [AS_HELP_STRING([--with-libcurses-includes=DIR], [libcurses include directory])], [CPPFLAGS="${CPPFLAGS} -I${withval}"] [INCVAL="yes"], [INCVAL="no"]) AC_ARG_WITH(libcurses_libraries, [AS_HELP_STRING([--with-libcurses-libraries=DIR], [libcurses library directory])], [LDFLAGS="${LDFLAGS} -L${withval}"] [LIBVAL="yes"], [LIBVAL="no"]) if test "$INCVAL" = "no" -o "$LIBVAL" = "no"; then PKG_CHECK_MODULES(ncurses, ncurses, [LIBS="${LIBS} $ncurses_LIBS"] [CPPFLAGS="${CPPFLAGS} $ncurses_CFLAGS"], AC_MSG_ERROR("ncurses library not found")) fi AC_CHECK_HEADER(ncurses.h, FOUND=yes AC_DEFINE([HAVE_NCURSES_H],1,[Define to 1 if you have the header file.]), FOUND=no) if test "$FOUND" = "no"; then AC_CHECK_HEADER(ncurses/ncurses.h, FOUND=yes AC_DEFINE([HAVE_NCURSES_NCURSES_H],1,[Define to 1 if you have the header file.]), FOUND=no) fi if test "$FOUND" = "no"; then AC_CHECK_HEADER(curses.h, FOUND=yes AC_DEFINE([HAVE_CURSES_H],1,[Define to 1 if you have the header file.]), FOUND=no) fi if test "$FOUND" = "no"; then AC_MSG_ERROR([Couldn't find curses headers (ncurses.h or curses.h)]) fi AC_SEARCH_LIBS([refresh], [ncurses curses],, AC_ERROR([Couldn't find curses library])) AC_SEARCH_LIBS([nodelay], [ncurses curses tinfo],, AC_ERROR([Couldn't find curses library])) else AC_DEFINE([DISABLE_CURSES],1,[Define to 1 to not use curses]) fi dnl dnl Use par-checking. Deafult: yes. dnl AC_MSG_CHECKING(whether to include code for par-checking) AC_ARG_ENABLE(parcheck, [AS_HELP_STRING([--disable-parcheck], [do not include par-check/-repair-support])], [ ENABLEPARCHECK=$enableval ], [ ENABLEPARCHECK=yes] ) AC_MSG_RESULT($ENABLEPARCHECK) if test "$ENABLEPARCHECK" = "yes"; then dnl PAR2 checks. dnl dnl Checks for header files. AC_CHECK_HEADERS([endian.h] [getopt.h]) dnl Checks for typedefs, structures, and compiler characteristics. AC_TYPE_SIZE_T AC_C_BIGENDIAN AC_FUNC_FSEEKO dnl Checks for library functions. AC_CHECK_FUNCS([stricmp]) AC_CHECK_FUNCS([getopt]) AM_CONDITIONAL(WITH_PAR2, true) else AC_DEFINE([DISABLE_PARCHECK],1,[Define to 1 to disable par-verification and repair]) AM_CONDITIONAL(WITH_PAR2, false) fi dnl dnl Use TLS/SSL. Deafult: yes dnl AC_MSG_CHECKING(whether to use TLS/SSL) AC_ARG_ENABLE(tls, [AS_HELP_STRING([--disable-tls], [do not use TLS/SSL (removes dependency from TLS/SSL-libraries)])], [ USETLS=$enableval ], [ USETLS=yes] ) AC_MSG_RESULT($USETLS) if test "$USETLS" = "yes"; then AC_ARG_WITH(tlslib, [AS_HELP_STRING([--with-tlslib=(OpenSSL, GnuTLS)], [TLS/SSL library to use])], [TLSLIB="$withval"]) if test "$TLSLIB" != "GnuTLS" -a "$TLSLIB" != "OpenSSL" -a "$TLSLIB" != ""; then AC_MSG_ERROR([Invalid argument for option --with-tlslib]) fi if test "$TLSLIB" = "OpenSSL" -o "$TLSLIB" = ""; then AC_ARG_WITH(openssl_includes, [AS_HELP_STRING([--with-openssl-includes=DIR], [OpenSSL include directory])], [CPPFLAGS="${CPPFLAGS} -I${withval}"] [INCVAL="yes"], [INCVAL="no"]) AC_ARG_WITH(openssl_libraries, [AS_HELP_STRING([--with-openssl-libraries=DIR], [OpenSSL library directory])], [LDFLAGS="${LDFLAGS} -L${withval}"] [LIBVAL="yes"], [LIBVAL="no"]) if test "$INCVAL" = "no" -o "$LIBVAL" = "no"; then PKG_CHECK_MODULES([openssl], [openssl], [LIBS="${LIBS} $openssl_LIBS"] [CPPFLAGS="${CPPFLAGS} $openssl_CFLAGS"]) fi AC_CHECK_HEADER(openssl/ssl.h, FOUND=yes TLSHEADERS=yes, FOUND=no) if test "$FOUND" = "no" -a "$TLSLIB" = "OpenSSL"; then AC_MSG_ERROR([Couldn't find OpenSSL headers (ssl.h)]) fi if test "$FOUND" = "yes"; then AC_SEARCH_LIBS([ASN1_OBJECT_free], [crypto], AC_SEARCH_LIBS([SSL_CTX_new], [ssl], FOUND=yes, FOUND=no), FOUND=no) if test "$FOUND" = "no" -a "$TLSLIB" = "OpenSSL"; then AC_MSG_ERROR([Couldn't find OpenSSL library]) fi if test "$FOUND" = "yes"; then TLSLIB="OpenSSL" AC_DEFINE([HAVE_OPENSSL],1,[Define to 1 to use OpenSSL library for TLS/SSL-support and decryption.]) AC_SEARCH_LIBS([X509_check_host], [crypto], AC_DEFINE([HAVE_X509_CHECK_HOST],1,[Define to 1 if OpenSSL supports function "X509_check_host".])) fi fi fi if test "$TLSLIB" = "GnuTLS" -o "$TLSLIB" = ""; then AC_ARG_WITH(libgnutls_includes, [AS_HELP_STRING([--with-libgnutls-includes=DIR], [GnuTLS include directory])], [CPPFLAGS="${CPPFLAGS} -I${withval}"] [INCVAL="yes"], [INCVAL="no"]) AC_ARG_WITH(libgnutls_libraries, [AS_HELP_STRING([--with-libgnutls-libraries=DIR], [GnuTLS library directory])], [LDFLAGS="${LDFLAGS} -L${withval}"] [LIBVAL="yes"], [LIBVAL="no"]) if test "$INCVAL" = "no" -o "$LIBVAL" = "no"; then PKG_CHECK_MODULES([gnutls], [gnutls], [LIBS="${LIBS} $gnutls_LIBS"] [CPPFLAGS="${CPPFLAGS} $gnutls_CFLAGS"]) fi AC_CHECK_HEADER(gnutls/gnutls.h, FOUND=yes TLSHEADERS=yes, FOUND=no) if test "$FOUND" = "no" -a "$TLSLIB" = "GnuTLS"; then AC_MSG_ERROR([Couldn't find GnuTLS headers (gnutls.h)]) fi if test "$FOUND" = "yes"; then AC_SEARCH_LIBS([gnutls_global_init], [gnutls], FOUND=yes, FOUND=no) if test "$FOUND" = "yes"; then dnl gcrypt is optional AC_MSG_CHECKING([whether gcrypt is needed]) AC_TRY_COMPILE( [#include ] [#if GNUTLS_VERSION_NUMBER <= 0x020b00] [compile error] [#endif], [int a;], AC_MSG_RESULT([no]) GCRYPT=no, AC_MSG_RESULT([yes]) GCRYPT=yes) if test "$GCRYPT" = "yes"; then AC_CHECK_HEADER([gcrypt.h], AC_SEARCH_LIBS([gcry_control], [gnutls gcrypt], FOUND=yes, FOUND=no), FOUND=yes) fi fi if test "$FOUND" = "no" -a "$TLSLIB" = "GnuTLS"; then AC_MSG_ERROR([Couldn't find GnuTLS library]) fi if test "$FOUND" = "yes"; then TLSLIB="GnuTLS" AC_DEFINE([HAVE_LIBGNUTLS],1,[Define to 1 to use GnuTLS library for TLS/SSL-support.]) fi fi if test "$TLSLIB" = "GnuTLS"; then AC_ARG_WITH(libnettle_includes, [AS_HELP_STRING([--with-libnettle-includes=DIR], [Nettle include directory])], [CPPFLAGS="${CPPFLAGS} -I${withval}"] [INCVAL="yes"], [INCVAL="no"]) AC_ARG_WITH(libnettle_libraries, [AS_HELP_STRING([--with-libnettle-libraries=DIR], [Nettle library directory])], [LDFLAGS="${LDFLAGS} -L${withval}"] [LIBVAL="yes"], [LIBVAL="no"]) if test "$INCVAL" = "no" -o "$LIBVAL" = "no"; then PKG_CHECK_MODULES([nettle], [nettle], [LIBS="${LIBS} $nettle_LIBS"] [CPPFLAGS="${CPPFLAGS} $nettle_CFLAGS"]) fi AC_CHECK_HEADER(nettle/sha.h, FOUND=yes, FOUND=no) if test "$FOUND" = "no"; then AC_MSG_ERROR([Couldn't find Nettle headers (sha.h)]) fi AC_SEARCH_LIBS([nettle_pbkdf2_hmac_sha256], [nettle], FOUND=yes, FOUND=no) if test "$FOUND" = "no"; then AC_MSG_ERROR([Couldn't find Nettle library, required when using GnuTLS]) fi if test "$FOUND" = "yes"; then AC_DEFINE([HAVE_NETTLE],1,[Define to 1 to use Nettle library for decryption.]) fi fi fi if test "$TLSLIB" = ""; then if test "$TLSHEADERS" = ""; then AC_MSG_ERROR([Couldn't find neither OpenSSL nor GnuTLS headers (ssl.h or gnutls.h)]) else AC_MSG_ERROR([Couldn't find neither OpenSSL nor GnuTLS library]) fi fi else AC_DEFINE([DISABLE_TLS],1,[Define to 1 to not use TLS/SSL]) fi dnl dnl checks for zlib includes and libraries. dnl AC_MSG_CHECKING(whether to use gzip) AC_ARG_ENABLE(gzip, [AS_HELP_STRING([--disable-gzip], [disable gzip-compression/decompression (removes dependency from zlib-library)])], [USEZLIB=$enableval], [USEZLIB=yes] ) AC_MSG_RESULT($USEZLIB) if test "$USEZLIB" = "yes"; then AC_ARG_WITH(zlib_includes, [AS_HELP_STRING([--with-zlib-includes=DIR], [zlib include directory])], [CPPFLAGS="${CPPFLAGS} -I${withval}"] [INCVAL="yes"], [INCVAL="no"]) AC_ARG_WITH(zlib_libraries, [AS_HELP_STRING([--with-zlib-libraries=DIR], [zlib library directory])], [LDFLAGS="${LDFLAGS} -L${withval}"] [LIBVAL="yes"], [LIBVAL="no"]) if test "$INCVAL" = "no" -o "$LIBVAL" = "no"; then PKG_CHECK_MODULES([zlib], [zlib], [LIBS="${LIBS} $zlib_LIBS"] [CPPFLAGS="${CPPFLAGS} $zlib_CFLAGS"]) fi AC_CHECK_HEADER(zlib.h,, AC_MSG_ERROR("zlib header files not found")) AC_SEARCH_LIBS([deflateBound], [z], , AC_MSG_ERROR("zlib library not found")) else AC_DEFINE([DISABLE_GZIP],1,[Define to 1 to disable gzip-support]) fi dnl dnl Some Linux systems require an empty signal handler for SIGCHLD dnl in order for exit codes to be correctly delivered to parent process. dnl Some 32-Bit BSD systems however may not function properly if the handler is installed. dnl The default behavior is to install the handler. dnl AC_MSG_CHECKING(whether to use an empty SIGCHLD handler) AC_ARG_ENABLE(sigchld-handler, [AS_HELP_STRING([--disable-sigchld-handler], [do not use sigchld-handler (the disabling may be neccessary on 32-Bit BSD)])], [SIGCHLDHANDLER=$enableval], [SIGCHLDHANDLER=yes]) AC_MSG_RESULT($SIGCHLDHANDLER) if test "$SIGCHLDHANDLER" = "yes"; then AC_DEFINE([SIGCHLD_HANDLER], 1, [Define to 1 to install an empty signal handler for SIGCHLD]) fi dnl dnl Debugging. Default: no dnl AC_MSG_CHECKING(whether to include all debugging code) AC_ARG_ENABLE(debug, [AS_HELP_STRING([--enable-debug], [enable debugging])], [ ENABLEDEBUG=$enableval ], [ ENABLEDEBUG=no] ) AC_MSG_RESULT($ENABLEDEBUG) if test "$ENABLEDEBUG" = "yes"; then dnl dnl Begin of debugging code dnl AC_DEFINE([DEBUG],1,Define to 1 to include debug-code) dnl dnl check for __FUNCTION__ or __func__ macro dnl AC_MSG_CHECKING(for macro returning current function name) AC_TRY_COMPILE( [#include ], [printf("%s\n", __FUNCTION__);], AC_MSG_RESULT(__FUNCTION__) FUNCTION_MACRO_NAME=__FUNCTION__, AC_TRY_COMPILE([#include ], [printf("%s\n", __func__);], AC_MSG_RESULT(__func__) FUNCTION_MACRO_NAME=__func__, AC_MSG_RESULT(none))) if test "$FUNCTION_MACRO_NAME" != ""; then AC_DEFINE_UNQUOTED(FUNCTION_MACRO_NAME, $FUNCTION_MACRO_NAME, [Define to the name of macro which returns the name of function being compiled]) fi dnl dnl variadic macros dnl AC_MSG_CHECKING(for variadic macros) AC_COMPILE_IFELSE([ #define macro(...) macrofunc(__VA_ARGS__) int macrofunc(int a, int b) { return a + b; } int test() { return macro(1, 2); } ], AC_MSG_RESULT([yes]) AC_DEFINE([HAVE_VARIADIC_MACROS], 1, Define to 1 if variadic macros are supported), AC_MSG_RESULT([no])) dnl dnl Backtracing on segmentation faults dnl AC_MSG_CHECKING(for backtrace) AC_TRY_COMPILE( [#include ] [#include ] [#include ], [ void *array[100]; size_t size; char **strings; ] [ size = backtrace(array, 100); ] [ strings = backtrace_symbols(array, size); ], FOUND=yes AC_MSG_RESULT([[yes]]) AC_DEFINE([HAVE_BACKTRACE], 1, [Define to 1 to create stacktrace on segmentation faults]), FOUND=no AC_MSG_RESULT([[no]])) dnl dnl "rdynamic" linker flag dnl AC_MSG_CHECKING(for rdynamic linker flag) old_LDFLAGS="$LDFLAGS" LDFLAGS="$LDFLAGS -rdynamic" AC_TRY_LINK([], [], AC_MSG_RESULT([[yes]]), AC_MSG_RESULT([[no]]) [LDFLAGS="$old_LDFLAGS"]) dnl dnl End of debugging code dnl else AC_DEFINE([NDEBUG],1,Define to 1 to exclude debug-code) fi dnl dnl Enable test suite. Deafult: no. dnl AC_MSG_CHECKING(whether to enable unit and integration tests) AC_ARG_ENABLE(tests, [AS_HELP_STRING([--enable-tests], [enable unit and integration tests])], [ ENABLETESTS=$enableval ], [ ENABLETESTS=no] ) AC_MSG_RESULT($ENABLETESTS) if test "$ENABLETESTS" = "yes"; then AC_DEFINE([ENABLE_TESTS],1,[Define to 1 to enable unit and integration tests]) AM_CONDITIONAL(WITH_TESTS, true) else AM_CONDITIONAL(WITH_TESTS, false) fi AC_CONFIG_FILES([Makefile]) AC_OUTPUT nzbget-19.1/daemon/0000755000175000017500000000000013130203062014002 5ustar andreasandreasnzbget-19.1/daemon/connect/0000755000175000017500000000000013130203062015433 5ustar andreasandreasnzbget-19.1/daemon/connect/WebDownloader.cpp0000644000175000017500000003251613130203062020702 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2016 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, see . */ #include "nzbget.h" #include "WebDownloader.h" #include "Log.h" #include "Options.h" #include "Util.h" #include "FileSystem.h" WebDownloader::WebDownloader() { debug("Creating WebDownloader"); SetLastUpdateTimeNow(); } void WebDownloader::SetUrl(const char* url) { m_url = WebUtil::UrlEncode(url); } void WebDownloader::SetStatus(EStatus status) { m_status = status; Notify(nullptr); } void WebDownloader::SetLastUpdateTimeNow() { m_lastUpdateTime = Util::CurrentTime(); } void WebDownloader::Run() { debug("Entering WebDownloader-loop"); SetStatus(adRunning); int remainedDownloadRetries = g_Options->GetUrlRetries() > 0 ? g_Options->GetUrlRetries() : 1; int remainedConnectRetries = remainedDownloadRetries > 10 ? remainedDownloadRetries : 10; if (!m_retry) { remainedDownloadRetries = 1; remainedConnectRetries = 1; } EStatus Status = adFailed; while (!IsStopped() && remainedDownloadRetries > 0 && remainedConnectRetries > 0) { SetLastUpdateTimeNow(); Status = DownloadWithRedirects(5); if ((((Status == adFailed) && (remainedDownloadRetries > 1)) || ((Status == adConnectError) && (remainedConnectRetries > 1))) && !IsStopped() && !(!m_force && g_Options->GetPauseDownload())) { detail("Waiting %i sec to retry", g_Options->GetUrlInterval()); int msec = 0; while (!IsStopped() && (msec < g_Options->GetUrlInterval() * 1000) && !(!m_force && g_Options->GetPauseDownload())) { usleep(100 * 1000); msec += 100; } } if (IsStopped() || (!m_force && g_Options->GetPauseDownload())) { Status = adRetry; break; } if (Status == adFinished || Status == adFatalError || Status == adNotFound) { break; } if (Status != adConnectError) { remainedDownloadRetries--; } else { remainedConnectRetries--; } } if (Status != adFinished && Status != adRetry) { Status = adFailed; } if (Status == adFailed) { if (IsStopped()) { detail("Download %s cancelled", *m_infoName); } else { error("Download %s failed", *m_infoName); } } if (Status == adFinished) { detail("Download %s completed", *m_infoName); } SetStatus(Status); debug("Exiting WebDownloader-loop"); } WebDownloader::EStatus WebDownloader::Download() { EStatus Status = adRunning; URL url(m_url); Status = CreateConnection(&url); if (Status != adRunning) { return Status; } m_connection->SetTimeout(g_Options->GetUrlTimeout()); m_connection->SetSuppressErrors(false); // connection bool connected = m_connection->Connect(); if (!connected || IsStopped()) { FreeConnection(); return adConnectError; } // Okay, we got a Connection. Now start downloading. detail("Downloading %s", *m_infoName); SendHeaders(&url); Status = DownloadHeaders(); if (Status == adRunning) { Status = DownloadBody(); } if (IsStopped()) { Status = adFailed; } FreeConnection(); if (Status != adFinished) { // Download failed, delete broken output file FileSystem::DeleteFile(m_outputFilename); } return Status; } WebDownloader::EStatus WebDownloader::DownloadWithRedirects(int maxRedirects) { // do sync download, following redirects EStatus status = adRedirect; while (status == adRedirect && maxRedirects >= 0) { maxRedirects--; status = Download(); } if (status == adRedirect && maxRedirects < 0) { warn("Too many redirects for %s", *m_infoName); status = adFailed; } return status; } WebDownloader::EStatus WebDownloader::CreateConnection(URL *url) { if (!url->IsValid()) { error("URL is not valid: %s", url->GetAddress()); return adFatalError; } int port = url->GetPort(); if (port == 0 && !strcasecmp(url->GetProtocol(), "http")) { port = 80; } if (port == 0 && !strcasecmp(url->GetProtocol(), "https")) { port = 443; } if (strcasecmp(url->GetProtocol(), "http") && strcasecmp(url->GetProtocol(), "https")) { error("Unsupported protocol in URL: %s", url->GetAddress()); return adFatalError; } #ifdef DISABLE_TLS if (!strcasecmp(url->GetProtocol(), "https")) { error("Program was compiled without TLS/SSL-support. Cannot download using https protocol. URL: %s", url->GetAddress()); return adFatalError; } #endif bool tls = !strcasecmp(url->GetProtocol(), "https"); m_connection = std::make_unique(url->GetHost(), port, tls); return adRunning; } void WebDownloader::SendHeaders(URL *url) { // retrieve file m_connection->WriteLine(BString<1024>("GET %s HTTP/1.0\r\n", url->GetResource())); m_connection->WriteLine(BString<1024>("User-Agent: nzbget/%s\r\n", Util::VersionRevision())); if ((!strcasecmp(url->GetProtocol(), "http") && (url->GetPort() == 80 || url->GetPort() == 0)) || (!strcasecmp(url->GetProtocol(), "https") && (url->GetPort() == 443 || url->GetPort() == 0))) { m_connection->WriteLine(BString<1024>("Host: %s\r\n", url->GetHost())); } else { m_connection->WriteLine(BString<1024>("Host: %s:%i\r\n", url->GetHost(), url->GetPort())); } m_connection->WriteLine("Accept: */*\r\n"); #ifndef DISABLE_GZIP m_connection->WriteLine("Accept-Encoding: gzip\r\n"); #endif m_connection->WriteLine("Connection: close\r\n"); m_connection->WriteLine("\r\n"); } WebDownloader::EStatus WebDownloader::DownloadHeaders() { EStatus Status = adRunning; m_confirmedLength = false; CharBuffer lineBuf(1024*10); m_contentLen = -1; bool firstLine = true; m_gzip = false; m_redirecting = false; m_redirected = false; // Headers while (!IsStopped()) { SetLastUpdateTimeNow(); int len = 0; char* line = m_connection->ReadLine(lineBuf, lineBuf.Size(), &len); if (firstLine) { Status = CheckResponse(lineBuf); if (Status != adRunning) { break; } firstLine = false; } // Have we encountered a timeout? if (!line) { if (!IsStopped()) { warn("URL %s failed: Unexpected end of file", *m_infoName); } Status = adFailed; break; } debug("Header: %s", line); // detect body of response if (*line == '\r' || *line == '\n') { break; } Util::TrimRight(line); ProcessHeader(line); if (m_redirected) { Status = adRedirect; break; } } return Status; } WebDownloader::EStatus WebDownloader::DownloadBody() { EStatus Status = adRunning; m_outFile.Close(); bool end = false; CharBuffer lineBuf(1024*10); int writtenLen = 0; #ifndef DISABLE_GZIP m_gUnzipStream.reset(); if (m_gzip) { m_gUnzipStream = std::make_unique(1024*10); } #endif // Body while (!IsStopped()) { SetLastUpdateTimeNow(); char* buffer; int len; m_connection->ReadBuffer(&buffer, &len); if (len == 0) { len = m_connection->TryRecv(lineBuf, lineBuf.Size()); buffer = lineBuf; } // Connection closed or timeout? if (len <= 0) { if (len == 0 && m_contentLen == -1 && writtenLen > 0) { end = true; break; } if (!IsStopped()) { warn("URL %s failed: Unexpected end of file", *m_infoName); } Status = adFailed; break; } // write to output file if (!Write(buffer, len)) { Status = adFatalError; break; } writtenLen += len; //detect end of file if (writtenLen == m_contentLen || (m_contentLen == -1 && m_gzip && m_confirmedLength)) { end = true; break; } } #ifndef DISABLE_GZIP m_gUnzipStream.reset(); #endif m_outFile.Close(); if (!end && Status == adRunning && !IsStopped()) { warn("URL %s failed: file incomplete", *m_infoName); Status = adFailed; } if (end) { Status = adFinished; } return Status; } WebDownloader::EStatus WebDownloader::CheckResponse(const char* response) { if (!response) { if (!IsStopped()) { warn("URL %s: Connection closed by remote host", *m_infoName); } return adConnectError; } const char* hTTPResponse = strchr(response, ' '); if (strncmp(response, "HTTP", 4) || !hTTPResponse) { warn("URL %s failed: %s", *m_infoName, response); return adFailed; } hTTPResponse++; if (!strncmp(hTTPResponse, "400", 3) || !strncmp(hTTPResponse, "499", 3)) { warn("URL %s failed: %s", *m_infoName, hTTPResponse); return adConnectError; } else if (!strncmp(hTTPResponse, "404", 3)) { warn("URL %s failed: %s", *m_infoName, hTTPResponse); return adNotFound; } else if (!strncmp(hTTPResponse, "301", 3) || !strncmp(hTTPResponse, "302", 3)) { m_redirecting = true; return adRunning; } else if (!strncmp(hTTPResponse, "200", 3)) { // OK return adRunning; } else { // unknown error, no special handling warn("URL %s failed: %s", *m_infoName, response); return adFailed; } } void WebDownloader::ProcessHeader(const char* line) { if (!strncasecmp(line, "Content-Length: ", 16)) { m_contentLen = atoi(line + 16); m_confirmedLength = true; } else if (!strncasecmp(line, "Content-Encoding: gzip", 22)) { m_gzip = true; } else if (!strncasecmp(line, "Content-Disposition: ", 21)) { ParseFilename(line); } else if (m_redirecting && !strncasecmp(line, "Location: ", 10)) { ParseRedirect(line + 10); m_redirected = true; } } void WebDownloader::ParseFilename(const char* contentDisposition) { // Examples: // Content-Disposition: attachment; filename="fname.ext" // Content-Disposition: attachement;filename=fname.ext // Content-Disposition: attachement;filename=fname.ext; const char *p = strstr(contentDisposition, "filename"); if (!p) { return; } p = strchr(p, '='); if (!p) { return; } p++; while (*p == ' ') p++; BString<1024> fname = p; char *pe = fname + strlen(fname) - 1; while ((*pe == ' ' || *pe == '\n' || *pe == '\r' || *pe == ';') && pe > fname) { *pe = '\0'; pe--; } WebUtil::HttpUnquote(fname); m_originalFilename = FileSystem::BaseFileName(fname); debug("OriginalFilename: %s", *m_originalFilename); } void WebDownloader::ParseRedirect(const char* location) { const char* newLocation = location; BString<1024> urlBuf; URL newUrl(newLocation); if (!newUrl.IsValid()) { // redirect within host BString<1024> resource; URL oldUrl(m_url); if (*location == '/') { // absolute path within host resource = location; } else { // relative path within host resource = oldUrl.GetResource(); char* p = strchr(resource, '?'); if (p) { *p = '\0'; } p = strrchr(resource, '/'); if (p) { p[1] = '\0'; } resource.Append(location); } if (oldUrl.GetPort() > 0) { urlBuf.Format("%s://%s:%i%s", oldUrl.GetProtocol(), oldUrl.GetHost(), oldUrl.GetPort(), *resource); } else { urlBuf.Format("%s://%s%s", oldUrl.GetProtocol(), oldUrl.GetHost(), *resource); } newLocation = urlBuf; } detail("URL %s redirected to %s", *m_url, newLocation); SetUrl(newLocation); } bool WebDownloader::Write(void* buffer, int len) { if (!m_outFile.Active() && !PrepareFile()) { return false; } #ifndef DISABLE_GZIP if (m_gzip) { m_gUnzipStream->Write(buffer, len); const void *outBuf; int outLen = 1; while (outLen > 0) { GUnzipStream::EStatus gZStatus = m_gUnzipStream->Read(&outBuf, &outLen); if (gZStatus == GUnzipStream::zlError) { error("URL %s: GUnzip failed", *m_infoName); return false; } if (outLen > 0 && m_outFile.Write(outBuf, outLen) <= 0) { return false; } if (gZStatus == GUnzipStream::zlFinished) { m_confirmedLength = true; return true; } } return true; } else #endif return m_outFile.Write(buffer, len) > 0; } bool WebDownloader::PrepareFile() { // prepare file for writing const char* filename = m_outputFilename; if (!m_outFile.Open(filename, DiskFile::omWrite)) { error("Could not %s file %s", "create", filename); return false; } if (g_Options->GetWriteBuffer() > 0) { m_outFile.SetWriteBuffer(g_Options->GetWriteBuffer() * 1024); } return true; } void WebDownloader::LogDebugInfo() { info(" Web-Download: status=%i, LastUpdateTime=%s, filename=%s", m_status, *Util::FormatTime(m_lastUpdateTime), FileSystem::BaseFileName(m_outputFilename)); } void WebDownloader::Stop() { debug("Trying to stop WebDownloader"); Thread::Stop(); Guard guard(m_connectionMutex); if (m_connection) { m_connection->SetSuppressErrors(true); m_connection->Cancel(); } debug("WebDownloader stopped successfully"); } bool WebDownloader::Terminate() { std::unique_ptr connection = std::move(m_connection); bool terminated = Kill(); if (terminated && connection) { debug("Terminating connection"); connection->SetSuppressErrors(true); connection->Cancel(); connection->Disconnect(); connection.reset(); } return terminated; } void WebDownloader::FreeConnection() { if (m_connection) { debug("Releasing connection"); Guard guard(m_connectionMutex); if (m_connection->GetStatus() == Connection::csCancelled) { m_connection->Disconnect(); } m_connection.reset(); } } nzbget-19.1/daemon/connect/WebDownloader.h0000644000175000017500000000557613130203062020355 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2016 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, see . */ #ifndef WEBDOWNLOADER_H #define WEBDOWNLOADER_H #include "NString.h" #include "Observer.h" #include "Thread.h" #include "Connection.h" #include "FileSystem.h" #include "Util.h" class WebDownloader : public Thread, public Subject { public: enum EStatus { adUndefined, adRunning, adFinished, adFailed, adRetry, adNotFound, adRedirect, adConnectError, adFatalError }; WebDownloader(); EStatus GetStatus() { return m_status; } virtual void Run(); virtual void Stop(); EStatus Download(); EStatus DownloadWithRedirects(int maxRedirects); bool Terminate(); void SetInfoName(const char* infoName) { m_infoName = infoName; } const char* GetInfoName() { return m_infoName; } void SetUrl(const char* url); const char* GetOutputFilename() { return m_outputFilename; } void SetOutputFilename(const char* outputFilename) { m_outputFilename = outputFilename; } time_t GetLastUpdateTime() { return m_lastUpdateTime; } void SetLastUpdateTimeNow(); bool GetConfirmedLength() { return m_confirmedLength; } const char* GetOriginalFilename() { return m_originalFilename; } void SetForce(bool force) { m_force = force; } void SetRetry(bool retry) { m_retry = retry; } void LogDebugInfo(); protected: virtual void ProcessHeader(const char* line); private: CString m_url; CString m_outputFilename; std::unique_ptr m_connection; Mutex m_connectionMutex; EStatus m_status = adUndefined; time_t m_lastUpdateTime; CString m_infoName; DiskFile m_outFile; int m_contentLen; bool m_confirmedLength = false; CString m_originalFilename; bool m_force = false; bool m_redirecting; bool m_redirected; bool m_gzip; bool m_retry = true; #ifndef DISABLE_GZIP std::unique_ptr m_gUnzipStream; #endif void SetStatus(EStatus status); bool Write(void* buffer, int len); bool PrepareFile(); void FreeConnection(); EStatus CheckResponse(const char* response); EStatus CreateConnection(URL *url); void ParseFilename(const char* contentDisposition); void SendHeaders(URL *url); EStatus DownloadHeaders(); EStatus DownloadBody(); void ParseRedirect(const char* location); }; #endif nzbget-19.1/daemon/connect/Connection.cpp0000644000175000017500000004707613130203062020254 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "Connection.h" #include "Log.h" static const int CONNECTION_READBUFFER_SIZE = 1024; #ifndef HAVE_GETADDRINFO #ifndef HAVE_GETHOSTBYNAME_R std::unique_ptr Connection::m_getHostByNameMutex; #endif #endif class ConnectionFinalizer { public: ~ConnectionFinalizer() { Connection::Final(); } }; std::unique_ptr m_connectionFinalizer; void closesocket_gracefully(SOCKET socket) { char buf[1024]; struct linger linger; // Set linger option to avoid socket hanging out after close. This prevent // ephemeral port exhaust problem under high QPS. linger.l_onoff = 1; linger.l_linger = 1; setsockopt(socket, SOL_SOCKET, SO_LINGER, (char *) &linger, sizeof(linger)); // Send FIN to the client shutdown(socket, SHUT_WR); // Set non-blocking mode #ifdef WIN32 u_long on = 1; ioctlsocket(socket, FIONBIO, &on); #else int flags; flags = fcntl(socket, F_GETFL, 0); fcntl(socket, F_SETFL, flags | O_NONBLOCK); #endif // Read and discard pending incoming data. If we do not do that and close the // socket, the data in the send buffer may be discarded. This // behaviour is seen on Windows, when client keeps sending data // when server decides to close the connection; then when client // does recv() it gets no data back. int n; do { n = recv(socket, buf, sizeof(buf), 0); } while (n > 0); // Now we know that our FIN is ACK-ed, safe to close closesocket(socket); } void Connection::Init() { debug("Initializing global connection data"); #ifdef WIN32 WSADATA wsaData; int err = WSAStartup(MAKEWORD(2, 0), &wsaData); if (err != 0) { error("Could not initialize socket library"); return; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE( wsaData.wVersion ) != 0) { error("Could not initialize socket library"); WSACleanup(); return; } #endif #ifndef HAVE_GETADDRINFO #ifndef HAVE_GETHOSTBYNAME_R m_getHostByNameMutex = std::make_unique(); #endif #endif m_connectionFinalizer = std::make_unique(); } void Connection::Final() { #ifdef WIN32 WSACleanup(); #endif } Connection::Connection(const char* host, int port, bool tls) : m_host(host), m_port(port), m_tls(tls) { debug("Creating Connection"); m_readBuf.Reserve(CONNECTION_READBUFFER_SIZE + 1); } Connection::Connection(SOCKET socket, bool tls) { debug("Creating Connection"); m_port = 0; m_tls = tls; m_status = csConnected; m_socket = socket; m_bufAvail = 0; m_timeout = 60; m_suppressErrors = true; m_readBuf.Reserve(CONNECTION_READBUFFER_SIZE + 1); #ifndef DISABLE_TLS m_tlsSocket = nullptr; m_tlsError = false; #endif } Connection::~Connection() { debug("Destroying Connection"); Disconnect(); } void Connection::SetSuppressErrors(bool suppressErrors) { m_suppressErrors = suppressErrors; #ifndef DISABLE_TLS if (m_tlsSocket) { m_tlsSocket->SetSuppressErrors(suppressErrors); } #endif } bool Connection::Connect() { debug("Connecting"); if (m_status == csConnected) { return true; } bool res = DoConnect(); if (res) { m_status = csConnected; } else { DoDisconnect(); } return res; } bool Connection::Disconnect() { debug("Disconnecting"); if (m_status == csDisconnected) { return true; } bool res = DoDisconnect(); m_status = csDisconnected; m_socket = INVALID_SOCKET; m_bufAvail = 0; return res; } bool Connection::Bind() { debug("Binding"); if (m_status == csListening) { return true; } #ifdef HAVE_GETADDRINFO struct addrinfo addr_hints, *addr_list, *addr; memset(&addr_hints, 0, sizeof(addr_hints)); addr_hints.ai_family = m_ipVersion == ipV4 ? AF_INET : m_ipVersion == ipV6 ? AF_INET6 : AF_UNSPEC; addr_hints.ai_socktype = SOCK_STREAM, addr_hints.ai_flags = AI_PASSIVE; // For wildcard IP address BString<100> portStr("%d", m_port); int res = getaddrinfo(m_host, portStr, &addr_hints, &addr_list); if (res != 0) { ReportError("Could not resolve hostname %s", m_host, true #ifndef WIN32 , res != EAI_SYSTEM ? res : 0 , res != EAI_SYSTEM ? gai_strerror(res) : nullptr #endif ); return false; } m_broken = false; m_socket = INVALID_SOCKET; for (addr = addr_list; addr != nullptr; addr = addr->ai_next) { m_socket = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); #ifdef WIN32 SetHandleInformation((HANDLE)m_socket, HANDLE_FLAG_INHERIT, 0); #endif if (m_socket != INVALID_SOCKET) { int opt = 1; setsockopt(m_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)); res = bind(m_socket, addr->ai_addr, addr->ai_addrlen); if (res != -1) { // Connection established break; } // Connection failed closesocket(m_socket); m_socket = INVALID_SOCKET; } } freeaddrinfo(addr_list); #else struct sockaddr_in sSocketAddress; memset(&sSocketAddress, 0, sizeof(sSocketAddress)); sSocketAddress.sin_family = AF_INET; if (!m_host || strlen(m_host) == 0) { sSocketAddress.sin_addr.s_addr = htonl(INADDR_ANY); } else { sSocketAddress.sin_addr.s_addr = ResolveHostAddr(m_host); if (sSocketAddress.sin_addr.s_addr == INADDR_NONE) { return false; } } sSocketAddress.sin_port = htons(m_port); m_socket = socket(PF_INET, SOCK_STREAM, 0); if (m_socket == INVALID_SOCKET) { ReportError("Socket creation failed for %s", m_host, true); return false; } int opt = 1; setsockopt(m_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)); int res = bind(m_socket, (struct sockaddr *) &sSocketAddress, sizeof(sSocketAddress)); if (res == -1) { // Connection failed closesocket(m_socket); m_socket = INVALID_SOCKET; } #endif if (m_socket == INVALID_SOCKET) { ReportError("Binding socket failed for %s", m_host, true); return false; } if (listen(m_socket, 100) < 0) { ReportError("Listen on socket failed for %s", m_host, true); return false; } m_status = csListening; return true; } int Connection::WriteLine(const char* buffer) { //debug("Connection::WriteLine"); if (m_status != csConnected) { return -1; } int res = send(m_socket, buffer, strlen(buffer), 0); if (res <= 0) { m_broken = true; } return res; } bool Connection::Send(const char* buffer, int size) { debug("Sending data"); if (m_status != csConnected) { return false; } int bytesSent = 0; while (bytesSent < size) { int res = send(m_socket, buffer + bytesSent, size-bytesSent, 0); if (res <= 0) { m_broken = true; return false; } bytesSent += res; } return true; } char* Connection::ReadLine(char* buffer, int size, int* bytesReadOut) { if (m_status != csConnected) { return nullptr; } char* inpBuffer = buffer; size--; // for trailing '0' int bytesRead = 0; int bufAvail = m_bufAvail; // local variable is faster char* bufPtr = m_bufPtr; // local variable is faster while (size) { if (!bufAvail) { bufAvail = recv(m_socket, m_readBuf, m_readBuf.Size() - 1, 0); if (bufAvail < 0) { ReportError("Could not receive data on socket from %s", m_host, true); m_broken = true; break; } else if (bufAvail == 0) { break; } bufPtr = m_readBuf; m_readBuf[bufAvail] = '\0'; } int len = 0; char* p = (char*)memchr(bufPtr, '\n', bufAvail); if (p) { len = (int)(p - bufPtr + 1); } else { len = bufAvail; } if (len > size) { len = size; } memcpy(inpBuffer, bufPtr, len); inpBuffer += len; bufPtr += len; bufAvail -= len; bytesRead += len; size -= len; if (p) { break; } } *inpBuffer = '\0'; m_bufAvail = bufAvail > 0 ? bufAvail : 0; // copy back to member m_bufPtr = bufPtr; // copy back to member if (bytesReadOut) { *bytesReadOut = bytesRead; } m_totalBytesRead += bytesRead; if (inpBuffer == buffer) { return nullptr; } return buffer; } std::unique_ptr Connection::Accept() { debug("Accepting connection"); if (m_status != csListening) { return nullptr; } SOCKET socket = accept(m_socket, nullptr, nullptr); if (socket == INVALID_SOCKET && m_status != csCancelled) { ReportError("Could not accept connection for %s", m_host, true); } if (socket == INVALID_SOCKET) { return nullptr; } return std::make_unique(socket, m_tls); } int Connection::TryRecv(char* buffer, int size) { debug("Receiving data"); memset(buffer, 0, size); int received = recv(m_socket, buffer, size, 0); if (received < 0) { ReportError("Could not receive data on socket from %s", m_host, true); } return received; } bool Connection::Recv(char * buffer, int size) { debug("Receiving data (full buffer)"); memset(buffer, 0, size); char* bufPtr = (char*)buffer; int NeedBytes = size; if (m_bufAvail > 0) { int len = size > m_bufAvail ? m_bufAvail : size; memcpy(bufPtr, m_bufPtr, len); bufPtr += len; m_bufPtr += len; m_bufAvail -= len; NeedBytes -= len; } // Read from the socket until nothing remains while (NeedBytes > 0) { int received = recv(m_socket, bufPtr, NeedBytes, 0); // Did the recv succeed? if (received <= 0) { ReportError("Could not receive data on socket from %s", m_host, true); return false; } bufPtr += received; NeedBytes -= received; } return true; } bool Connection::DoConnect() { debug("Do connecting"); m_socket = INVALID_SOCKET; m_broken = false; #ifdef HAVE_GETADDRINFO struct addrinfo addr_hints, *addr_list, *addr; memset(&addr_hints, 0, sizeof(addr_hints)); addr_hints.ai_family = m_ipVersion == ipV4 ? AF_INET : m_ipVersion == ipV6 ? AF_INET6 : AF_UNSPEC; addr_hints.ai_socktype = SOCK_STREAM; BString<100> portStr("%d", m_port); int res = getaddrinfo(m_host, portStr, &addr_hints, &addr_list); if (res != 0) { ReportError("Could not resolve hostname %s", m_host, true #ifndef WIN32 , res != EAI_SYSTEM ? res : 0 , res != EAI_SYSTEM ? gai_strerror(res) : nullptr #endif ); return false; } std::vector triedAddr; bool connected = false; for (addr = addr_list; addr != nullptr; addr = addr->ai_next) { // don't try the same combinations of ai_family, ai_socktype, ai_protocol multiple times SockAddr sa = { addr->ai_family, addr->ai_socktype, addr->ai_protocol }; if (std::find(triedAddr.begin(), triedAddr.end(), sa) != triedAddr.end()) { continue; } triedAddr.push_back(sa); if (m_socket != INVALID_SOCKET) { closesocket(m_socket); } m_socket = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); #ifdef WIN32 SetHandleInformation((HANDLE)m_socket, HANDLE_FLAG_INHERIT, 0); #endif if (m_socket == INVALID_SOCKET) { // try another addr/family/protocol continue; } if (ConnectWithTimeout(addr->ai_addr, addr->ai_addrlen)) { // Connection established connected = true; break; } } if (m_socket == INVALID_SOCKET && addr_list) { ReportError("Socket creation failed for %s", m_host, true); } if (!connected && m_socket != INVALID_SOCKET) { ReportError("Connection to %s failed", m_host, true); closesocket(m_socket); m_socket = INVALID_SOCKET; } freeaddrinfo(addr_list); if (m_socket == INVALID_SOCKET) { return false; } #else struct sockaddr_in sSocketAddress; memset(&sSocketAddress, 0, sizeof(sSocketAddress)); sSocketAddress.sin_family = AF_INET; sSocketAddress.sin_port = htons(m_port); sSocketAddress.sin_addr.s_addr = ResolveHostAddr(m_host); if (sSocketAddress.sin_addr.s_addr == INADDR_NONE) { return false; } m_socket = socket(PF_INET, SOCK_STREAM, 0); if (m_socket == INVALID_SOCKET) { ReportError("Socket creation failed for %s", m_host, true); return false; } if (!ConnectWithTimeout(&sSocketAddress, sizeof(sSocketAddress))) { ReportError("Connection to %s failed", m_host, true); closesocket(m_socket); m_socket = INVALID_SOCKET; return false; } #endif if (!InitSocketOpts()) { return false; } #ifndef DISABLE_TLS if (m_tls && !StartTls(true, nullptr, nullptr)) { return false; } #endif return true; } bool Connection::InitSocketOpts() { char* optbuf = nullptr; int optsize = 0; #ifdef WIN32 int MSecVal = m_timeout * 1000; optbuf = (char*)&MSecVal; optsize = sizeof(MSecVal); #else struct timeval TimeVal; TimeVal.tv_sec = m_timeout; TimeVal.tv_usec = 0; optbuf = (char*)&TimeVal; optsize = sizeof(TimeVal); #endif int err = setsockopt(m_socket, SOL_SOCKET, SO_RCVTIMEO, optbuf, optsize); if (err != 0) { ReportError("Socket initialization failed for %s", m_host, true); return false; } err = setsockopt(m_socket, SOL_SOCKET, SO_SNDTIMEO, optbuf, optsize); if (err != 0) { ReportError("Socket initialization failed for %s", m_host, true); return false; } return true; } bool Connection::ConnectWithTimeout(void* address, int address_len) { int flags = 0, error = 0, ret = 0; fd_set rset, wset; socklen_t len = sizeof(error); struct timeval ts; ts.tv_sec = m_timeout; ts.tv_usec = 0; //clear out descriptor sets for select //add socket to the descriptor sets FD_ZERO(&rset); FD_SET(m_socket, &rset); wset = rset; //structure assignment ok //set socket nonblocking flag #ifdef WIN32 u_long mode = 1; if (ioctlsocket(m_socket, FIONBIO, &mode) != 0) { return false; } #else flags = fcntl(m_socket, F_GETFL, 0); if (flags < 0) { return false; } if (fcntl(m_socket, F_SETFL, flags | O_NONBLOCK) < 0) { return false; } #endif //initiate non-blocking connect ret = connect(m_socket, (struct sockaddr*)address, address_len); if (ret < 0) { #ifdef WIN32 int err = WSAGetLastError(); if (err != WSAEWOULDBLOCK) { return false; } #else if (errno != EINPROGRESS) { return false; } #endif } //connect succeeded right away? if (ret != 0) { ret = select(m_socket + 1, &rset, &wset, nullptr, m_timeout ? &ts : nullptr); //we are waiting for connect to complete now if (ret < 0) { return false; } if (ret == 0) { //we had a timeout #ifdef WIN32 WSASetLastError(WSAETIMEDOUT); #else errno = ETIMEDOUT; #endif return false; } if (!(FD_ISSET(m_socket, &rset) || FD_ISSET(m_socket, &wset))) { return false; } //we had a positivite return so a descriptor is ready if (getsockopt(m_socket, SOL_SOCKET, SO_ERROR, (char*)&error, &len) < 0) { return false; } //check if we had a socket error if (error) { errno = error; return false; } } //put socket back in blocking mode #ifdef WIN32 mode = 0; if (ioctlsocket(m_socket, FIONBIO, &mode) != 0) { return false; } #else if (fcntl(m_socket, F_SETFL, flags) < 0) { return false; } #endif return true; } bool Connection::DoDisconnect() { debug("Do disconnecting"); if (m_socket != INVALID_SOCKET) { #ifndef DISABLE_TLS CloseTls(); #endif if (m_gracefull) { closesocket_gracefully(m_socket); } else { closesocket(m_socket); } m_socket = INVALID_SOCKET; } m_status = csDisconnected; return true; } void Connection::ReadBuffer(char** buffer, int *bufLen) { *bufLen = m_bufAvail; *buffer = m_bufPtr; m_bufAvail = 0; }; void Connection::Cancel() { debug("Cancelling connection"); if (m_socket != INVALID_SOCKET) { m_status = csCancelled; int r = shutdown(m_socket, SHUT_RDWR); if (r == -1) { ReportError("Could not shutdown connection for %s", m_host, true); } } } void Connection::ReportError(const char* msgPrefix, const char* msgArg, bool PrintErrCode, int herrno, const char* herrMsg) { #ifndef DISABLE_TLS if (m_tlsError) { // TLS-Error was already reported m_tlsError = false; return; } #endif BString<1024> errPrefix(msgPrefix, msgArg); if (PrintErrCode) { #ifdef WIN32 int ErrCode = WSAGetLastError(); char errMsg[1024]; errMsg[0] = '\0'; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, ErrCode, 0, errMsg, 1024, nullptr); errMsg[1024-1] = '\0'; #else const char* errMsg = herrMsg; int ErrCode = herrno; if (herrno == 0) { ErrCode = errno; errMsg = strerror(ErrCode); } else if (!herrMsg) { errMsg = hstrerror(ErrCode); } #endif if (m_suppressErrors) { debug("%s: ErrNo %i, %s", *errPrefix, ErrCode, errMsg); } else { PrintError(BString<1024>("%s: ErrNo %i, %s", *errPrefix, ErrCode, errMsg)); } } else { if (m_suppressErrors) { debug("%s", *errPrefix); } else { PrintError(errPrefix); } } } void Connection::PrintError(const char* errMsg) { error("%s", errMsg); } #ifndef DISABLE_TLS bool Connection::StartTls(bool isClient, const char* certFile, const char* keyFile) { debug("Starting TLS"); m_tlsSocket = std::make_unique(m_socket, isClient, m_host, certFile, keyFile, m_cipher, this); m_tlsSocket->SetSuppressErrors(m_suppressErrors); return m_tlsSocket->Start(); } void Connection::CloseTls() { if (m_tlsSocket) { m_tlsSocket->Close(); m_tlsSocket.reset(); } } int Connection::recv(SOCKET s, char* buf, int len, int flags) { int received = 0; if (m_tlsSocket) { m_tlsError = false; received = m_tlsSocket->Recv(buf, len); if (received < 0) { m_tlsError = true; return -1; } } else { received = ::recv(s, buf, len, flags); } return received; } int Connection::send(SOCKET s, const char* buf, int len, int flags) { int sent = 0; if (m_tlsSocket) { m_tlsError = false; sent = m_tlsSocket->Send(buf, len); if (sent < 0) { m_tlsError = true; return -1; } return sent; } else { sent = ::send(s, buf, len, flags); return sent; } } #endif #ifndef HAVE_GETADDRINFO in_addr_t Connection::ResolveHostAddr(const char* host) { in_addr_t uaddr = inet_addr(host); if (uaddr == INADDR_NONE) { struct hostent* hinfo; bool err = false; int h_errnop = 0; #ifdef HAVE_GETHOSTBYNAME_R struct hostent hinfobuf; char strbuf[1024]; #ifdef HAVE_GETHOSTBYNAME_R_6 err = gethostbyname_r(host, &hinfobuf, strbuf, sizeof(strbuf), &hinfo, &h_errnop); err = err || (hinfo == nullptr); // error on null hinfo (means 'no entry') #endif #ifdef HAVE_GETHOSTBYNAME_R_5 hinfo = gethostbyname_r(host, &hinfobuf, strbuf, sizeof(strbuf), &h_errnop); err = hinfo == nullptr; #endif #ifdef HAVE_GETHOSTBYNAME_R_3 //NOTE: gethostbyname_r with three parameters were not tested struct hostent_data hinfo_data; hinfo = gethostbyname_r((char*)host, (struct hostent*)hinfobuf, &hinfo_data); err = hinfo == nullptr; #endif #else Guard guard(m_getHostByNameMutex); hinfo = gethostbyname(host); err = hinfo == nullptr; #endif if (err) { ReportError("Could not resolve hostname %s", host, true, h_errnop); return INADDR_NONE; } memcpy(&uaddr, hinfo->h_addr_list[0], sizeof(uaddr)); } return uaddr; } #endif const char* Connection::GetRemoteAddr() { struct sockaddr_in PeerName; int peerNameLength = sizeof(PeerName); if (getpeername(m_socket, (struct sockaddr*)&PeerName, (SOCKLEN_T*) &peerNameLength) >= 0) { #ifdef WIN32 m_remoteAddr = inet_ntoa(PeerName.sin_addr); #else inet_ntop(AF_INET, &PeerName.sin_addr, m_remoteAddr, m_remoteAddr.Capacity()); m_remoteAddr[m_remoteAddr.Capacity() - 1] = '\0'; #endif } return m_remoteAddr; } int Connection::FetchTotalBytesRead() { int total = m_totalBytesRead; m_totalBytesRead = 0; return total; } nzbget-19.1/daemon/connect/TlsSocket.cpp0000644000175000017500000003657313130203062020070 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2008-2016 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, see . */ #include "nzbget.h" #ifndef DISABLE_TLS #include "TlsSocket.h" #include "Thread.h" #include "Log.h" #include "Util.h" #include "FileSystem.h" class TlsSocketFinalizer { public: ~TlsSocketFinalizer() { TlsSocket::Final(); } }; std::unique_ptr m_tlsSocketFinalizer; CString TlsSocket::m_certStore; #ifdef HAVE_LIBGNUTLS #ifdef NEED_GCRYPT_LOCKING /** * Mutexes for gcryptlib */ std::vector> g_GCryptLibMutexes; static int gcry_mutex_init(void **priv) { g_GCryptLibMutexes.emplace_back(std::make_unique()); *priv = g_GCryptLibMutexes.back().get(); return 0; } static int gcry_mutex_destroy(void **lock) { Mutex* mutex = ((Mutex*)*lock); g_GCryptLibMutexes.erase(std::find_if(g_GCryptLibMutexes.begin(), g_GCryptLibMutexes.end(), [mutex](std::unique_ptr& itMutex) { return itMutex.get() == mutex; })); return 0; } static int gcry_mutex_lock(void **lock) { ((Mutex*)*lock)->Lock(); return 0; } static int gcry_mutex_unlock(void **lock) { ((Mutex*)*lock)->Unlock(); return 0; } static struct gcry_thread_cbs gcry_threads_Mutex = { GCRY_THREAD_OPTION_USER, nullptr, gcry_mutex_init, gcry_mutex_destroy, gcry_mutex_lock, gcry_mutex_unlock, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; #endif /* NEED_GCRYPT_LOCKING */ #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL #ifndef CRYPTO_set_locking_callback #define NEED_CRYPTO_LOCKING #endif #ifdef NEED_CRYPTO_LOCKING /** * Mutexes for OpenSSL */ std::vector> g_OpenSSLMutexes; static void openssl_locking(int mode, int n, const char* file, int line) { Mutex* mutex = g_OpenSSLMutexes[n].get(); if (mode & CRYPTO_LOCK) { mutex->Lock(); } else { mutex->Unlock(); } } static struct CRYPTO_dynlock_value* openssl_dynlock_create(const char *file, int line) { return (CRYPTO_dynlock_value*)new Mutex(); } static void openssl_dynlock_destroy(struct CRYPTO_dynlock_value *l, const char *file, int line) { Mutex* mutex = (Mutex*)l; delete mutex; } static void openssl_dynlock_lock(int mode, struct CRYPTO_dynlock_value *l, const char *file, int line) { Mutex* mutex = (Mutex*)l; if (mode & CRYPTO_LOCK) { mutex->Lock(); } else { mutex->Unlock(); } } #endif /* NEED_CRYPTO_LOCKING */ #endif /* HAVE_OPENSSL */ void TlsSocket::Init() { debug("Initializing TLS library"); #ifdef HAVE_LIBGNUTLS int error_code; #ifdef NEED_GCRYPT_LOCKING error_code = gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_Mutex); if (error_code != 0) { error("Could not initialize libcrypt"); return; } #endif /* NEED_GCRYPT_LOCKING */ error_code = gnutls_global_init(); if (error_code != 0) { error("Could not initialize libgnutls"); return; } #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL #ifdef NEED_CRYPTO_LOCKING for (int i = 0, num = CRYPTO_num_locks(); i < num; i++) { g_OpenSSLMutexes.emplace_back(std::make_unique()); } CRYPTO_set_locking_callback(openssl_locking); CRYPTO_set_dynlock_create_callback(openssl_dynlock_create); CRYPTO_set_dynlock_destroy_callback(openssl_dynlock_destroy); CRYPTO_set_dynlock_lock_callback(openssl_dynlock_lock); #endif /* NEED_CRYPTO_LOCKING */ SSL_load_error_strings(); SSL_library_init(); OpenSSL_add_all_algorithms(); #endif /* HAVE_OPENSSL */ m_tlsSocketFinalizer = std::make_unique(); } void TlsSocket::Final() { #ifdef HAVE_LIBGNUTLS gnutls_global_deinit(); #endif /* HAVE_LIBGNUTLS */ } TlsSocket::~TlsSocket() { Close(); } void TlsSocket::ReportError(const char* errMsg, bool suppressable) { #ifdef HAVE_LIBGNUTLS const char* errstr = gnutls_strerror(m_retCode); if (suppressable && m_suppressErrors) { debug("%s: %s", errMsg, errstr); } else { PrintError(BString<1024>("%s: %s", errMsg, errstr)); } #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL int errcode = ERR_get_error(); do { char errstr[1024]; ERR_error_string_n(errcode, errstr, sizeof(errstr)); errstr[1024-1] = '\0'; if (suppressable && m_suppressErrors) { debug("%s: %s", errMsg, errstr); } else if (errcode != 0) { PrintError(BString<1024>("%s: %s", errMsg, errstr)); } else { PrintError(errMsg); } errcode = ERR_get_error(); } while (errcode); #endif /* HAVE_OPENSSL */ } void TlsSocket::PrintError(const char* errMsg) { error("%s", errMsg); } bool TlsSocket::Start() { #ifdef HAVE_LIBGNUTLS gnutls_certificate_credentials_t cred; m_retCode = gnutls_certificate_allocate_credentials(&cred); if (m_retCode != 0) { ReportError("Could not create TLS context", false); return false; } m_context = cred; if (m_certFile && m_keyFile) { m_retCode = gnutls_certificate_set_x509_key_file((gnutls_certificate_credentials_t)m_context, m_certFile, m_keyFile, GNUTLS_X509_FMT_PEM); if (m_retCode != 0) { ReportError("Could not load certificate or key file", false); Close(); return false; } } gnutls_session_t sess; m_retCode = gnutls_init(&sess, m_isClient ? GNUTLS_CLIENT : GNUTLS_SERVER); if (m_retCode != 0) { ReportError("Could not create TLS session", false); Close(); return false; } m_session = sess; m_initialized = true; const char* priority = !m_cipher.Empty() ? m_cipher.Str() : (m_certFile && m_keyFile ? "NORMAL:!VERS-SSL3.0" : "NORMAL"); m_retCode = gnutls_priority_set_direct((gnutls_session_t)m_session, priority, nullptr); if (m_retCode != 0) { ReportError("Could not select cipher for TLS", false); Close(); return false; } if (m_host) { m_retCode = gnutls_server_name_set((gnutls_session_t)m_session, GNUTLS_NAME_DNS, m_host, m_host.Length()); if (m_retCode != 0) { ReportError("Could not set hostname for TLS"); Close(); return false; } } m_retCode = gnutls_credentials_set((gnutls_session_t)m_session, GNUTLS_CRD_CERTIFICATE, (gnutls_certificate_credentials_t*)m_context); if (m_retCode != 0) { ReportError("Could not initialize TLS session", false); Close(); return false; } gnutls_transport_set_ptr((gnutls_session_t)m_session, (gnutls_transport_ptr_t)(size_t)m_socket); m_retCode = gnutls_handshake((gnutls_session_t)m_session); if (m_retCode != 0) { ReportError(BString<1024>("TLS handshake failed for %s", *m_host)); Close(); return false; } if (m_isClient && !m_certStore.Empty() && !ValidateCert()) { Close(); return false; } m_connected = true; return true; #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL m_context = SSL_CTX_new(SSLv23_method()); if (!m_context) { ReportError("Could not create TLS context", false); return false; } if (m_certFile && m_keyFile) { if (SSL_CTX_use_certificate_chain_file((SSL_CTX*)m_context, m_certFile) != 1) { ReportError("Could not load certificate file", false); Close(); return false; } if (SSL_CTX_use_PrivateKey_file((SSL_CTX*)m_context, m_keyFile, SSL_FILETYPE_PEM) != 1) { ReportError("Could not load key file", false); Close(); return false; } if (!SSL_CTX_set_options((SSL_CTX*)m_context, SSL_OP_NO_SSLv3)) { ReportError("Could not select minimum protocol version for TLS", false); Close(); return false; } // For ECC certificates EC_KEY* ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); if (!ecdh) { ReportError("Could not generate ecdh parameters for TLS", false); Close(); return false; } if (!SSL_CTX_set_tmp_ecdh((SSL_CTX*)m_context, ecdh)) { ReportError("Could not set ecdh parameters for TLS", false); EC_KEY_free(ecdh); Close(); return false; } EC_KEY_free(ecdh); } if (m_isClient && !m_certStore.Empty()) { // Enable certificate validation if (SSL_CTX_load_verify_locations((SSL_CTX*)m_context, m_certStore, nullptr) != 1) { ReportError("Could not set certificate store location", false); Close(); return false; } SSL_CTX_set_verify((SSL_CTX*)m_context, SSL_VERIFY_PEER, nullptr); } m_session = SSL_new((SSL_CTX*)m_context); if (!m_session) { ReportError("Could not create TLS session", false); Close(); return false; } if (!m_cipher.Empty() && !SSL_set_cipher_list((SSL*)m_session, m_cipher)) { ReportError("Could not select cipher for TLS", false); Close(); return false; } if (m_host && !SSL_set_tlsext_host_name((SSL*)m_session, m_host)) { ReportError("Could not set host name for TLS"); Close(); return false; } if (!SSL_set_fd((SSL*)m_session, m_socket)) { ReportError("Could not set the file descriptor for TLS"); Close(); return false; } int error_code = m_isClient ? SSL_connect((SSL*)m_session) : SSL_accept((SSL*)m_session); if (error_code < 1) { long verifyRes = SSL_get_verify_result((SSL*)m_session); if (verifyRes != X509_V_OK) { PrintError(BString<1024>("TLS certificate verification failed for %s: %s." " For more info visit http://nzbget.net/certificate-verification", *m_host, X509_verify_cert_error_string(verifyRes))); } else { ReportError(BString<1024>("TLS handshake failed for %s", *m_host)); } Close(); return false; } if (m_isClient && !m_certStore.Empty() && !ValidateCert()) { Close(); return false; } m_connected = true; return true; #endif /* HAVE_OPENSSL */ } bool TlsSocket::ValidateCert() { #ifdef HAVE_LIBGNUTLS #if GNUTLS_VERSION_NUMBER >= 0x030104 #if GNUTLS_VERSION_NUMBER >= 0x030306 if (FileSystem::DirectoryExists(m_certStore)) { if (gnutls_certificate_set_x509_trust_dir((gnutls_certificate_credentials_t)m_context, m_certStore, GNUTLS_X509_FMT_PEM) < 0) { ReportError("Could not set certificate store location"); return false; } } else #endif { if (gnutls_certificate_set_x509_trust_file((gnutls_certificate_credentials_t)m_context, m_certStore, GNUTLS_X509_FMT_PEM) < 0) { ReportError("Could not set certificate store location"); return false; } } unsigned int status = 0; if (gnutls_certificate_verify_peers3((gnutls_session_t)m_session, m_host, &status) != 0 || gnutls_certificate_type_get((gnutls_session_t)m_session) != GNUTLS_CRT_X509) { ReportError("Could not verify TLS certificate"); return false; } if (status != 0) { if (status & GNUTLS_CERT_UNEXPECTED_OWNER) { // Extracting hostname from the certificate unsigned int cert_list_size = 0; const gnutls_datum_t* cert_list = gnutls_certificate_get_peers((gnutls_session_t)m_session, &cert_list_size); if (cert_list_size > 0) { gnutls_x509_crt_t cert; gnutls_x509_crt_init(&cert); gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER); char dn[256]; size_t size = sizeof(dn); if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0, dn, &size) == 0) { PrintError(BString<1024>("TLS certificate verification failed for %s: certificate hostname mismatch (%s)." " For more info visit http://nzbget.net/certificate-verification", *m_host, dn)); gnutls_x509_crt_deinit(cert); return false; } gnutls_x509_crt_deinit(cert); } } gnutls_datum_t msgdata; if (gnutls_certificate-verification_status_print(status, GNUTLS_CRT_X509, &msgdata, 0) == 0) { PrintError(BString<1024>("TLS certificate verification failed for %s: %s." " For more info visit http://nzbget.net/certificate-verification", *m_host, msgdata.data)); gnutls_free(&msgdata); } else { ReportError(BString<1024>("TLS certificate verification failed for %s." " For more info visit http://nzbget.net/certificate-verification", *m_host)); } return false; } #endif return true; #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL // verify a server certificate was presented during the negotiation X509* cert = SSL_get_peer_certificate((SSL*)m_session); if (!cert) { PrintError(BString<1024>("TLS certificate verification failed for %s: no certificate provided by server." " For more info visit http://nzbget.net/certificate-verification", *m_host)); return false; } #ifdef HAVE_X509_CHECK_HOST // hostname verification if (!m_host.Empty() && X509_check_host(cert, m_host, m_host.Length(), 0, nullptr) != 1) { char* certHost = nullptr; // Find the position of the CN field in the Subject field of the certificate int common_name_loc = X509_NAME_get_index_by_NID(X509_get_subject_name(cert), NID_commonName, -1); if (common_name_loc >= 0) { // Extract the CN field X509_NAME_ENTRY* common_name_entry = X509_NAME_get_entry(X509_get_subject_name(cert), common_name_loc); if (common_name_entry != nullptr) { // Convert the CN field to a C string ASN1_STRING* common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry); if (common_name_asn1 != nullptr) { certHost = (char*)ASN1_STRING_data(common_name_asn1); } } } PrintError(BString<1024>("TLS certificate verification failed for %s: certificate hostname mismatch (%s)." " For more info visit http://nzbget.net/certificate-verification", *m_host, certHost)); X509_free(cert); return false; } #endif X509_free(cert); return true; #endif /* HAVE_OPENSSL */ } void TlsSocket::Close() { if (m_session) { #ifdef HAVE_LIBGNUTLS if (m_connected) { gnutls_bye((gnutls_session_t)m_session, GNUTLS_SHUT_WR); } if (m_initialized) { gnutls_deinit((gnutls_session_t)m_session); } #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL if (m_connected) { SSL_shutdown((SSL*)m_session); } SSL_free((SSL*)m_session); #endif /* HAVE_OPENSSL */ m_session = nullptr; } if (m_context) { #ifdef HAVE_LIBGNUTLS gnutls_certificate_free_credentials((gnutls_certificate_credentials_t)m_context); #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL SSL_CTX_free((SSL_CTX*)m_context); #endif /* HAVE_OPENSSL */ m_context = nullptr; } } int TlsSocket::Send(const char* buffer, int size) { #ifdef HAVE_LIBGNUTLS m_retCode = gnutls_record_send((gnutls_session_t)m_session, buffer, size); #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL m_retCode = SSL_write((SSL*)m_session, buffer, size); #endif /* HAVE_OPENSSL */ if (m_retCode < 0) { #ifdef HAVE_OPENSSL if (ERR_peek_error() == 0) { ReportError("Could not write to TLS-Socket: Connection closed by remote host"); } else #endif /* HAVE_OPENSSL */ ReportError("Could not write to TLS-Socket"); return -1; } return m_retCode; } int TlsSocket::Recv(char* buffer, int size) { #ifdef HAVE_LIBGNUTLS m_retCode = gnutls_record_recv((gnutls_session_t)m_session, buffer, size); #endif /* HAVE_LIBGNUTLS */ #ifdef HAVE_OPENSSL m_retCode = SSL_read((SSL*)m_session, buffer, size); #endif /* HAVE_OPENSSL */ if (m_retCode < 0) { #ifdef HAVE_OPENSSL if (ERR_peek_error() == 0) { ReportError("Could not read from TLS-Socket: Connection closed by remote host"); } else #endif /* HAVE_OPENSSL */ { ReportError("Could not read from TLS-Socket"); } return -1; } return m_retCode; } #endif nzbget-19.1/daemon/connect/TlsSocket.h0000644000175000017500000000406013130203062017517 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2008-2016 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, see . */ #ifndef TLSSOCKET_H #define TLSSOCKET_H #ifndef DISABLE_TLS #include "NString.h" class TlsSocket { public: TlsSocket(SOCKET socket, bool isClient, const char* host, const char* certFile, const char* keyFile, const char* cipher) : m_socket(socket), m_isClient(isClient), m_host(host), m_certFile(certFile), m_keyFile(keyFile), m_cipher(cipher) {} virtual ~TlsSocket(); static void Init(); static void InitOptions(const char* certStore) { m_certStore = certStore; } bool Start(); void Close(); int Send(const char* buffer, int size); int Recv(char* buffer, int size); void SetSuppressErrors(bool suppressErrors) { m_suppressErrors = suppressErrors; } protected: virtual void PrintError(const char* errMsg); private: bool m_isClient; CString m_host; CString m_certFile; CString m_keyFile; CString m_cipher; SOCKET m_socket; bool m_suppressErrors = false; bool m_initialized = false; bool m_connected = false; int m_retCode; static CString m_certStore; // using "void*" to prevent the including of GnuTLS/OpenSSL header files into TlsSocket.h void* m_context = nullptr; void* m_session = nullptr; void ReportError(const char* errMsg, bool suppressable = true); bool ValidateCert(); static void Final(); friend class TlsSocketFinalizer; }; #endif #endif nzbget-19.1/daemon/connect/Connection.h0000644000175000017500000001035113130203062017703 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef CONNECTION_H #define CONNECTION_H #include "NString.h" #ifndef HAVE_GETADDRINFO #ifndef HAVE_GETHOSTBYNAME_R #include "Thread.h" #endif #endif #ifndef DISABLE_TLS #include "TlsSocket.h" #endif class Connection { public: enum EStatus { csConnected, csDisconnected, csListening, csCancelled }; enum EIPVersion { ipAuto, ipV4, ipV6 }; Connection(const char* host, int port, bool tls); Connection(SOCKET socket, bool tls); virtual ~Connection(); static void Init(); virtual bool Connect(); virtual bool Disconnect(); bool Bind(); bool Send(const char* buffer, int size); bool Recv(char* buffer, int size); int TryRecv(char* buffer, int size); char* ReadLine(char* buffer, int size, int* bytesRead); void ReadBuffer(char** buffer, int *bufLen); int WriteLine(const char* buffer); std::unique_ptr Accept(); void Cancel(); const char* GetHost() { return m_host; } int GetPort() { return m_port; } bool GetTls() { return m_tls; } const char* GetCipher() { return m_cipher; } void SetCipher(const char* cipher) { m_cipher = cipher; } void SetTimeout(int timeout) { m_timeout = timeout; } void SetIPVersion(EIPVersion ipVersion) { m_ipVersion = ipVersion; } EStatus GetStatus() { return m_status; } void SetSuppressErrors(bool suppressErrors); bool GetSuppressErrors() { return m_suppressErrors; } const char* GetRemoteAddr(); bool GetGracefull() { return m_gracefull; } void SetGracefull(bool gracefull) { m_gracefull = gracefull; } #ifndef DISABLE_TLS bool StartTls(bool isClient, const char* certFile, const char* keyFile); #endif int FetchTotalBytesRead(); protected: CString m_host; int m_port; bool m_tls; EIPVersion m_ipVersion = ipAuto; SOCKET m_socket = INVALID_SOCKET; CString m_cipher; CharBuffer m_readBuf; int m_bufAvail = 0; char* m_bufPtr = nullptr; EStatus m_status = csDisconnected; int m_timeout = 60; bool m_suppressErrors = true; BString<100> m_remoteAddr; int m_totalBytesRead = 0; bool m_broken = false; bool m_gracefull = false; struct SockAddr { int ai_family; int ai_socktype; int ai_protocol; bool operator==(const SockAddr& rhs) const { return memcmp(this, &rhs, sizeof(SockAddr)) == 0; } }; #ifndef DISABLE_TLS class ConTlsSocket: public TlsSocket { public: ConTlsSocket(SOCKET socket, bool isClient, const char* host, const char* certFile, const char* keyFile, const char* cipher, Connection* owner) : TlsSocket(socket, isClient, host, certFile, keyFile, cipher), m_owner(owner) {} protected: virtual void PrintError(const char* errMsg) { m_owner->PrintError(errMsg); } private: Connection* m_owner; }; std::unique_ptr m_tlsSocket; bool m_tlsError = false; #endif #ifndef HAVE_GETADDRINFO #ifndef HAVE_GETHOSTBYNAME_R static std::unique_ptr m_getHostByNameMutex; #endif #endif void ReportError(const char* msgPrefix, const char* msgArg, bool PrintErrCode, int herrno = 0, const char* herrMsg = nullptr); virtual void PrintError(const char* errMsg); bool DoConnect(); bool DoDisconnect(); bool InitSocketOpts(); bool ConnectWithTimeout(void* address, int address_len); #ifndef HAVE_GETADDRINFO in_addr_t ResolveHostAddr(const char* host); #endif #ifndef DISABLE_TLS int recv(SOCKET s, char* buf, int len, int flags); int send(SOCKET s, const char* buf, int len, int flags); void CloseTls(); #endif private: static void Final(); friend class ConnectionFinalizer; }; #endif nzbget-19.1/daemon/queue/0000755000175000017500000000000013130203062015126 5ustar andreasandreasnzbget-19.1/daemon/queue/HistoryCoordinator.h0000644000175000017500000000527313130203062021153 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2016 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, see . */ #ifndef HISTORYCOORDINATOR_H #define HISTORYCOORDINATOR_H #include "DownloadInfo.h" #include "Service.h" class HistoryCoordinator : public Service { public: void AddToHistory(DownloadQueue* downloadQueue, NzbInfo* nzbInfo); bool EditList(DownloadQueue* downloadQueue, IdList* idList, DownloadQueue::EEditAction action, const char* args); void DeleteDiskFiles(NzbInfo* nzbInfo); void HistoryHide(DownloadQueue* downloadQueue, HistoryInfo* historyInfo, int rindex); void Redownload(DownloadQueue* downloadQueue, HistoryInfo* historyInfo); protected: virtual int ServiceInterval() { return 600000; } virtual void ServiceWork(); private: void HistoryDelete(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo, bool final); void HistoryReturn(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo); void HistoryProcess(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo); void HistoryRedownload(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo, bool restorePauseState); void HistoryRetry(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo, bool resetFailed, bool reprocess); bool HistorySetParameter(HistoryInfo* historyInfo, const char* text); void HistorySetDupeParam(HistoryInfo* historyInfo, DownloadQueue::EEditAction action, const char* text); bool HistorySetCategory(HistoryInfo* historyInfo, const char* text); bool HistorySetName(HistoryInfo* historyInfo, const char* text); void MoveToQueue(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo, bool reprocess); void PrepareEdit(DownloadQueue* downloadQueue, IdList* idList, DownloadQueue::EEditAction action); void ResetArticles(FileInfo* fileInfo, bool allFailed, bool resetFailed); }; extern HistoryCoordinator* g_HistoryCoordinator; #endif nzbget-19.1/daemon/queue/QueueCoordinator.h0000644000175000017500000001050413130203062020567 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2017 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, see . */ #ifndef QUEUECOORDINATOR_H #define QUEUECOORDINATOR_H #include "Log.h" #include "Thread.h" #include "NzbFile.h" #include "ArticleDownloader.h" #include "DownloadInfo.h" #include "Observer.h" #include "QueueEditor.h" #include "NntpConnection.h" #include "DirectRenamer.h" class QueueCoordinator : public Thread, public Observer, public Debuggable { public: typedef std::list ActiveDownloads; QueueCoordinator(); virtual ~QueueCoordinator(); virtual void Run(); virtual void Stop(); void Update(Subject* Caller, void* Aspect); // editing queue NzbInfo* AddNzbFileToQueue(std::unique_ptr nzbInfo, NzbInfo* urlInfo, bool addFirst); void CheckDupeFileInfos(NzbInfo* nzbInfo); bool HasMoreJobs() { return m_hasMoreJobs; } void DiscardTempFiles(FileInfo* fileInfo); bool DeleteQueueEntry(DownloadQueue* downloadQueue, FileInfo* fileInfo); bool SetQueueEntryCategory(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, const char* category); bool SetQueueEntryName(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, const char* name); bool MergeQueueEntries(DownloadQueue* downloadQueue, NzbInfo* destNzbInfo, NzbInfo* srcNzbInfo); bool SplitQueueEntries(DownloadQueue* downloadQueue, RawFileList* fileList, const char* name, NzbInfo** newNzbInfo); protected: virtual void LogDebugInfo(); private: class CoordinatorDownloadQueue : public DownloadQueue { public: CoordinatorDownloadQueue(QueueCoordinator* owner) : m_owner(owner) {} virtual bool EditEntry(int ID, EEditAction action, const char* args); virtual bool EditList(IdList* idList, NameList* nameList, EMatchMode matchMode, EEditAction action, const char* args); virtual void HistoryChanged() { m_historyChanged = true; } virtual void Save(); private: QueueCoordinator* m_owner; bool m_massEdit = false; bool m_wantSave = false; bool m_historyChanged = false; friend class QueueCoordinator; }; class CoordinatorDirectRenamer : public DirectRenamer { public: CoordinatorDirectRenamer(QueueCoordinator* owner) : m_owner(owner) {} protected: virtual void RenameCompleted(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) { m_owner->DirectRenameCompleted(downloadQueue, nzbInfo); } private: QueueCoordinator* m_owner; }; CoordinatorDownloadQueue m_downloadQueue{this}; ActiveDownloads m_activeDownloads; QueueEditor m_queueEditor; CoordinatorDirectRenamer m_directRenamer{this}; bool m_hasMoreJobs = true; int m_downloadsLimit; int m_serverConfigGeneration = 0; bool GetNextArticle(DownloadQueue* downloadQueue, FileInfo* &fileInfo, ArticleInfo* &articleInfo); bool GetNextFirstArticle(NzbInfo* nzbInfo, FileInfo* &fileInfo, ArticleInfo* &articleInfo); void StartArticleDownload(FileInfo* fileInfo, ArticleInfo* articleInfo, NntpConnection* connection); void ArticleCompleted(ArticleDownloader* articleDownloader); void DeleteDownloader(DownloadQueue* downloadQueue, ArticleDownloader* articleDownloader, bool fileCompleted); void DeleteFileInfo(DownloadQueue* downloadQueue, FileInfo* fileInfo, bool completed); void DirectRenameCompleted(DownloadQueue* downloadQueue, NzbInfo* nzbInfo); void DiscardDirectRename(DownloadQueue* downloadQueue, NzbInfo* nzbInfo); void CheckHealth(DownloadQueue* downloadQueue, FileInfo* fileInfo); void ResetHangingDownloads(); void AdjustDownloadsLimit(); void Load(); void SaveAllPartialState(); void SavePartialState(FileInfo* fileInfo); void LoadPartialState(FileInfo* fileInfo); void WaitJobs(); }; extern QueueCoordinator* g_QueueCoordinator; #endif nzbget-19.1/daemon/queue/DirectRenamer.cpp0000644000175000017500000003561713130203062020372 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2017 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, see . */ #include "nzbget.h" #include "DirectRenamer.h" #include "Options.h" #include "FileSystem.h" #include "ParParser.h" #ifndef DISABLE_PARCHECK #include "par2cmdline.h" #include "par2fileformat.h" #include "md5.h" #endif class RenameContentAnalyzer : public ArticleContentAnalyzer { public: virtual void Reset(); virtual void Append(const void* buffer, int len); void Finish(); const char* GetHash16k() { return m_hash16k; } bool GetParFile() { return m_parFile; } const char* GetParSetId() { return m_parSetId; } private: #ifndef DISABLE_PARCHECK Par2::MD5Context m_md5Context; char m_signature[sizeof(Par2::PACKET_HEADER)]; #endif int m_dataSize = 0; CString m_hash16k; CString m_parSetId; bool m_parFile = false; }; #ifndef DISABLE_PARCHECK class DirectParRepairer : public Par2::Par2Repairer { public: DirectParRepairer() : Par2::Par2Repairer(m_nout, m_nout) {}; friend class DirectParLoader; private: class NullStreamBuf : public std::streambuf {}; NullStreamBuf m_nullbuf; std::ostream m_nout{&m_nullbuf}; }; class DirectParLoader : public Thread { public: static void StartLoader(DirectRenamer* owner, NzbInfo* nzbInfo); virtual void Run(); private: typedef std::vector ParFiles; DirectRenamer* m_owner; ParFiles m_parFiles; DirectRenamer::FileHashList m_parHashes; int m_nzbId; void LoadParFile(const char* parFile); }; void DirectParLoader::StartLoader(DirectRenamer* owner, NzbInfo* nzbInfo) { nzbInfo->PrintMessage(Message::mkInfo, "Directly checking renamed files for %s", nzbInfo->GetName()); DirectParLoader* directParLoader = new DirectParLoader(); directParLoader->m_owner = owner; directParLoader->m_nzbId = nzbInfo->GetId(); for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { if (completedFile.GetParFile()) { directParLoader->m_parFiles.emplace_back(BString<1024>("%s%c%s", nzbInfo->GetDestDir(), PATH_SEPARATOR, completedFile.GetFilename())); } } directParLoader->SetAutoDestroy(true); directParLoader->Start(); } void DirectParLoader::Run() { debug("Started DirectParLoader"); for (CString& parFile : m_parFiles) { LoadParFile(parFile); } GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); NzbInfo* nzbInfo = downloadQueue->GetQueue()->Find(m_nzbId); if (nzbInfo) { // nzb is still in queue m_owner->RenameFiles(downloadQueue, nzbInfo, &m_parHashes); } } void DirectParLoader::LoadParFile(const char* parFile) { DirectParRepairer repairer; if (!repairer.LoadPacketsFromFile(parFile)) { warn("Could not load par2-file %s", parFile); return; } GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); NzbInfo* nzbInfo = downloadQueue->GetQueue()->Find(m_nzbId); if (!nzbInfo) { // nzb isn't in queue anymore return; } nzbInfo->PrintMessage(Message::mkInfo, "Loaded par2-file %s for direct-rename", FileSystem::BaseFileName(parFile)); for (std::pair& entry : repairer.sourcefilemap) { if (IsStopped()) { break; } Par2::Par2RepairerSourceFile* sourceFile = entry.second; if (!sourceFile || !sourceFile->GetDescriptionPacket()) { nzbInfo->PrintMessage(Message::mkWarning, "Damaged par2-file detected: %s", FileSystem::BaseFileName(parFile)); return; } std::string filename = Par2::DiskFile::TranslateFilename(sourceFile->GetDescriptionPacket()->FileName()); std::string hash = sourceFile->GetDescriptionPacket()->Hash16k().print(); debug("file: %s, hash-16k: %s", filename.c_str(), hash.c_str()); m_parHashes.emplace_back(filename.c_str(), hash.c_str()); } } #endif std::unique_ptr DirectRenamer::MakeArticleContentAnalyzer() { return std::make_unique(); } void DirectRenamer::ArticleDownloaded(DownloadQueue* downloadQueue, FileInfo* fileInfo, ArticleInfo* articleInfo, ArticleContentAnalyzer* articleContentAnalyzer) { debug("Applying analyzer data %s for ", fileInfo->GetFilename()); RenameContentAnalyzer* contentAnalyzer = (RenameContentAnalyzer*)articleContentAnalyzer; contentAnalyzer->Finish(); NzbInfo* nzbInfo = fileInfo->GetNzbInfo(); // we don't support analyzing of files split into articles smaller than 16KB if (articleInfo->GetSize() >= 16 * 1024 || fileInfo->GetArticles()->size() == 1) { fileInfo->SetHash16k(contentAnalyzer->GetHash16k()); debug("file: %s; article-hash16k: %s", fileInfo->GetFilename(), fileInfo->GetHash16k()); } detail("Detected %s %s", (contentAnalyzer->GetParFile() ? "par2-file" : "non-par2-file"), fileInfo->GetFilename()); if (fileInfo->GetParFile() != contentAnalyzer->GetParFile()) { debug("Changing par2-flag for %s", fileInfo->GetFilename()); fileInfo->SetParFile(contentAnalyzer->GetParFile()); int delta = fileInfo->GetParFile() ? 1 : -1; nzbInfo->SetParSize(nzbInfo->GetParSize() + fileInfo->GetSize() * delta); nzbInfo->SetParCurrentSuccessSize(nzbInfo->GetParCurrentSuccessSize() + fileInfo->GetSuccessSize() * delta); nzbInfo->SetParCurrentFailedSize(nzbInfo->GetParCurrentFailedSize() + fileInfo->GetFailedSize() * delta + fileInfo->GetMissedSize() * delta); nzbInfo->SetRemainingParCount(nzbInfo->GetRemainingParCount() + 1 * delta); downloadQueue->Save(); } if (fileInfo->GetParFile()) { fileInfo->SetParSetId(contentAnalyzer->GetParSetId()); debug("file: %s; setid: %s", fileInfo->GetFilename(), fileInfo->GetParSetId()); } CheckState(downloadQueue, nzbInfo); } void DirectRenamer::FileDownloaded(DownloadQueue* downloadQueue, FileInfo* fileInfo) { CheckState(downloadQueue, fileInfo->GetNzbInfo()); } void DirectRenamer::CheckState(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) { #ifndef DISABLE_PARCHECK if (nzbInfo->GetDirectRenameStatus() > NzbInfo::tsRunning) { return; } // check if all first articles are successfully downloaded (1) for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (Util::EmptyStr(fileInfo->GetHash16k()) || (fileInfo->GetParFile() && Util::EmptyStr(fileInfo->GetParSetId()))) { return; } } // check if all first articles are successfully downloaded (2) for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { if (Util::EmptyStr(completedFile.GetHash16k()) || (completedFile.GetParFile() && Util::EmptyStr(completedFile.GetParSetId()))) { return; } } if (!nzbInfo->GetWaitingPar()) { // all first articles downloaded UnpausePars(nzbInfo); nzbInfo->SetWaitingPar(true); downloadQueue->Save(); } if (nzbInfo->GetWaitingPar() && !nzbInfo->GetLoadingPar()) { // check if all par2-files scheduled for downloading already completed FileList::iterator pos = std::find_if( nzbInfo->GetFileList()->begin(), nzbInfo->GetFileList()->end(), [](std::unique_ptr& fileInfo) { return fileInfo->GetExtraPriority(); }); if (pos == nzbInfo->GetFileList()->end()) { // all wanted par2-files are downloaded nzbInfo->SetLoadingPar(true); DirectParLoader::StartLoader(this, nzbInfo); return; } } #endif } // Unpause smallest par-files from each par-set void DirectRenamer::UnpausePars(NzbInfo* nzbInfo) { ParFileList parFiles; CollectPars(nzbInfo, &parFiles); std::vector parsets; // sort by size std::sort(parFiles.begin(), parFiles.end(), [nzbInfo](const ParFile& parFile1, const ParFile& parFile2) { FileInfo* fileInfo1 = nzbInfo->GetFileList()->Find(const_cast(parFile1).GetId()); FileInfo* fileInfo2 = nzbInfo->GetFileList()->Find(const_cast(parFile2).GetId()); return (!fileInfo1 && fileInfo2) || (fileInfo1 && fileInfo2 && fileInfo1->GetSize() < fileInfo2->GetSize()); }); // 1. count already downloaded files for (ParFile& parFile : parFiles) { if (parFile.GetCompleted()) { parsets.emplace_back(parFile.GetSetId()); } } // 2. find smallest par-file from each par-set from not yet completely downloaded files for (ParFile& parFile : parFiles) { std::vector::iterator pos = std::find(parsets.begin(), parsets.end(), parFile.GetSetId()); if (pos == parsets.end()) { // this par-set is not yet downloaded parsets.emplace_back(parFile.GetSetId()); FileInfo* fileInfo = nzbInfo->GetFileList()->Find(parFile.GetId()); if (fileInfo) { nzbInfo->PrintMessage(Message::mkDetail, "Increasing priority for par2-file %s", fileInfo->GetFilename()); fileInfo->SetPaused(false); fileInfo->SetExtraPriority(true); } } } } void DirectRenamer::CollectPars(NzbInfo* nzbInfo, ParFileList* parFiles) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (fileInfo->GetParFile()) { parFiles->emplace_back(fileInfo->GetId(), fileInfo->GetFilename(), fileInfo->GetParSetId(), false); } } for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { if (completedFile.GetParFile()) { parFiles->emplace_back(completedFile.GetId(), completedFile.GetFilename(), completedFile.GetParSetId(), true); } } } void DirectRenamer::RenameFiles(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, FileHashList* parHashes) { int renamedCount = 0; bool renamePars = NeedRenamePars(nzbInfo); int vol = 1; // rename in-progress files for (FileInfo* fileInfo : nzbInfo->GetFileList()) { CString newName; if (fileInfo->GetParFile() && renamePars) { newName = BuildNewParName(fileInfo->GetFilename(), nzbInfo->GetDestDir(), fileInfo->GetParSetId(), vol); } else if (!fileInfo->GetParFile()) { newName = BuildNewRegularName(fileInfo->GetFilename(), parHashes, fileInfo->GetHash16k()); } if (newName) { bool written = fileInfo->GetOutputFilename() && !Util::EndsWith(fileInfo->GetOutputFilename(), ".out.tmp", true); if (!written) { nzbInfo->PrintMessage(Message::mkInfo, "Renaming in-progress file %s to %s", fileInfo->GetFilename(), *newName); fileInfo->SetFilename(newName); fileInfo->SetFilenameConfirmed(true); renamedCount++; } else if (RenameCompletedFile(nzbInfo, fileInfo->GetFilename(), newName)) { fileInfo->SetFilename(newName); fileInfo->SetFilenameConfirmed(true); renamedCount++; } } } // rename completed files for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { CString newName; if (completedFile.GetParFile() && renamePars) { newName = BuildNewParName(completedFile.GetFilename(), nzbInfo->GetDestDir(), completedFile.GetParSetId(), vol); } else if (!completedFile.GetParFile()) { newName = BuildNewRegularName(completedFile.GetFilename(), parHashes, completedFile.GetHash16k()); } if (newName && RenameCompletedFile(nzbInfo, completedFile.GetFilename(), newName)) { completedFile.SetFilename(newName); renamedCount++; } } if (renamedCount > 0) { nzbInfo->PrintMessage(Message::mkInfo, "Successfully renamed %i file(s) for %s", renamedCount, nzbInfo->GetName()); } else { nzbInfo->PrintMessage(Message::mkInfo, "No renamed files found for %s", nzbInfo->GetName()); } RenameCompleted(downloadQueue, nzbInfo); } CString DirectRenamer::BuildNewRegularName(const char* oldName, FileHashList* parHashes, const char* hash16k) { if (Util::EmptyStr(hash16k)) { return nullptr; } FileHashList::iterator pos = std::find_if(parHashes->begin(), parHashes->end(), [hash16k](FileHash& parHash) { return !strcmp(parHash.GetHash(), hash16k); }); if (pos != parHashes->end()) { FileHash& parHash = *pos; if (strcasecmp(oldName, parHash.GetFilename())) { return parHash.GetFilename(); } } return nullptr; } CString DirectRenamer::BuildNewParName(const char* oldName, const char* destDir, const char* setId, int& vol) { BString<1024> newName; BString<1024> destFileName; // trying to reuse file suffix const char* suffix = strstr(oldName, ".vol"); const char* extension = suffix ? strrchr(suffix, '.') : nullptr; if (suffix && extension && !strcasecmp(extension, ".par2")) { newName.Format("%s%s", setId, suffix); destFileName.Format("%s%c%s", destDir, PATH_SEPARATOR, *newName); } while (destFileName.Empty() || FileSystem::FileExists(destFileName)) { newName.Format("%s.vol%03i+01.PAR2", setId, vol); destFileName.Format("%s%c%s", destDir, PATH_SEPARATOR, *newName); vol++; } return *newName; } bool DirectRenamer::NeedRenamePars(NzbInfo* nzbInfo) { // renaming is needed if par2-files from same par-set have different base names // or if any par2-file has non .par2-extension ParFileList parFiles; CollectPars(nzbInfo, &parFiles); for (ParFile& parFile : parFiles) { if (!Util::EndsWith(parFile.GetFilename(), ".par2", false)) { return true; } for (ParFile& parFile2 : parFiles) { if (&parFile != &parFile2 && !strcmp(parFile.GetSetId(), parFile2.GetSetId()) && !ParParser::SameParCollection(parFile.GetFilename(), parFile2.GetFilename(), false)) { return true; } } } return false; } bool DirectRenamer::RenameCompletedFile(NzbInfo* nzbInfo, const char* oldName, const char* newName) { BString<1024> oldFullFilename("%s%c%s", nzbInfo->GetDestDir(), PATH_SEPARATOR, oldName); BString<1024> newFullFilename("%s%c%s", nzbInfo->GetDestDir(), PATH_SEPARATOR, newName); nzbInfo->PrintMessage(Message::mkInfo, "Renaming completed file %s to %s", oldName, newName); if (!FileSystem::MoveFile(oldFullFilename, newFullFilename)) { nzbInfo->PrintMessage(Message::mkError, "Could not rename completed file %s to %s: %s", *oldFullFilename, *newFullFilename, *FileSystem::GetLastErrorMessage()); return false; } return true; } void RenameContentAnalyzer::Reset() { #ifndef DISABLE_PARCHECK m_md5Context.Reset(); #endif m_dataSize = 0; } void RenameContentAnalyzer::Append(const void* buffer, int len) { #ifndef DISABLE_PARCHECK if (m_dataSize < sizeof(m_signature)) { memcpy(m_signature + m_dataSize, buffer, std::min((size_t)len, sizeof(m_signature) - m_dataSize)); } if (m_dataSize >= sizeof(m_signature) && (*(Par2::MAGIC*)m_signature) == Par2::packet_magic) { m_parFile = true; m_parSetId = ((Par2::PACKET_HEADER*)m_signature)->setid.print().c_str(); } int rem16kSize = std::min(len, 16 * 1024 - m_dataSize); if (rem16kSize > 0) { m_md5Context.Update(buffer, rem16kSize); } m_dataSize += len; #endif } // Must be called with locked DownloadQueue void RenameContentAnalyzer::Finish() { #ifndef DISABLE_PARCHECK Par2::MD5Hash hash; m_md5Context.Final(hash); m_hash16k = hash.print().c_str(); #endif } nzbget-19.1/daemon/queue/QueueCoordinator.cpp0000644000175000017500000013253213130203062021130 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2005 Bo Cordes Petersen * Copyright (C) 2007-2017 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, see . */ #include "nzbget.h" #include "QueueCoordinator.h" #include "Options.h" #include "ServerPool.h" #include "ArticleDownloader.h" #include "ArticleWriter.h" #include "DiskState.h" #include "Util.h" #include "FileSystem.h" #include "Decoder.h" #include "StatMeter.h" bool QueueCoordinator::CoordinatorDownloadQueue::EditEntry( int ID, EEditAction action, const char* args) { return m_owner->m_queueEditor.EditEntry(&m_owner->m_downloadQueue, ID, action, args); } bool QueueCoordinator::CoordinatorDownloadQueue::EditList( IdList* idList, NameList* nameList, EMatchMode matchMode, EEditAction action, const char* args) { m_massEdit = true; bool ret = m_owner->m_queueEditor.EditList(&m_owner->m_downloadQueue, idList, nameList, matchMode, action, args); m_massEdit = false; if (m_wantSave) { Save(); } return ret; } void QueueCoordinator::CoordinatorDownloadQueue::Save() { if (m_massEdit) { m_wantSave = true; return; } if (g_Options->GetSaveQueue() && g_Options->GetServerMode()) { g_DiskState->SaveDownloadQueue(this, m_historyChanged); } for (NzbInfo* nzbInfo : GetQueue()) { nzbInfo->SetChanged(false); } m_wantSave = false; m_historyChanged = false; } QueueCoordinator::QueueCoordinator() { debug("Creating QueueCoordinator"); CoordinatorDownloadQueue::Init(&m_downloadQueue); } QueueCoordinator::~QueueCoordinator() { debug("Destroying QueueCoordinator"); for (ArticleDownloader* articleDownloader : m_activeDownloads) { delete articleDownloader; } m_activeDownloads.clear(); CoordinatorDownloadQueue::Final(); debug("QueueCoordinator destroyed"); } void QueueCoordinator::Load() { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); bool statLoaded = true; bool perfectServerMatch = true; bool queueLoaded = false; if (g_Options->GetServerMode() && g_Options->GetSaveQueue()) { statLoaded = g_StatMeter->Load(&perfectServerMatch); if (g_Options->GetReloadQueue() && g_DiskState->DownloadQueueExists()) { queueLoaded = g_DiskState->LoadDownloadQueue(downloadQueue, g_ServerPool->GetServers()); } else { g_DiskState->DiscardDownloadQueue(); } } if (queueLoaded && statLoaded) { g_DiskState->CleanupTempDir(downloadQueue); } if (queueLoaded && statLoaded && !perfectServerMatch) { debug("Changes in section of config file detected, resaving queue"); // re-save current server list into diskstate to update server ids g_StatMeter->Save(); // re-save queue into diskstate to update server ids downloadQueue->HistoryChanged(); downloadQueue->Save(); // re-save file states into diskstate to update server ids if (g_Options->GetServerMode() && g_Options->GetSaveQueue()) { for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { if (g_Options->GetContinuePartial()) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (!fileInfo->GetArticles()->empty()) { g_DiskState->SaveFileState(fileInfo, false); } } } for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { if ((completedFile.GetStatus() == CompletedFile::cfPartial || completedFile.GetStatus() == CompletedFile::cfFailure) && completedFile.GetId() > 0) { FileInfo fileInfo(completedFile.GetId()); if (g_DiskState->LoadFileState(&fileInfo, g_ServerPool->GetServers(), true)) { g_DiskState->SaveFileState(&fileInfo, true); } } } } } } CoordinatorDownloadQueue::Loaded(); } void QueueCoordinator::Run() { debug("Entering QueueCoordinator-loop"); Load(); AdjustDownloadsLimit(); bool wasStandBy = true; bool articeDownloadsRunning = false; int resetCounter = 0; g_StatMeter->IntervalCheck(); while (!IsStopped()) { bool downloadsChecked = false; bool downloadStarted = false; NntpConnection* connection = g_ServerPool->GetConnection(0, nullptr, nullptr); if (connection) { // start download for next article FileInfo* fileInfo; ArticleInfo* articleInfo; bool freeConnection = false; { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); bool hasMoreArticles = GetNextArticle(downloadQueue, fileInfo, articleInfo); articeDownloadsRunning = !m_activeDownloads.empty(); downloadsChecked = true; m_hasMoreJobs = hasMoreArticles || articeDownloadsRunning; if (hasMoreArticles && !IsStopped() && (int)m_activeDownloads.size() < m_downloadsLimit && (!g_Options->GetTempPauseDownload() || fileInfo->GetExtraPriority())) { StartArticleDownload(fileInfo, articleInfo, connection); articeDownloadsRunning = true; downloadStarted = true; } else { freeConnection = true; } } if (freeConnection) { g_ServerPool->FreeConnection(connection, false); } } if (!downloadsChecked) { GuardedDownloadQueue guard = DownloadQueue::Guard(); articeDownloadsRunning = !m_activeDownloads.empty(); } bool standBy = !articeDownloadsRunning; if (standBy != wasStandBy) { g_StatMeter->EnterLeaveStandBy(standBy); wasStandBy = standBy; if (standBy) { SaveAllPartialState(); } } // sleep longer in StandBy int sleepInterval = downloadStarted ? 0 : standBy ? 100 : 5; usleep(sleepInterval * 1000); if (!standBy) { g_StatMeter->AddSpeedReading(0); } Util::SetStandByMode(standBy); resetCounter += sleepInterval; if (resetCounter >= 1000) { // this code should not be called too often, once per second is OK g_ServerPool->CloseUnusedConnections(); ResetHangingDownloads(); if (!standBy) { SaveAllPartialState(); } resetCounter = 0; g_StatMeter->IntervalCheck(); AdjustDownloadsLimit(); } } WaitJobs(); SaveAllPartialState(); debug("Exiting QueueCoordinator-loop"); } void QueueCoordinator::WaitJobs() { // waiting for downloads debug("QueueCoordinator: waiting for Downloads to complete"); while (true) { { GuardedDownloadQueue guard = DownloadQueue::Guard(); if (m_activeDownloads.empty()) { break; } } usleep(100 * 1000); ResetHangingDownloads(); } debug("QueueCoordinator: Downloads are completed"); } /* * Compute maximum number of allowed download threads **/ void QueueCoordinator::AdjustDownloadsLimit() { if (m_serverConfigGeneration == g_ServerPool->GetGeneration()) { return; } // two extra threads for completing files (when connections are not needed) int downloadsLimit = 2; // allow one thread per 0-level (main) and 1-level (backup) server connection for (NewsServer* newsServer : g_ServerPool->GetServers()) { if ((newsServer->GetNormLevel() == 0 || newsServer->GetNormLevel() == 1) && newsServer->GetActive()) { downloadsLimit += newsServer->GetMaxConnections(); } } m_downloadsLimit = downloadsLimit; } NzbInfo* QueueCoordinator::AddNzbFileToQueue(std::unique_ptr nzbInfo, NzbInfo* urlInfo, bool addFirst) { debug("Adding NZBFile to queue"); NzbInfo* addedNzb = nzbInfo.get(); GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); DownloadQueue::Aspect foundAspect = { DownloadQueue::eaNzbFound, downloadQueue, nzbInfo.get(), nullptr }; downloadQueue->Notify(&foundAspect); NzbInfo::EDeleteStatus deleteStatus = nzbInfo->GetDeleteStatus(); if (deleteStatus != NzbInfo::dsNone) { bool allPaused = !nzbInfo->GetFileList()->empty(); for (FileInfo* fileInfo: nzbInfo->GetFileList()) { allPaused &= fileInfo->GetPaused(); if (g_Options->GetSaveQueue() && g_Options->GetServerMode()) { g_DiskState->DiscardFile(fileInfo->GetId(), true, false, false); } } nzbInfo->SetDeletePaused(allPaused); } if (deleteStatus == NzbInfo::dsNone) { if (g_Options->GetDupeCheck() && nzbInfo->GetDupeMode() != dmForce) { CheckDupeFileInfos(nzbInfo.get()); } if (urlInfo) { // insert at the URL position downloadQueue->GetQueue()->insert(downloadQueue->GetQueue()->Find(urlInfo), std::move(nzbInfo)); } else { downloadQueue->GetQueue()->Add(std::move(nzbInfo), addFirst); } } else { // temporary adding to queue in order for listeners to see it downloadQueue->GetQueue()->Add(std::move(nzbInfo), true); } if (urlInfo) { addedNzb->SetId(urlInfo->GetId()); downloadQueue->GetQueue()->Remove(urlInfo); } if (deleteStatus == NzbInfo::dsNone) { addedNzb->PrintMessage(Message::mkInfo, "Collection %s added to queue", addedNzb->GetName()); } if (deleteStatus != NzbInfo::dsManual) { DownloadQueue::Aspect addedAspect = { DownloadQueue::eaNzbAdded, downloadQueue, addedNzb, nullptr }; downloadQueue->Notify(&addedAspect); } if (deleteStatus != NzbInfo::dsNone) { // in a case if none of listeners did already delete the temporary object - we do it ourselves downloadQueue->GetQueue()->Remove(addedNzb); if (!downloadQueue->GetHistory()->Find(addedNzb->GetId())) { addedNzb = nullptr; } } downloadQueue->Save(); return addedNzb; } void QueueCoordinator::CheckDupeFileInfos(NzbInfo* nzbInfo) { debug("CheckDupeFileInfos"); if (!g_Options->GetDupeCheck() || nzbInfo->GetDupeMode() == dmForce) { return; } RawFileList dupeList; int index1 = 0; for (FileInfo* fileInfo : nzbInfo->GetFileList()) { index1++; bool dupe = false; int index2 = 0; for (FileInfo* fileInfo2 : nzbInfo->GetFileList()) { index2++; if (fileInfo != fileInfo2 && !strcmp(fileInfo->GetFilename(), fileInfo2->GetFilename()) && (fileInfo->GetSize() < fileInfo2->GetSize() || (fileInfo->GetSize() == fileInfo2->GetSize() && index2 < index1))) { warn("File \"%s\" appears twice in collection, adding only the biggest file", fileInfo->GetFilename()); dupe = true; break; } } if (dupe) { dupeList.push_back(fileInfo); continue; } } for (FileInfo* fileInfo : dupeList) { nzbInfo->UpdateDeletedStats(fileInfo); nzbInfo->GetFileList()->Remove(fileInfo); if (g_Options->GetSaveQueue() && g_Options->GetServerMode()) { g_DiskState->DiscardFile(fileInfo->GetId(), true, false, false); } } } void QueueCoordinator::Stop() { Thread::Stop(); debug("Stopping ArticleDownloads"); GuardedDownloadQueue guard = DownloadQueue::Guard(); for (ArticleDownloader* articleDownloader : m_activeDownloads) { articleDownloader->Stop(); } debug("ArticleDownloads are notified"); } /* * Returns next article for download. */ bool QueueCoordinator::GetNextArticle(DownloadQueue* downloadQueue, FileInfo* &fileInfo, ArticleInfo* &articleInfo) { // find an unpaused file with the highest priority, then take the next article from the file. // if the file doesn't have any articles left for download, we store that fact and search again, // ignoring all files which were previously marked as not having any articles. // special case: if the file has ExtraPriority-flag set, it has the highest priority and the // Paused-flag is ignored. //debug("QueueCoordinator::GetNextArticle()"); bool ok = false; RawFileList checkedFiles; time_t curDate = Util::CurrentTime(); while (!ok) { fileInfo = nullptr; for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { for (FileInfo* fileInfo1 : nzbInfo->GetFileList()) { if ((checkedFiles.empty() || std::find(checkedFiles.begin(), checkedFiles.end(), fileInfo1) == checkedFiles.end()) && !fileInfo1->GetPaused() && !fileInfo1->GetDeleted() && (g_Options->GetPropagationDelay() == 0 || (int)fileInfo1->GetTime() < (int)curDate - g_Options->GetPropagationDelay()) && (!(g_Options->GetPauseDownload() || g_Options->GetQuotaReached()) || nzbInfo->GetForcePriority()) && (!fileInfo || (fileInfo1->GetExtraPriority() == fileInfo->GetExtraPriority() && fileInfo1->GetNzbInfo()->GetPriority() > fileInfo->GetNzbInfo()->GetPriority()) || (fileInfo1->GetExtraPriority() > fileInfo->GetExtraPriority()))) { fileInfo = fileInfo1; } } } if (!fileInfo) { // there are no more files for download break; } if (g_Options->GetDirectRename() && fileInfo->GetNzbInfo()->GetDirectRenameStatus() <= NzbInfo::tsRunning && !fileInfo->GetNzbInfo()->GetAllFirst() && GetNextFirstArticle(fileInfo->GetNzbInfo(), fileInfo, articleInfo)) { return true; } if (fileInfo->GetArticles()->empty() && g_Options->GetSaveQueue() && g_Options->GetServerMode()) { g_DiskState->LoadArticles(fileInfo); LoadPartialState(fileInfo); } // check if the file has any articles left for download for (ArticleInfo* article : fileInfo->GetArticles()) { if (article->GetStatus() == ArticleInfo::aiUndefined) { articleInfo = article; return true; } } if (!ok) { // the file doesn't have any articles left for download checkedFiles.reserve(100); checkedFiles.push_back(fileInfo); } } return false; } bool QueueCoordinator::GetNextFirstArticle(NzbInfo* nzbInfo, FileInfo* &fileInfo, ArticleInfo* &articleInfo) { // find a file not renamed yet for (FileInfo* fileInfo1 : nzbInfo->GetFileList()) { if (!fileInfo1->GetFilenameConfirmed()) { if (fileInfo1->GetArticles()->empty() && g_Options->GetSaveQueue() && g_Options->GetServerMode()) { g_DiskState->LoadArticles(fileInfo1); LoadPartialState(fileInfo1); } if (!fileInfo1->GetArticles()->empty()) { ArticleInfo* article = fileInfo1->GetArticles()->at(0).get(); if (article->GetStatus() == ArticleInfo::aiUndefined) { fileInfo = fileInfo1; articleInfo = article; nzbInfo->SetDirectRenameStatus(NzbInfo::tsRunning); return true; } } } } // no more files for renaming remained nzbInfo->SetAllFirst(true); return false; } void QueueCoordinator::StartArticleDownload(FileInfo* fileInfo, ArticleInfo* articleInfo, NntpConnection* connection) { debug("Starting new ArticleDownloader"); ArticleDownloader* articleDownloader = new ArticleDownloader(); articleDownloader->SetAutoDestroy(true); articleDownloader->Attach(this); articleDownloader->SetFileInfo(fileInfo); articleDownloader->SetArticleInfo(articleInfo); articleDownloader->SetConnection(connection); if (articleInfo->GetPartNumber() == 1 && g_Options->GetDirectRename() && g_Options->GetDecode()) { articleDownloader->SetContentAnalyzer(m_directRenamer.MakeArticleContentAnalyzer()); } BString<1024> infoName("%s%c%s [%i/%i]", fileInfo->GetNzbInfo()->GetName(), PATH_SEPARATOR, fileInfo->GetFilename(), articleInfo->GetPartNumber(), (int)fileInfo->GetArticles()->size()); articleDownloader->SetInfoName(infoName); articleInfo->SetStatus(ArticleInfo::aiRunning); fileInfo->SetActiveDownloads(fileInfo->GetActiveDownloads() + 1); fileInfo->GetNzbInfo()->SetActiveDownloads(fileInfo->GetNzbInfo()->GetActiveDownloads() + 1); m_activeDownloads.push_back(articleDownloader); articleDownloader->Start(); } void QueueCoordinator::Update(Subject* Caller, void* Aspect) { debug("Notification from ArticleDownloader received"); ArticleDownloader* articleDownloader = (ArticleDownloader*)Caller; if ((articleDownloader->GetStatus() == ArticleDownloader::adFinished) || (articleDownloader->GetStatus() == ArticleDownloader::adFailed) || (articleDownloader->GetStatus() == ArticleDownloader::adRetry)) { ArticleCompleted(articleDownloader); } } void QueueCoordinator::ArticleCompleted(ArticleDownloader* articleDownloader) { debug("Article downloaded"); FileInfo* fileInfo = articleDownloader->GetFileInfo(); bool completeFileParts = false; { NzbInfo* nzbInfo = fileInfo->GetNzbInfo(); ArticleInfo* articleInfo = articleDownloader->GetArticleInfo(); bool retry = false; bool fileCompleted = false; GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); if (articleDownloader->GetStatus() == ArticleDownloader::adFinished) { articleInfo->SetStatus(ArticleInfo::aiFinished); fileInfo->SetSuccessSize(fileInfo->GetSuccessSize() + articleInfo->GetSize()); nzbInfo->SetCurrentSuccessSize(nzbInfo->GetCurrentSuccessSize() + articleInfo->GetSize()); nzbInfo->SetParCurrentSuccessSize(nzbInfo->GetParCurrentSuccessSize() + (fileInfo->GetParFile() ? articleInfo->GetSize() : 0)); fileInfo->SetSuccessArticles(fileInfo->GetSuccessArticles() + 1); nzbInfo->SetCurrentSuccessArticles(nzbInfo->GetCurrentSuccessArticles() + 1); } else if (articleDownloader->GetStatus() == ArticleDownloader::adFailed) { articleInfo->SetStatus(ArticleInfo::aiFailed); fileInfo->SetFailedSize(fileInfo->GetFailedSize() + articleInfo->GetSize()); nzbInfo->SetCurrentFailedSize(nzbInfo->GetCurrentFailedSize() + articleInfo->GetSize()); nzbInfo->SetParCurrentFailedSize(nzbInfo->GetParCurrentFailedSize() + (fileInfo->GetParFile() ? articleInfo->GetSize() : 0)); fileInfo->SetFailedArticles(fileInfo->GetFailedArticles() + 1); nzbInfo->SetCurrentFailedArticles(nzbInfo->GetCurrentFailedArticles() + 1); } else if (articleDownloader->GetStatus() == ArticleDownloader::adRetry) { articleInfo->SetStatus(ArticleInfo::aiUndefined); retry = true; if (articleInfo->GetPartNumber() == 1) { nzbInfo->SetAllFirst(false); } } if (!retry) { fileInfo->SetRemainingSize(fileInfo->GetRemainingSize() - articleInfo->GetSize()); nzbInfo->SetRemainingSize(nzbInfo->GetRemainingSize() - articleInfo->GetSize()); if (fileInfo->GetPaused()) { nzbInfo->SetPausedSize(nzbInfo->GetPausedSize() - articleInfo->GetSize()); } fileInfo->SetCompletedArticles(fileInfo->GetCompletedArticles() + 1); fileCompleted = (int)fileInfo->GetArticles()->size() == fileInfo->GetCompletedArticles(); fileInfo->GetServerStats()->ListOp(articleDownloader->GetServerStats(), ServerStatList::soAdd); nzbInfo->GetCurrentServerStats()->ListOp(articleDownloader->GetServerStats(), ServerStatList::soAdd); fileInfo->SetPartialChanged(true); } if (!fileInfo->GetFilenameConfirmed() && articleDownloader->GetStatus() == ArticleDownloader::adFinished && articleDownloader->GetArticleFilename()) { // in "FileNaming=auto"-mode prefer filename from nzb-file to filename read from article // if the name from article seems to be obfuscated bool useFilenameFromArticle = g_Options->GetFileNaming() == Options::nfArticle || (g_Options->GetFileNaming() == Options::nfAuto && !Util::AlphaNum(articleDownloader->GetArticleFilename()) && !nzbInfo->GetManyDupeFiles()); if (useFilenameFromArticle) { fileInfo->SetFilename(articleDownloader->GetArticleFilename()); fileInfo->MakeValidFilename(); } fileInfo->SetFilenameConfirmed(true); if (g_Options->GetDupeCheck() && nzbInfo->GetDupeMode() != dmForce && !nzbInfo->GetManyDupeFiles() && FileSystem::FileExists(BString<1024>("%s%c%s", nzbInfo->GetDestDir(), PATH_SEPARATOR, fileInfo->GetFilename()))) { warn("File \"%s\" seems to be duplicate, cancelling download and deleting file from queue", fileInfo->GetFilename()); fileCompleted = false; fileInfo->SetDupeDeleted(true); DeleteQueueEntry(downloadQueue, fileInfo); } } if (articleDownloader->GetContentAnalyzer() && articleDownloader->GetStatus() == ArticleDownloader::adFinished) { m_directRenamer.ArticleDownloaded(downloadQueue, fileInfo, articleInfo, articleDownloader->GetContentAnalyzer()); } nzbInfo->SetDownloadedSize(nzbInfo->GetDownloadedSize() + articleDownloader->GetDownloadedSize()); CheckHealth(downloadQueue, fileInfo); if (nzbInfo->GetParking() && fileInfo->GetActiveDownloads() == 1 && !fileInfo->GetDupeDeleted()) { fileCompleted = true; } completeFileParts = fileCompleted && (!fileInfo->GetDeleted() || nzbInfo->GetParking()); if (!completeFileParts) { DeleteDownloader(downloadQueue, articleDownloader, false); } } if (completeFileParts) { // all jobs done articleDownloader->CompleteFileParts(); fileInfo->SetPartialChanged(false); GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); DeleteDownloader(downloadQueue, articleDownloader, true); } } void QueueCoordinator::DeleteDownloader(DownloadQueue* downloadQueue, ArticleDownloader* articleDownloader, bool fileCompleted) { FileInfo* fileInfo = articleDownloader->GetFileInfo(); NzbInfo* nzbInfo = fileInfo->GetNzbInfo(); bool hasOtherDownloaders = fileInfo->GetActiveDownloads() > 1; bool deleteFileObj = fileCompleted || (fileInfo->GetDeleted() && !hasOtherDownloaders); // remove downloader from downloader list m_activeDownloads.erase(std::find(m_activeDownloads.begin(), m_activeDownloads.end(), articleDownloader)); fileInfo->SetActiveDownloads(fileInfo->GetActiveDownloads() - 1); nzbInfo->SetActiveDownloads(nzbInfo->GetActiveDownloads() - 1); if (deleteFileObj) { DeleteFileInfo(downloadQueue, fileInfo, fileCompleted); downloadQueue->Save(); } } void QueueCoordinator::DeleteFileInfo(DownloadQueue* downloadQueue, FileInfo* fileInfo, bool completed) { while (g_ArticleCache->FileBusy(fileInfo)) { usleep(5*1000); } NzbInfo* nzbInfo = fileInfo->GetNzbInfo(); bool parking = fileInfo->GetNzbInfo()->GetParking(); bool fileDeleted = fileInfo->GetDeleted(); fileInfo->SetDeleted(true); if (completed || nzbInfo->GetDeleting()) { nzbInfo->UpdateCompletedStats(fileInfo); } else { nzbInfo->UpdateDeletedStats(fileInfo); } CompletedFile::EStatus fileStatus = fileInfo->GetTotalArticles() == fileInfo->GetSuccessArticles() ? CompletedFile::cfSuccess : fileInfo->GetTotalArticles() == fileInfo->GetMissedArticles() + fileInfo->GetFailedArticles() ? CompletedFile::cfFailure : fileInfo->GetSuccessArticles() > 0 || fileInfo->GetFailedArticles() > 0 ? CompletedFile::cfPartial : CompletedFile::cfNone; if (g_Options->GetSaveQueue() && g_Options->GetServerMode()) { g_DiskState->DiscardFile(fileInfo->GetId(), fileStatus == CompletedFile::cfSuccess || (fileDeleted && !parking), true, false); if (fileStatus == CompletedFile::cfPartial && (completed || parking)) { g_DiskState->SaveFileState(fileInfo, true); } } if (!completed) { DiscardTempFiles(fileInfo); } if (completed || parking) { fileInfo->GetNzbInfo()->GetCompletedFiles()->emplace_back( fileInfo->GetId(), completed && fileInfo->GetOutputFilename() ? FileSystem::BaseFileName(fileInfo->GetOutputFilename()) : fileInfo->GetFilename(), fileStatus, fileStatus == CompletedFile::cfSuccess ? fileInfo->GetCrc() : 0, fileInfo->GetParFile(), fileInfo->GetHash16k(), fileInfo->GetParSetId()); } if (g_Options->GetDirectRename()) { m_directRenamer.FileDownloaded(downloadQueue, fileInfo); } if (nzbInfo->GetDirectRenameStatus() == NzbInfo::tsRunning && !nzbInfo->GetDeleting() && nzbInfo->IsDownloadCompleted(true)) { DiscardDirectRename(downloadQueue, nzbInfo); } std::unique_ptr srcFileInfo = nzbInfo->GetFileList()->Remove(fileInfo); DownloadQueue::Aspect aspect = { completed && !fileDeleted ? DownloadQueue::eaFileCompleted : DownloadQueue::eaFileDeleted, downloadQueue, nzbInfo, fileInfo }; downloadQueue->Notify(&aspect); // now can destroy FileInfo srcFileInfo.reset(); } void QueueCoordinator::DiscardTempFiles(FileInfo* fileInfo) { if (!g_Options->GetDirectWrite() && !fileInfo->GetForceDirectWrite()) { for (ArticleInfo* pa : fileInfo->GetArticles()) { if (pa->GetResultFilename()) { FileSystem::DeleteFile(pa->GetResultFilename()); } } } if (g_Options->GetDirectWrite() && fileInfo->GetOutputFilename() && !fileInfo->GetForceDirectWrite()) { FileSystem::DeleteFile(fileInfo->GetOutputFilename()); } } void QueueCoordinator::SaveAllPartialState() { if (!(g_Options->GetServerMode() && g_Options->GetSaveQueue())) { return; } bool hasUnsavedData = false; GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { if (g_Options->GetContinuePartial()) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { SavePartialState(fileInfo); } } hasUnsavedData |= nzbInfo->GetChanged(); } if (hasUnsavedData) { downloadQueue->Save(); } } void QueueCoordinator::SavePartialState(FileInfo* fileInfo) { if (fileInfo->GetPartialChanged()) { debug("Saving partial state for %s", fileInfo->GetFilename()); if (fileInfo->GetPartialState() == FileInfo::psCompleted) { g_DiskState->DiscardFile(fileInfo->GetId(), false, false, true); } g_DiskState->SaveFileState(fileInfo, false); fileInfo->SetPartialChanged(false); fileInfo->SetPartialState(FileInfo::psPartial); } } void QueueCoordinator::LoadPartialState(FileInfo* fileInfo) { if (fileInfo->GetPartialState() == FileInfo::psPartial) { g_DiskState->LoadFileState(fileInfo, g_ServerPool->GetServers(), false); } else if (fileInfo->GetPartialState() == FileInfo::psCompleted) { g_DiskState->LoadFileState(fileInfo, g_ServerPool->GetServers(), true); BString<1024> outputFilename("%s%c%s", fileInfo->GetNzbInfo()->GetDestDir(), PATH_SEPARATOR, fileInfo->GetFilename()); fileInfo->SetOutputFilename(outputFilename); fileInfo->SetOutputInitialized(true); fileInfo->SetForceDirectWrite(true); fileInfo->SetFilenameConfirmed(true); } } void QueueCoordinator::CheckHealth(DownloadQueue* downloadQueue, FileInfo* fileInfo) { if (g_Options->GetHealthCheck() == Options::hcNone || fileInfo->GetNzbInfo()->GetHealthPaused() || fileInfo->GetNzbInfo()->GetDeleteStatus() == NzbInfo::dsHealth || fileInfo->GetNzbInfo()->CalcHealth() >= fileInfo->GetNzbInfo()->CalcCriticalHealth(true) || (g_Options->GetParScan() == Options::psDupe && g_Options->GetHealthCheck() == Options::hcPark && fileInfo->GetNzbInfo()->GetSuccessArticles() * 100 / fileInfo->GetNzbInfo()->GetTotalArticles() > 10)) { return; } if (g_Options->GetHealthCheck() == Options::hcPause) { warn("Pausing %s due to health %.1f%% below critical %.1f%%", fileInfo->GetNzbInfo()->GetName(), fileInfo->GetNzbInfo()->CalcHealth() / 10.0, fileInfo->GetNzbInfo()->CalcCriticalHealth(true) / 10.0); fileInfo->GetNzbInfo()->SetHealthPaused(true); downloadQueue->EditEntry(fileInfo->GetNzbInfo()->GetId(), DownloadQueue::eaGroupPause, nullptr); } else if (g_Options->GetHealthCheck() == Options::hcDelete || g_Options->GetHealthCheck() == Options::hcPark) { fileInfo->GetNzbInfo()->PrintMessage(Message::mkWarning, "Cancelling download and deleting %s due to health %.1f%% below critical %.1f%%", fileInfo->GetNzbInfo()->GetName(), fileInfo->GetNzbInfo()->CalcHealth() / 10.0, fileInfo->GetNzbInfo()->CalcCriticalHealth(true) / 10.0); fileInfo->GetNzbInfo()->SetDeleteStatus(NzbInfo::dsHealth); downloadQueue->EditEntry(fileInfo->GetNzbInfo()->GetId(), g_Options->GetHealthCheck() == Options::hcPark ? DownloadQueue::eaGroupParkDelete : DownloadQueue::eaGroupDelete, nullptr); } } void QueueCoordinator::LogDebugInfo() { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); info(" ---------- Queue"); int64 remaining, remainingForced; downloadQueue->CalcRemainingSize(&remaining, &remainingForced); info(" Remaining: %.1f MB, Forced: %.1f MB", remaining / 1024.0 / 1024.0, remainingForced / 1024.0 / 1024.0); info(" Download: %s, Post-process: %s, Scan: %s", (g_Options->GetPauseDownload() ? "paused" : g_Options->GetTempPauseDownload() ? "temp-paused" : "active"), (g_Options->GetPausePostProcess() ? "paused" : "active"), (g_Options->GetPauseScan() ? "paused" : "active")); info(" ---------- QueueCoordinator"); info(" Active Downloads: %i, Limit: %i", (int)m_activeDownloads.size(), m_downloadsLimit); for (ArticleDownloader* articleDownloader : m_activeDownloads) { articleDownloader->LogDebugInfo(); } } void QueueCoordinator::ResetHangingDownloads() { if (g_Options->GetTerminateTimeout() == 0 && g_Options->GetArticleTimeout() == 0) { return; } GuardedDownloadQueue guard = DownloadQueue::Guard(); time_t tm = Util::CurrentTime(); m_activeDownloads.erase(std::remove_if(m_activeDownloads.begin(), m_activeDownloads.end(), [tm](ArticleDownloader* articleDownloader) { if (tm - articleDownloader->GetLastUpdateTime() > g_Options->GetArticleTimeout() + 1 && articleDownloader->GetStatus() == ArticleDownloader::adRunning) { error("Cancelling hanging download %s @ %s", articleDownloader->GetInfoName(), articleDownloader->GetConnectionName()); articleDownloader->Stop(); } if (tm - articleDownloader->GetLastUpdateTime() > g_Options->GetTerminateTimeout() && articleDownloader->GetStatus() == ArticleDownloader::adRunning) { ArticleInfo* articleInfo = articleDownloader->GetArticleInfo(); debug("Terminating hanging download %s", articleDownloader->GetInfoName()); if (articleDownloader->Terminate()) { error("Terminated hanging download %s @ %s", articleDownloader->GetInfoName(), articleDownloader->GetConnectionName()); articleInfo->SetStatus(ArticleInfo::aiUndefined); } else { error("Could not terminate hanging download %s @ %s", articleDownloader->GetInfoName(), articleDownloader->GetConnectionName()); } articleDownloader->GetFileInfo()->SetActiveDownloads(articleDownloader->GetFileInfo()->GetActiveDownloads() - 1); articleDownloader->GetFileInfo()->GetNzbInfo()->SetActiveDownloads(articleDownloader->GetFileInfo()->GetNzbInfo()->GetActiveDownloads() - 1); articleDownloader->GetFileInfo()->GetNzbInfo()->SetDownloadedSize(articleDownloader->GetFileInfo()->GetNzbInfo()->GetDownloadedSize() + articleDownloader->GetDownloadedSize()); // it's not safe to destroy pArticleDownloader, because the state of object is unknown delete articleDownloader; return true; } return false; }), m_activeDownloads.end()); } /* * Returns True if Entry was deleted from Queue or False if it was scheduled for Deletion. * NOTE: "False" does not mean unsuccess; the entry is (or will be) deleted in any case. */ bool QueueCoordinator::DeleteQueueEntry(DownloadQueue* downloadQueue, FileInfo* fileInfo) { fileInfo->SetDeleted(true); bool downloading = false; for (ArticleDownloader* articleDownloader : m_activeDownloads) { if (articleDownloader->GetFileInfo() == fileInfo) { downloading = true; articleDownloader->Stop(); } } if (!downloading) { DeleteFileInfo(downloadQueue, fileInfo, false); } return downloading; } bool QueueCoordinator::SetQueueEntryCategory(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, const char* category) { if (nzbInfo->GetPostInfo()) { error("Could not change category for %s. File in post-process-stage", nzbInfo->GetName()); return false; } BString<1024> oldDestDir = nzbInfo->GetDestDir(); nzbInfo->SetCategory(category); nzbInfo->BuildDestDirName(); bool dirUnchanged = !strcmp(nzbInfo->GetDestDir(), oldDestDir); bool ok = dirUnchanged || ArticleWriter::MoveCompletedFiles(nzbInfo, oldDestDir); return ok; } bool QueueCoordinator::SetQueueEntryName(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, const char* name) { if (nzbInfo->GetPostInfo()) { error("Could not rename %s. File in post-process-stage", nzbInfo->GetName()); return false; } if (Util::EmptyStr(name)) { error("Could not rename %s. The new name cannot be empty", nzbInfo->GetName()); return false; } nzbInfo->SetName(NzbInfo::MakeNiceNzbName(name, false)); if (nzbInfo->GetKind() == NzbInfo::nkUrl) { nzbInfo->SetFilename(BString<1024>("%s.nzb", nzbInfo->GetName())); return true; } BString<1024> oldDestDir = nzbInfo->GetDestDir(); nzbInfo->BuildDestDirName(); bool dirUnchanged = !strcmp(nzbInfo->GetDestDir(), oldDestDir); bool ok = dirUnchanged || ArticleWriter::MoveCompletedFiles(nzbInfo, oldDestDir); return ok; } bool QueueCoordinator::MergeQueueEntries(DownloadQueue* downloadQueue, NzbInfo* destNzbInfo, NzbInfo* srcNzbInfo) { if (destNzbInfo->GetPostInfo() || srcNzbInfo->GetPostInfo()) { error("Could not merge %s and %s. File in post-process-stage", destNzbInfo->GetName(), srcNzbInfo->GetName()); return false; } if (destNzbInfo->GetKind() == NzbInfo::nkUrl || srcNzbInfo->GetKind() == NzbInfo::nkUrl) { error("Could not merge %s and %s. URLs cannot be merged", destNzbInfo->GetName(), srcNzbInfo->GetName()); return false; } // set new dest directory, new category and move downloaded files to new dest directory srcNzbInfo->SetFilename(srcNzbInfo->GetFilename()); SetQueueEntryCategory(downloadQueue, srcNzbInfo, destNzbInfo->GetCategory()); // reattach file items to new NZBInfo-object for (std::unique_ptr& fileInfo : *srcNzbInfo->GetFileList()) { fileInfo->SetNzbInfo(destNzbInfo); destNzbInfo->GetFileList()->Add(std::move(fileInfo)); } srcNzbInfo->GetFileList()->clear(); destNzbInfo->SetFileCount(destNzbInfo->GetFileCount() + srcNzbInfo->GetFileCount()); destNzbInfo->SetActiveDownloads(destNzbInfo->GetActiveDownloads() + srcNzbInfo->GetActiveDownloads()); destNzbInfo->SetFullContentHash(0); destNzbInfo->SetFilteredContentHash(0); destNzbInfo->SetSize(destNzbInfo->GetSize() + srcNzbInfo->GetSize()); destNzbInfo->SetRemainingSize(destNzbInfo->GetRemainingSize() + srcNzbInfo->GetRemainingSize()); destNzbInfo->SetPausedFileCount(destNzbInfo->GetPausedFileCount() + srcNzbInfo->GetPausedFileCount()); destNzbInfo->SetPausedSize(destNzbInfo->GetPausedSize() + srcNzbInfo->GetPausedSize()); destNzbInfo->SetSuccessSize(destNzbInfo->GetSuccessSize() + srcNzbInfo->GetSuccessSize()); destNzbInfo->SetCurrentSuccessSize(destNzbInfo->GetCurrentSuccessSize() + srcNzbInfo->GetCurrentSuccessSize()); destNzbInfo->SetFailedSize(destNzbInfo->GetFailedSize() + srcNzbInfo->GetFailedSize()); destNzbInfo->SetCurrentFailedSize(destNzbInfo->GetCurrentFailedSize() + srcNzbInfo->GetCurrentFailedSize()); destNzbInfo->SetParSize(destNzbInfo->GetParSize() + srcNzbInfo->GetParSize()); destNzbInfo->SetParSuccessSize(destNzbInfo->GetParSuccessSize() + srcNzbInfo->GetParSuccessSize()); destNzbInfo->SetParCurrentSuccessSize(destNzbInfo->GetParCurrentSuccessSize() + srcNzbInfo->GetParCurrentSuccessSize()); destNzbInfo->SetParFailedSize(destNzbInfo->GetParFailedSize() + srcNzbInfo->GetParFailedSize()); destNzbInfo->SetParCurrentFailedSize(destNzbInfo->GetParCurrentFailedSize() + srcNzbInfo->GetParCurrentFailedSize()); destNzbInfo->SetRemainingParCount(destNzbInfo->GetRemainingParCount() + srcNzbInfo->GetRemainingParCount()); destNzbInfo->SetTotalArticles(destNzbInfo->GetTotalArticles() + srcNzbInfo->GetTotalArticles()); destNzbInfo->SetSuccessArticles(destNzbInfo->GetSuccessArticles() + srcNzbInfo->GetSuccessArticles()); destNzbInfo->SetFailedArticles(destNzbInfo->GetFailedArticles() + srcNzbInfo->GetFailedArticles()); destNzbInfo->SetCurrentSuccessArticles(destNzbInfo->GetCurrentSuccessArticles() + srcNzbInfo->GetCurrentSuccessArticles()); destNzbInfo->SetCurrentFailedArticles(destNzbInfo->GetCurrentFailedArticles() + srcNzbInfo->GetCurrentFailedArticles()); destNzbInfo->GetServerStats()->ListOp(srcNzbInfo->GetServerStats(), ServerStatList::soAdd); destNzbInfo->GetCurrentServerStats()->ListOp(srcNzbInfo->GetCurrentServerStats(), ServerStatList::soAdd); destNzbInfo->SetMinTime(srcNzbInfo->GetMinTime() < destNzbInfo->GetMinTime() ? srcNzbInfo->GetMinTime() : destNzbInfo->GetMinTime()); destNzbInfo->SetMaxTime(srcNzbInfo->GetMaxTime() > destNzbInfo->GetMaxTime() ? srcNzbInfo->GetMaxTime() : destNzbInfo->GetMaxTime()); destNzbInfo->SetDownloadedSize(destNzbInfo->GetDownloadedSize() + srcNzbInfo->GetDownloadedSize()); destNzbInfo->SetDownloadSec(destNzbInfo->GetDownloadSec() + srcNzbInfo->GetDownloadSec()); destNzbInfo->SetDownloadStartTime((destNzbInfo->GetDownloadStartTime() > 0 && destNzbInfo->GetDownloadStartTime() < srcNzbInfo->GetDownloadStartTime()) || srcNzbInfo->GetDownloadStartTime() == 0 ? destNzbInfo->GetDownloadStartTime() : srcNzbInfo->GetDownloadStartTime()); // reattach completed file items to new NZBInfo-object for (CompletedFile& completedFile : srcNzbInfo->GetCompletedFiles()) { destNzbInfo->GetCompletedFiles()->push_back(std::move(completedFile)); } srcNzbInfo->GetCompletedFiles()->clear(); // concatenate QueuedFilenames using character '|' as separator CString queuedFilename; queuedFilename.Format("%s|%s", destNzbInfo->GetQueuedFilename(), srcNzbInfo->GetQueuedFilename()); destNzbInfo->SetQueuedFilename(queuedFilename); g_DiskState->DiscardFiles(srcNzbInfo); downloadQueue->GetQueue()->Remove(srcNzbInfo); return true; } /* * Creates new nzb-item out of existing files from other nzb-items. * If any of file-items is being downloaded the command fail. * For each file-item an event "eaFileDeleted" is fired. */ bool QueueCoordinator::SplitQueueEntries(DownloadQueue* downloadQueue, RawFileList* fileList, const char* name, NzbInfo** newNzbInfo) { if (fileList->empty()) { return false; } NzbInfo* srcNzbInfo = nullptr; for (FileInfo* fileInfo : fileList) { if (fileInfo->GetActiveDownloads() > 0 || fileInfo->GetCompletedArticles() > 0) { error("Could not split %s. File is already (partially) downloaded", fileInfo->GetFilename()); return false; } if (fileInfo->GetNzbInfo()->GetPostInfo()) { error("Could not split %s. File in post-process-stage", fileInfo->GetFilename()); return false; } if (!srcNzbInfo) { srcNzbInfo = fileInfo->GetNzbInfo(); } } std::unique_ptr nzbInfo = std::make_unique(); nzbInfo->SetFilename(srcNzbInfo->GetFilename()); nzbInfo->SetName(name); nzbInfo->SetCategory(srcNzbInfo->GetCategory()); nzbInfo->SetFullContentHash(0); nzbInfo->SetFilteredContentHash(0); nzbInfo->SetPriority(srcNzbInfo->GetPriority()); nzbInfo->BuildDestDirName(); nzbInfo->SetQueuedFilename(srcNzbInfo->GetQueuedFilename()); nzbInfo->GetParameters()->CopyFrom(srcNzbInfo->GetParameters()); srcNzbInfo->SetFullContentHash(0); srcNzbInfo->SetFilteredContentHash(0); for (FileInfo* fileInfo : fileList) { DownloadQueue::Aspect aspect = { DownloadQueue::eaFileDeleted, downloadQueue, fileInfo->GetNzbInfo(), fileInfo }; downloadQueue->Notify(&aspect); nzbInfo->GetFileList()->Add(srcNzbInfo->GetFileList()->Remove(fileInfo)); fileInfo->SetNzbInfo(nzbInfo.get()); srcNzbInfo->SetFileCount(srcNzbInfo->GetFileCount() - 1); srcNzbInfo->SetSize(srcNzbInfo->GetSize() - fileInfo->GetSize()); srcNzbInfo->SetRemainingSize(srcNzbInfo->GetRemainingSize() - fileInfo->GetRemainingSize()); srcNzbInfo->SetCurrentSuccessSize(srcNzbInfo->GetCurrentSuccessSize() - fileInfo->GetSuccessSize()); srcNzbInfo->SetCurrentFailedSize(srcNzbInfo->GetCurrentFailedSize() - fileInfo->GetFailedSize() - fileInfo->GetMissedSize()); srcNzbInfo->SetTotalArticles(srcNzbInfo->GetTotalArticles() - fileInfo->GetTotalArticles()); srcNzbInfo->SetFailedArticles(srcNzbInfo->GetFailedArticles() - fileInfo->GetMissedArticles()); srcNzbInfo->SetCurrentSuccessArticles(srcNzbInfo->GetCurrentSuccessArticles() - fileInfo->GetSuccessArticles()); srcNzbInfo->SetCurrentFailedArticles(srcNzbInfo->GetCurrentFailedArticles() - fileInfo->GetFailedArticles() - fileInfo->GetMissedArticles()); srcNzbInfo->GetCurrentServerStats()->ListOp(fileInfo->GetServerStats(), ServerStatList::soSubtract); nzbInfo->SetFileCount(nzbInfo->GetFileCount() + 1); nzbInfo->SetSize(nzbInfo->GetSize() + fileInfo->GetSize()); nzbInfo->SetRemainingSize(nzbInfo->GetRemainingSize() + fileInfo->GetRemainingSize()); nzbInfo->SetCurrentSuccessSize(nzbInfo->GetCurrentSuccessSize() + fileInfo->GetSuccessSize()); nzbInfo->SetCurrentFailedSize(nzbInfo->GetCurrentFailedSize() + fileInfo->GetFailedSize() + fileInfo->GetMissedSize()); nzbInfo->SetTotalArticles(nzbInfo->GetTotalArticles() + fileInfo->GetTotalArticles()); nzbInfo->SetFailedArticles(nzbInfo->GetFailedArticles() + fileInfo->GetMissedArticles()); nzbInfo->SetCurrentSuccessArticles(nzbInfo->GetCurrentSuccessArticles() + fileInfo->GetSuccessArticles()); nzbInfo->SetCurrentFailedArticles(nzbInfo->GetCurrentFailedArticles() + fileInfo->GetFailedArticles() + fileInfo->GetMissedArticles()); nzbInfo->GetCurrentServerStats()->ListOp(fileInfo->GetServerStats(), ServerStatList::soAdd); if (fileInfo->GetParFile()) { srcNzbInfo->SetParSize(srcNzbInfo->GetParSize() - fileInfo->GetSize()); srcNzbInfo->SetParCurrentSuccessSize(srcNzbInfo->GetParCurrentSuccessSize() - fileInfo->GetSuccessSize()); srcNzbInfo->SetParCurrentFailedSize(srcNzbInfo->GetParCurrentFailedSize() - fileInfo->GetFailedSize() - fileInfo->GetMissedSize()); srcNzbInfo->SetRemainingParCount(srcNzbInfo->GetRemainingParCount() - 1); nzbInfo->SetParSize(nzbInfo->GetParSize() + fileInfo->GetSize()); nzbInfo->SetParCurrentSuccessSize(nzbInfo->GetParCurrentSuccessSize() + fileInfo->GetSuccessSize()); nzbInfo->SetParCurrentFailedSize(nzbInfo->GetParCurrentFailedSize() + fileInfo->GetFailedSize() + fileInfo->GetMissedSize()); nzbInfo->SetRemainingParCount(nzbInfo->GetRemainingParCount() + 1); } if (fileInfo->GetPaused()) { srcNzbInfo->SetPausedFileCount(srcNzbInfo->GetPausedFileCount() - 1); srcNzbInfo->SetPausedSize(srcNzbInfo->GetPausedSize() - fileInfo->GetRemainingSize()); nzbInfo->SetPausedFileCount(srcNzbInfo->GetPausedFileCount() + 1); nzbInfo->SetPausedSize(nzbInfo->GetPausedSize() + fileInfo->GetRemainingSize()); } } nzbInfo->UpdateMinMaxTime(); if (srcNzbInfo->GetCompletedFiles()->empty()) { srcNzbInfo->UpdateMinMaxTime(); } if (srcNzbInfo->GetFileList()->empty()) { g_DiskState->DiscardFiles(srcNzbInfo); downloadQueue->GetQueue()->Remove(srcNzbInfo); } *newNzbInfo = nzbInfo.get(); downloadQueue->GetQueue()->Add(std::move(nzbInfo)); return true; } void QueueCoordinator::DirectRenameCompleted(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (g_Options->GetSaveQueue() && g_Options->GetServerMode() && !fileInfo->GetArticles()->empty()) { // save new file name into disk state file g_DiskState->SaveFile(fileInfo); } } DiscardDirectRename(downloadQueue, nzbInfo); nzbInfo->SetDirectRenameStatus(NzbInfo::tsSuccess); if (g_Options->GetParCheck() != Options::pcForce) { downloadQueue->EditEntry(nzbInfo->GetId(), DownloadQueue::eaGroupResume, nullptr); downloadQueue->EditEntry(nzbInfo->GetId(), DownloadQueue::eaGroupPauseAllPars, nullptr); } if (g_Options->GetReorderFiles()) { nzbInfo->PrintMessage(Message::mkInfo, "Reordering files for %s", nzbInfo->GetName()); downloadQueue->EditEntry(nzbInfo->GetId(), DownloadQueue::eaGroupSortFiles, nullptr); } downloadQueue->Save(); DownloadQueue::Aspect namedAspect = { DownloadQueue::eaNzbNamed, downloadQueue, nzbInfo, nullptr }; downloadQueue->Notify(&namedAspect); } void QueueCoordinator::DiscardDirectRename(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) { int64 discardedSize = 0; int discardedCount = 0; for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (fileInfo->GetParFile() && fileInfo->GetCompletedArticles() == 1 && fileInfo->GetActiveDownloads() == 0) { // discard downloaded articles from partially downloaded par-files discardedSize += fileInfo->GetSuccessSize(); discardedCount++; nzbInfo->SetCurrentSuccessArticles(nzbInfo->GetCurrentSuccessArticles() - fileInfo->GetSuccessArticles()); nzbInfo->SetCurrentSuccessSize(nzbInfo->GetCurrentSuccessSize() - fileInfo->GetSuccessSize()); nzbInfo->SetParCurrentSuccessSize(nzbInfo->GetParCurrentSuccessSize() - fileInfo->GetSuccessSize()); fileInfo->SetSuccessSize(0); fileInfo->SetSuccessArticles(0); nzbInfo->SetCurrentFailedArticles(nzbInfo->GetCurrentFailedArticles() - fileInfo->GetFailedArticles()); nzbInfo->SetCurrentFailedSize(nzbInfo->GetCurrentFailedSize() - fileInfo->GetFailedSize()); nzbInfo->SetParCurrentFailedSize(nzbInfo->GetParCurrentFailedSize() - fileInfo->GetFailedSize()); fileInfo->SetFailedSize(0); fileInfo->SetFailedArticles(0); fileInfo->SetCompletedArticles(0); fileInfo->SetRemainingSize(fileInfo->GetSize() - fileInfo->GetMissedSize()); // discard temporary files DiscardTempFiles(fileInfo); g_DiskState->DiscardFile(fileInfo->GetId(), false, true, false); fileInfo->SetOutputFilename(nullptr); fileInfo->SetOutputInitialized(false); fileInfo->SetCachedArticles(0); fileInfo->SetPartialChanged(false); fileInfo->SetPartialState(FileInfo::psNone); if (g_Options->GetSaveQueue() && g_Options->GetServerMode()) { // free up memory used by articles if possible fileInfo->GetArticles()->clear(); } else { // reset article states if discarding isn't possible for (ArticleInfo* articleInfo : fileInfo->GetArticles()) { articleInfo->SetStatus(ArticleInfo::aiUndefined); articleInfo->SetResultFilename(nullptr); articleInfo->DiscardSegment(); } } } if (g_Options->GetSaveQueue() && g_Options->GetServerMode() && !fileInfo->GetArticles()->empty() && g_Options->GetContinuePartial() && fileInfo->GetActiveDownloads() == 0 && fileInfo->GetCachedArticles() == 0) { // discard article infos to free up memory if possible debug("Discarding article infos for %s/%s", nzbInfo->GetName(), fileInfo->GetFilename()); fileInfo->SetPartialChanged(true); SavePartialState(fileInfo); fileInfo->GetArticles()->clear(); } } if (discardedSize > 0) { nzbInfo->PrintMessage(Message::mkDetail, "Discarded %s from %i files used for direct renaming", *Util::FormatSize(discardedSize), discardedCount); } } nzbget-19.1/daemon/queue/NzbFile.h0000644000175000017500000000461513130203062016636 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef NZBFILE_H #define NZBFILE_H #include "NString.h" #include "DownloadInfo.h" class NzbFile { public: NzbFile(const char* fileName, const char* category); bool Parse(); const char* GetFileName() const { return m_fileName; } std::unique_ptr DetachNzbInfo() { return std::move(m_nzbInfo); } const char* GetPassword() { return m_password; } void LogDebugInfo(); private: std::unique_ptr m_nzbInfo; CString m_fileName; CString m_password; void AddArticle(FileInfo* fileInfo, std::unique_ptr articleInfo); void AddFileInfo(std::unique_ptr fileInfo); void ParseSubject(FileInfo* fileInfo, bool TryQuotes); void BuildFilenames(); void ProcessFiles(); void CalcHashes(); bool HasDuplicateFilenames(); void ReadPassword(); #ifdef WIN32 bool ParseNzb(IUnknown* nzb); static void EncodeUrl(const char* filename, char* url, int bufLen); #else std::unique_ptr m_fileInfo; ArticleInfo* m_article = nullptr; StringBuilder m_tagContent; bool m_ignoreNextError; bool m_hasPassword = false; static void SAX_StartElement(NzbFile* file, const char *name, const char **atts); static void SAX_EndElement(NzbFile* file, const char *name); static void SAX_characters(NzbFile* file, const char * xmlstr, int len); static void* SAX_getEntity(NzbFile* file, const char * name); static void SAX_error(NzbFile* file, const char *msg, ...); void Parse_StartElement(const char *name, const char **atts); void Parse_EndElement(const char *name); void Parse_Content(const char *buf, int len); #endif }; #endif nzbget-19.1/daemon/queue/DownloadInfo.cpp0000644000175000017500000005074013130203062020223 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2017 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, see . */ #include "nzbget.h" #include "DownloadInfo.h" #include "DiskState.h" #include "Options.h" #include "Util.h" #include "FileSystem.h" int FileInfo::m_idGen = 0; int FileInfo::m_idMax = 0; int NzbInfo::m_idGen = 0; int NzbInfo::m_idMax = 0; DownloadQueue* DownloadQueue::g_DownloadQueue = nullptr; bool DownloadQueue::g_Loaded = false; void NzbParameterList::SetParameter(const char* name, const char* value) { bool emptyVal = Util::EmptyStr(value); iterator pos = std::find_if(begin(), end(), [name](NzbParameter& parameter) { return !strcmp(parameter.GetName(), name); }); if (emptyVal && pos != end()) { erase(pos); } else if (pos != end()) { pos->SetValue(value); } else if (!emptyVal) { emplace_back(name, value); } } NzbParameter* NzbParameterList::Find(const char* name, bool caseSensitive) { for (NzbParameter& parameter : this) { if ((caseSensitive && !strcmp(parameter.GetName(), name)) || (!caseSensitive && !strcasecmp(parameter.GetName(), name))) { return ¶meter; } } return nullptr; } void NzbParameterList::CopyFrom(NzbParameterList* sourceParameters) { for (NzbParameter& parameter : sourceParameters) { SetParameter(parameter.GetName(), parameter.GetValue()); } } ScriptStatus::EStatus ScriptStatusList::CalcTotalStatus() { ScriptStatus::EStatus status = ScriptStatus::srNone; for (ScriptStatus& scriptStatus : this) { // Failure-Status overrides Success-Status if ((scriptStatus.GetStatus() == ScriptStatus::srSuccess && status == ScriptStatus::srNone) || (scriptStatus.GetStatus() == ScriptStatus::srFailure)) { status = scriptStatus.GetStatus(); } } return status; } void ServerStatList::StatOp(int serverId, int successArticles, int failedArticles, EStatOperation statOperation) { ServerStat* serverStat = nullptr; for (ServerStat& serverStat1 : this) { if (serverStat1.GetServerId() == serverId) { serverStat = &serverStat1; break; } } if (!serverStat) { emplace_back(serverId); serverStat = &back(); } switch (statOperation) { case soSet: serverStat->SetSuccessArticles(successArticles); serverStat->SetFailedArticles(failedArticles); break; case soAdd: serverStat->SetSuccessArticles(serverStat->GetSuccessArticles() + successArticles); serverStat->SetFailedArticles(serverStat->GetFailedArticles() + failedArticles); break; case soSubtract: serverStat->SetSuccessArticles(serverStat->GetSuccessArticles() - successArticles); serverStat->SetFailedArticles(serverStat->GetFailedArticles() - failedArticles); break; } } void ServerStatList::ListOp(ServerStatList* serverStats, EStatOperation statOperation) { for (ServerStat& serverStat : serverStats) { StatOp(serverStat.GetServerId(), serverStat.GetSuccessArticles(), serverStat.GetFailedArticles(), statOperation); } } void NzbInfo::SetId(int id) { m_id = id; if (m_idMax < m_id) { m_idMax = m_id; } } void NzbInfo::ResetGenId(bool max) { if (max) { m_idGen = m_idMax; } else { m_idGen = 0; m_idMax = 0; } } int NzbInfo::GenerateId() { return ++m_idGen; } void NzbInfo::SetUrl(const char* url) { m_url = url; if (!m_name) { CString nzbNicename = MakeNiceUrlName(url, m_filename); SetName(nzbNicename); } } void NzbInfo::SetFilename(const char* filename) { bool hadFilename = !Util::EmptyStr(m_filename); m_filename = filename; if ((!m_name || !hadFilename) && !Util::EmptyStr(filename)) { CString nzbNicename = MakeNiceNzbName(m_filename, true); SetName(nzbNicename); } } CString NzbInfo::MakeNiceNzbName(const char * nzbFilename, bool removeExt) { BString<1024> nicename = FileSystem::BaseFileName(nzbFilename); if (removeExt) { // wipe out ".nzb" char* p = strrchr(nicename, '.'); if (p && !strcasecmp(p, ".nzb")) *p = '\0'; } CString validname = FileSystem::MakeValidFilename(nicename); return validname; } CString NzbInfo::MakeNiceUrlName(const char* urlStr, const char* nzbFilename) { CString urlNicename; URL url(urlStr); if (!Util::EmptyStr(nzbFilename)) { CString nzbNicename = MakeNiceNzbName(nzbFilename, true); urlNicename.Format("%s @ %s", *nzbNicename, url.GetHost()); } else if (url.IsValid()) { urlNicename.Format("%s%s", url.GetHost(), url.GetResource()); } else { urlNicename = urlStr; } return urlNicename; } void NzbInfo::BuildDestDirName() { if (Util::EmptyStr(g_Options->GetInterDir())) { m_destDir = BuildFinalDirName(); } else { m_destDir.Format("%s%c%s.#%i", g_Options->GetInterDir(), PATH_SEPARATOR, GetName(), GetId()); } } CString NzbInfo::BuildFinalDirName() { CString finalDir = g_Options->GetDestDir(); bool useCategory = !m_category.Empty(); if (useCategory) { Options::Category* category = g_Options->FindCategory(m_category, false); if (category && !Util::EmptyStr(category->GetDestDir())) { finalDir = category->GetDestDir(); useCategory = false; } } if (g_Options->GetAppendCategoryDir() && useCategory) { CString categoryDir = FileSystem::MakeValidFilename(m_category, true); // we can't format with "finalDir.Format" because one of the parameter is "finalDir" itself. finalDir = CString::FormatStr("%s%c%s", *finalDir, PATH_SEPARATOR, *categoryDir); } finalDir.AppendFmt("%c%s", PATH_SEPARATOR, GetName()); return finalDir; } int NzbInfo::CalcHealth() { if (m_currentFailedSize == 0 || m_size == m_parSize) { return 1000; } int health = (int)((m_size - m_parSize - (m_currentFailedSize - m_parCurrentFailedSize)) * 1000 / (m_size - m_parSize)); if (health == 1000 && m_currentFailedSize - m_parCurrentFailedSize > 0) { health = 999; } return health; } int NzbInfo::CalcCriticalHealth(bool allowEstimation) { if (m_size == 0) { return 1000; } if (m_size == m_parSize) { return 0; } int64 goodParSize = m_parSize - m_parCurrentFailedSize; int criticalHealth = (int)((m_size - goodParSize*2) * 1000 / (m_size - goodParSize)); if (goodParSize*2 > m_size) { criticalHealth = 0; } else if (criticalHealth == 1000 && m_parSize > 0) { criticalHealth = 999; } if (criticalHealth == 1000 && allowEstimation) { // using empirical critical health 85%, to avoid false alarms for downloads with renamed par-files criticalHealth = 850; } return criticalHealth; } void NzbInfo::UpdateMinMaxTime() { m_minTime = 0; m_maxTime = 0; bool first = true; for (FileInfo* fileInfo : &m_fileList) { if (first) { m_minTime = fileInfo->GetTime(); m_maxTime = fileInfo->GetTime(); first = false; } if (fileInfo->GetTime() > 0) { if (fileInfo->GetTime() < m_minTime) { m_minTime = fileInfo->GetTime(); } if (fileInfo->GetTime() > m_maxTime) { m_maxTime = fileInfo->GetTime(); } } } } void NzbInfo::AddMessage(Message::EKind kind, const char * text) { switch (kind) { case Message::mkDetail: detail("%s", text); break; case Message::mkInfo: info("%s", text); break; case Message::mkWarning: warn("%s", text); break; case Message::mkError: error("%s", text); break; case Message::mkDebug: debug("%s", text); break; } Guard guard(m_logMutex); m_messages.emplace_back(++m_idMessageGen, kind, Util::CurrentTime(), text); if (g_Options->GetSaveQueue() && g_Options->GetServerMode() && g_Options->GetNzbLog()) { g_DiskState->AppendNzbMessage(m_id, kind, text); m_messageCount++; } while (m_messages.size() > (uint32)g_Options->GetLogBufferSize()) { m_messages.pop_front(); } m_cachedMessageCount = m_messages.size(); } void NzbInfo::PrintMessage(Message::EKind kind, const char* format, ...) { char tmp2[1024]; va_list ap; va_start(ap, format); vsnprintf(tmp2, 1024, format, ap); tmp2[1024-1] = '\0'; va_end(ap); AddMessage(kind, tmp2); } void NzbInfo::ClearMessages() { Guard guard(m_logMutex); m_messages.clear(); m_cachedMessageCount = 0; } void NzbInfo::MoveFileList(NzbInfo* srcNzbInfo) { m_fileList = std::move(*srcNzbInfo->GetFileList()); for (FileInfo* fileInfo : &m_fileList) { fileInfo->SetNzbInfo(this); } SetFullContentHash(srcNzbInfo->GetFullContentHash()); SetFilteredContentHash(srcNzbInfo->GetFilteredContentHash()); SetFileCount(srcNzbInfo->GetFileCount()); SetPausedFileCount(srcNzbInfo->GetPausedFileCount()); SetRemainingParCount(srcNzbInfo->GetRemainingParCount()); SetSize(srcNzbInfo->GetSize()); SetRemainingSize(srcNzbInfo->GetRemainingSize()); SetPausedSize(srcNzbInfo->GetPausedSize()); SetSuccessSize(srcNzbInfo->GetSuccessSize()); SetCurrentSuccessSize(srcNzbInfo->GetCurrentSuccessSize()); SetFailedSize(srcNzbInfo->GetFailedSize()); SetCurrentFailedSize(srcNzbInfo->GetCurrentFailedSize()); SetParSize(srcNzbInfo->GetParSize()); SetParSuccessSize(srcNzbInfo->GetParSuccessSize()); SetParCurrentSuccessSize(srcNzbInfo->GetParCurrentSuccessSize()); SetParFailedSize(srcNzbInfo->GetParFailedSize()); SetParCurrentFailedSize(srcNzbInfo->GetParCurrentFailedSize()); SetTotalArticles(srcNzbInfo->GetTotalArticles()); SetSuccessArticles(srcNzbInfo->GetSuccessArticles()); SetFailedArticles(srcNzbInfo->GetFailedArticles()); SetCurrentSuccessArticles(srcNzbInfo->GetSuccessArticles()); SetCurrentFailedArticles(srcNzbInfo->GetFailedArticles()); SetMinTime(srcNzbInfo->GetMinTime()); SetMaxTime(srcNzbInfo->GetMaxTime()); } void NzbInfo::EnterPostProcess() { m_postInfo = std::make_unique(); m_postInfo->SetNzbInfo(this); } void NzbInfo::LeavePostProcess() { m_postInfo.reset(); ClearMessages(); } void NzbInfo::SetActiveDownloads(int activeDownloads) { if (((m_activeDownloads == 0 && activeDownloads > 0) || (m_activeDownloads > 0 && activeDownloads == 0)) && m_kind == NzbInfo::nkNzb) { if (activeDownloads > 0) { m_downloadStartTime = Util::CurrentTime(); m_downloadStartSec = m_downloadSec; } else { m_downloadSec = m_downloadStartSec + (Util::CurrentTime() - m_downloadStartTime); m_downloadStartTime = 0; m_changed = true; } } else if (activeDownloads > 0) { m_downloadSec = m_downloadStartSec + (Util::CurrentTime() - m_downloadStartTime); m_changed = true; } m_activeDownloads = activeDownloads; } bool NzbInfo::IsDupeSuccess() { bool failure = m_markStatus != NzbInfo::ksSuccess && m_markStatus != NzbInfo::ksGood && (m_deleteStatus != NzbInfo::dsNone || m_markStatus == NzbInfo::ksBad || m_parStatus == NzbInfo::psFailure || m_unpackStatus == NzbInfo::usFailure || m_unpackStatus == NzbInfo::usPassword || (m_parStatus == NzbInfo::psSkipped && m_unpackStatus == NzbInfo::usSkipped && CalcHealth() < CalcCriticalHealth(true))); return !failure; } const char* NzbInfo::MakeTextStatus(bool ignoreScriptStatus) { const char* status = "FAILURE/INTERNAL_ERROR"; if (m_kind == NzbInfo::nkNzb) { int health = CalcHealth(); int criticalHealth = CalcCriticalHealth(false); ScriptStatus::EStatus scriptStatus = ignoreScriptStatus ? ScriptStatus::srSuccess : m_scriptStatuses.CalcTotalStatus(); if (m_markStatus == NzbInfo::ksBad) { status = "FAILURE/BAD"; } else if (m_markStatus == NzbInfo::ksGood) { status = "SUCCESS/GOOD"; } else if (m_markStatus == NzbInfo::ksSuccess) { status = "SUCCESS/MARK"; } else if (m_deleteStatus == NzbInfo::dsHealth) { status = "FAILURE/HEALTH"; } else if (m_deleteStatus == NzbInfo::dsManual) { status = "DELETED/MANUAL"; } else if (m_deleteStatus == NzbInfo::dsDupe) { status = "DELETED/DUPE"; } else if (m_deleteStatus == NzbInfo::dsBad) { status = "FAILURE/BAD"; } else if (m_deleteStatus == NzbInfo::dsGood) { status = "DELETED/GOOD"; } else if (m_deleteStatus == NzbInfo::dsCopy) { status = "DELETED/COPY"; } else if (m_deleteStatus == NzbInfo::dsScan) { status = "FAILURE/SCAN"; } else if (m_parStatus == NzbInfo::psFailure) { status = "FAILURE/PAR"; } else if (m_unpackStatus == NzbInfo::usFailure) { status = "FAILURE/UNPACK"; } else if (m_moveStatus == NzbInfo::msFailure) { status = "FAILURE/MOVE"; } else if (m_parStatus == NzbInfo::psManual) { status = "WARNING/DAMAGED"; } else if (m_parStatus == NzbInfo::psRepairPossible) { status = "WARNING/REPAIRABLE"; } else if ((m_parStatus == NzbInfo::psNone || m_parStatus == NzbInfo::psSkipped) && (m_unpackStatus == NzbInfo::usNone || m_unpackStatus == NzbInfo::usSkipped) && health < criticalHealth) { status = "FAILURE/HEALTH"; } else if ((m_parStatus == NzbInfo::psNone || m_parStatus == NzbInfo::psSkipped) && (m_unpackStatus == NzbInfo::usNone || m_unpackStatus == NzbInfo::usSkipped) && health < 1000 && health >= criticalHealth) { status = "WARNING/HEALTH"; } else if ((m_parStatus == NzbInfo::psNone || m_parStatus == NzbInfo::psSkipped) && (m_unpackStatus == NzbInfo::usNone || m_unpackStatus == NzbInfo::usSkipped) && scriptStatus != ScriptStatus::srFailure && health == 1000) { status = "SUCCESS/HEALTH"; } else if (m_unpackStatus == NzbInfo::usSpace) { status = "WARNING/SPACE"; } else if (m_unpackStatus == NzbInfo::usPassword) { status = "WARNING/PASSWORD"; } else if ((m_unpackStatus == NzbInfo::usSuccess || ((m_unpackStatus == NzbInfo::usNone || m_unpackStatus == NzbInfo::usSkipped) && m_parStatus == NzbInfo::psSuccess)) && scriptStatus == ScriptStatus::srSuccess) { status = "SUCCESS/ALL"; } else if (m_unpackStatus == NzbInfo::usSuccess && scriptStatus == ScriptStatus::srNone) { status = "SUCCESS/UNPACK"; } else if (m_parStatus == NzbInfo::psSuccess && scriptStatus == ScriptStatus::srNone) { status = "SUCCESS/PAR"; } else if (scriptStatus == ScriptStatus::srFailure) { status = "WARNING/SCRIPT"; } } else if (m_kind == NzbInfo::nkUrl) { if (m_deleteStatus == NzbInfo::dsManual) { status = "DELETED/MANUAL"; } else if (m_deleteStatus == NzbInfo::dsDupe) { status = "DELETED/DUPE"; } else { const char* urlStatusName[] = { "FAILURE/INTERNAL_ERROR", "FAILURE/INTERNAL_ERROR", "FAILURE/INTERNAL_ERROR", "FAILURE/FETCH", "FAILURE/INTERNAL_ERROR", "WARNING/SKIPPED", "FAILURE/SCAN" }; status = urlStatusName[m_urlStatus]; } } return status; } void NzbInfo::UpdateCurrentStats() { m_pausedFileCount = 0; m_remainingParCount = 0; m_remainingSize = 0; m_pausedSize = 0; m_currentSuccessArticles = m_successArticles; m_currentFailedArticles = m_failedArticles; m_currentSuccessSize = m_successSize; m_currentFailedSize = m_failedSize; m_parCurrentSuccessSize = m_parSuccessSize; m_parCurrentFailedSize = m_parFailedSize; m_currentServerStats.ListOp(&m_serverStats, ServerStatList::soSet); for (FileInfo* fileInfo : &m_fileList) { m_remainingSize += fileInfo->GetRemainingSize(); m_currentSuccessArticles += fileInfo->GetSuccessArticles(); m_currentFailedArticles += fileInfo->GetFailedArticles(); m_currentSuccessSize += fileInfo->GetSuccessSize(); m_currentFailedSize += fileInfo->GetFailedSize(); if (fileInfo->GetPaused()) { m_pausedFileCount++; m_pausedSize += fileInfo->GetRemainingSize(); } if (fileInfo->GetParFile()) { m_remainingParCount++; m_parCurrentSuccessSize += fileInfo->GetSuccessSize(); m_parCurrentFailedSize += fileInfo->GetFailedSize(); } m_currentServerStats.ListOp(fileInfo->GetServerStats(), ServerStatList::soAdd); } } void NzbInfo::UpdateCompletedStats(FileInfo* fileInfo) { m_successSize += fileInfo->GetSuccessSize(); m_failedSize += fileInfo->GetFailedSize(); m_failedArticles += fileInfo->GetFailedArticles(); m_successArticles += fileInfo->GetSuccessArticles(); if (fileInfo->GetParFile()) { m_parSuccessSize += fileInfo->GetSuccessSize(); m_parFailedSize += fileInfo->GetFailedSize(); m_remainingParCount--; } if (fileInfo->GetPaused()) { m_pausedFileCount--; } m_serverStats.ListOp(fileInfo->GetServerStats(), ServerStatList::soAdd); } void NzbInfo::UpdateDeletedStats(FileInfo* fileInfo) { m_fileCount--; m_size -= fileInfo->GetSize(); m_currentSuccessSize -= fileInfo->GetSuccessSize(); m_failedSize -= fileInfo->GetMissedSize(); m_failedArticles -= fileInfo->GetMissedArticles(); m_currentFailedSize -= fileInfo->GetFailedSize() + fileInfo->GetMissedSize(); m_totalArticles -= fileInfo->GetTotalArticles(); m_currentSuccessArticles -= fileInfo->GetSuccessArticles(); m_currentFailedArticles -= fileInfo->GetFailedArticles() + fileInfo->GetMissedArticles(); m_remainingSize -= fileInfo->GetRemainingSize(); if (fileInfo->GetParFile()) { m_remainingParCount--; m_parSize -= fileInfo->GetSize(); m_parCurrentSuccessSize -= fileInfo->GetSuccessSize(); m_parFailedSize -= fileInfo->GetMissedSize(); m_parCurrentFailedSize -= fileInfo->GetFailedSize() + fileInfo->GetMissedSize(); } if (fileInfo->GetPaused()) { m_pausedFileCount--; m_pausedSize -= fileInfo->GetRemainingSize(); } m_currentServerStats.ListOp(fileInfo->GetServerStats(), ServerStatList::soSubtract); } bool NzbInfo::IsDownloadCompleted(bool ignorePausedPars) { if (m_activeDownloads) { return false; } for (FileInfo* fileInfo : &m_fileList) { if ((!fileInfo->GetPaused() || !ignorePausedPars || !fileInfo->GetParFile()) && !fileInfo->GetDeleted()) { return false; } } return true; } void ArticleInfo::AttachSegment(std::unique_ptr content, int64 offset, int size) { m_segmentContent = std::move(content); m_segmentOffset = offset; m_segmentSize = size; } void ArticleInfo::DiscardSegment() { m_segmentContent.reset(); } void FileInfo::SetId(int id) { m_id = id; if (m_idMax < m_id) { m_idMax = m_id; } } void FileInfo::ResetGenId(bool max) { if (max) { m_idGen = m_idMax; } else { m_idGen = 0; m_idMax = 0; } } void FileInfo::SetPaused(bool paused) { if (m_paused != paused && m_nzbInfo) { m_nzbInfo->SetPausedFileCount(m_nzbInfo->GetPausedFileCount() + (paused ? 1 : -1)); m_nzbInfo->SetPausedSize(m_nzbInfo->GetPausedSize() + (paused ? m_remainingSize : - m_remainingSize)); } m_paused = paused; } void FileInfo::MakeValidFilename() { m_filename = FileSystem::MakeValidFilename(m_filename); } void FileInfo::SetActiveDownloads(int activeDownloads) { m_activeDownloads = activeDownloads; if (m_activeDownloads > 0 && !m_outputFileMutex) { m_outputFileMutex = std::make_unique(); } else if (m_activeDownloads == 0) { m_outputFileMutex.reset(); } } CompletedFile::CompletedFile(int id, const char* filename, EStatus status, uint32 crc, bool parFile, const char* hash16k, const char* parSetId) : m_id(id), m_filename(filename), m_status(status), m_crc(crc), m_parFile(parFile), m_hash16k(hash16k), m_parSetId(parSetId) { if (FileInfo::m_idMax < m_id) { FileInfo::m_idMax = m_id; } } void DupInfo::SetId(int id) { m_id = id; if (NzbInfo::m_idMax < m_id) { NzbInfo::m_idMax = m_id; } } HistoryInfo::~HistoryInfo() { if ((m_kind == hkNzb || m_kind == hkUrl) && m_info) { delete (NzbInfo*)m_info; } else if (m_kind == hkDup && m_info) { delete (DupInfo*)m_info; } } int HistoryInfo::GetId() { if ((m_kind == hkNzb || m_kind == hkUrl)) { return ((NzbInfo*)m_info)->GetId(); } else // if (m_eKind == hkDup) { return ((DupInfo*)m_info)->GetId(); } } const char* HistoryInfo::GetName() { if (m_kind == hkNzb || m_kind == hkUrl) { return GetNzbInfo()->GetName(); } else if (m_kind == hkDup) { return GetDupInfo()->GetName(); } else { return ""; } } void DownloadQueue::CalcRemainingSize(int64* remaining, int64* remainingForced) { int64 remainingSize = 0; int64 remainingForcedSize = 0; for (NzbInfo* nzbInfo : &m_queue) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (!fileInfo->GetPaused() && !fileInfo->GetDeleted()) { remainingSize += fileInfo->GetRemainingSize(); if (nzbInfo->GetForcePriority()) { remainingForcedSize += fileInfo->GetRemainingSize(); } } } } *remaining = remainingSize; if (remainingForced) { *remainingForced = remainingForcedSize; } } nzbget-19.1/daemon/queue/UrlCoordinator.cpp0000644000175000017500000002552413130203062020610 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2016 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, see . */ #include "nzbget.h" #include "UrlCoordinator.h" #include "Options.h" #include "WebDownloader.h" #include "Util.h" #include "FileSystem.h" #include "NzbFile.h" #include "Scanner.h" #include "DiskState.h" #include "QueueScript.h" void UrlDownloader::ProcessHeader(const char* line) { WebDownloader::ProcessHeader(line); if (!strncmp(line, "X-DNZB-Category:", 16)) { m_category = Util::Trim(CString(line + 16)); debug("Category: %s", *m_category); } else if (!strncmp(line, "X-DNZB-", 7)) { CString modLine = line; char* value = strchr(modLine, ':'); if (value) { *value = '\0'; value++; while (*value == ' ') value++; Util::Trim(value); debug("X-DNZB: %s", *modLine); debug("Value: %s", value); BString<100> paramName("*DNZB:%s", modLine + 7); CString paramValue = WebUtil::Latin1ToUtf8(value); m_nzbInfo->GetParameters()->SetParameter(paramName, paramValue); } } } UrlCoordinator::~UrlCoordinator() { debug("Destroying UrlCoordinator"); for (UrlDownloader* urlDownloader : m_activeDownloads) { delete urlDownloader; } m_activeDownloads.clear(); debug("UrlCoordinator destroyed"); } void UrlCoordinator::Run() { debug("Entering UrlCoordinator-loop"); while (!DownloadQueue::IsLoaded()) { usleep(20 * 1000); } int resetCounter = 0; while (!IsStopped()) { bool downloadStarted = false; if (!g_Options->GetPauseDownload() || g_Options->GetUrlForce()) { // start download for next URL GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); if ((int)m_activeDownloads.size() < g_Options->GetUrlConnections()) { NzbInfo* nzbInfo = GetNextUrl(downloadQueue); bool hasMoreUrls = nzbInfo != nullptr; bool urlDownloadsRunning = !m_activeDownloads.empty(); m_hasMoreJobs = hasMoreUrls || urlDownloadsRunning; if (hasMoreUrls && !IsStopped()) { StartUrlDownload(nzbInfo); downloadStarted = true; } } } int sleepInterval = downloadStarted ? 0 : 100; usleep(sleepInterval * 1000); resetCounter += sleepInterval; if (resetCounter >= 1000) { // this code should not be called too often, once per second is OK ResetHangingDownloads(); resetCounter = 0; } } WaitJobs(); debug("Exiting UrlCoordinator-loop"); } void UrlCoordinator::WaitJobs() { // waiting for downloads debug("UrlCoordinator: waiting for Downloads to complete"); while (true) { { GuardedDownloadQueue guard = DownloadQueue::Guard(); if (m_activeDownloads.empty()) { break; } } usleep(100 * 1000); ResetHangingDownloads(); } debug("UrlCoordinator: Downloads are completed"); } void UrlCoordinator::Stop() { Thread::Stop(); debug("Stopping UrlDownloads"); GuardedDownloadQueue guard = DownloadQueue::Guard(); for (UrlDownloader* urlDownloader : m_activeDownloads) { urlDownloader->Stop(); } debug("UrlDownloads are notified"); } void UrlCoordinator::ResetHangingDownloads() { const int timeout = g_Options->GetTerminateTimeout(); if (timeout == 0) { return; } GuardedDownloadQueue guard = DownloadQueue::Guard(); time_t tm = Util::CurrentTime(); m_activeDownloads.erase(std::remove_if(m_activeDownloads.begin(), m_activeDownloads.end(), [timeout, tm](UrlDownloader* urlDownloader) { if (tm - urlDownloader->GetLastUpdateTime() > timeout && urlDownloader->GetStatus() == UrlDownloader::adRunning) { NzbInfo* nzbInfo = urlDownloader->GetNzbInfo(); debug("Terminating hanging download %s", urlDownloader->GetInfoName()); if (urlDownloader->Terminate()) { error("Terminated hanging download %s", urlDownloader->GetInfoName()); nzbInfo->SetUrlStatus(NzbInfo::lsNone); } else { error("Could not terminate hanging download %s", urlDownloader->GetInfoName()); } // it's not safe to destroy urlDownloader, because the state of object is unknown delete urlDownloader; return true; } return false; }), m_activeDownloads.end()); } void UrlCoordinator::LogDebugInfo() { info(" ---------- UrlCoordinator"); GuardedDownloadQueue guard = DownloadQueue::Guard(); info(" Active Downloads: %i", (int)m_activeDownloads.size()); for (UrlDownloader* urlDownloader : m_activeDownloads) { urlDownloader->LogDebugInfo(); } } /* * Returns next URL for download. */ NzbInfo* UrlCoordinator::GetNextUrl(DownloadQueue* downloadQueue) { bool pauseDownload = g_Options->GetPauseDownload(); NzbInfo* nzbInfo = nullptr; for (NzbInfo* nzbInfo1 : downloadQueue->GetQueue()) { if (nzbInfo1->GetKind() == NzbInfo::nkUrl && nzbInfo1->GetUrlStatus() == NzbInfo::lsNone && nzbInfo1->GetDeleteStatus() == NzbInfo::dsNone && (!pauseDownload || g_Options->GetUrlForce()) && (!nzbInfo || nzbInfo1->GetPriority() > nzbInfo->GetPriority())) { nzbInfo = nzbInfo1; } } return nzbInfo; } void UrlCoordinator::StartUrlDownload(NzbInfo* nzbInfo) { debug("Starting new UrlDownloader"); UrlDownloader* urlDownloader = new UrlDownloader(); urlDownloader->SetAutoDestroy(true); urlDownloader->Attach(this); urlDownloader->SetNzbInfo(nzbInfo); urlDownloader->SetUrl(nzbInfo->GetUrl()); urlDownloader->SetForce(g_Options->GetUrlForce()); urlDownloader->SetInfoName(nzbInfo->MakeNiceUrlName(nzbInfo->GetUrl(), nzbInfo->GetFilename())); urlDownloader->SetOutputFilename(BString<1024>("%s%curl-%i.tmp", g_Options->GetTempDir(), PATH_SEPARATOR, nzbInfo->GetId())); nzbInfo->SetActiveDownloads(1); nzbInfo->SetUrlStatus(NzbInfo::lsRunning); m_activeDownloads.push_back(urlDownloader); urlDownloader->Start(); } void UrlCoordinator::Update(Subject* caller, void* aspect) { debug("Notification from UrlDownloader received"); UrlDownloader* urlDownloader = (UrlDownloader*) caller; if ((urlDownloader->GetStatus() == WebDownloader::adFinished) || (urlDownloader->GetStatus() == WebDownloader::adFailed) || (urlDownloader->GetStatus() == WebDownloader::adRetry)) { UrlCompleted(urlDownloader); } } void UrlCoordinator::UrlCompleted(UrlDownloader* urlDownloader) { debug("URL downloaded"); NzbInfo* nzbInfo = urlDownloader->GetNzbInfo(); const char* origname; if (urlDownloader->GetOriginalFilename()) { origname = urlDownloader->GetOriginalFilename(); } else { origname = FileSystem::BaseFileName(nzbInfo->GetUrl()); // TODO: decode URL escaping } CString filename = FileSystem::MakeValidFilename(origname); debug("Filename: [%s]", *filename); bool retry; { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); // remove downloader from downloader list m_activeDownloads.erase(std::find(m_activeDownloads.begin(), m_activeDownloads.end(), urlDownloader)); nzbInfo->SetActiveDownloads(0); retry = urlDownloader->GetStatus() == WebDownloader::adRetry && !nzbInfo->GetDeleting(); if (nzbInfo->GetDeleting()) { nzbInfo->SetDeleteStatus(NzbInfo::dsManual); nzbInfo->SetUrlStatus(NzbInfo::lsNone); nzbInfo->SetDeleting(false); } else if (urlDownloader->GetStatus() == WebDownloader::adFinished) { nzbInfo->SetUrlStatus(NzbInfo::lsFinished); } else if (urlDownloader->GetStatus() == WebDownloader::adFailed) { nzbInfo->SetUrlStatus(NzbInfo::lsFailed); } else if (urlDownloader->GetStatus() == WebDownloader::adRetry) { nzbInfo->SetUrlStatus(NzbInfo::lsNone); } if (!retry) { DownloadQueue::Aspect aspect = {DownloadQueue::eaUrlCompleted, downloadQueue, nzbInfo, nullptr}; downloadQueue->Notify(&aspect); } } if (retry) { return; } if (nzbInfo->GetUrlStatus() == NzbInfo::lsFinished) { // add nzb-file to download queue Scanner::EAddStatus addStatus = g_Scanner->AddExternalFile( !Util::EmptyStr(nzbInfo->GetFilename()) ? nzbInfo->GetFilename() : *filename, !Util::EmptyStr(nzbInfo->GetCategory()) ? nzbInfo->GetCategory() : urlDownloader->GetCategory(), nzbInfo->GetPriority(), nzbInfo->GetDupeKey(), nzbInfo->GetDupeScore(), nzbInfo->GetDupeMode(), nzbInfo->GetParameters(), false, nzbInfo->GetAddUrlPaused(), nzbInfo, urlDownloader->GetOutputFilename(), nullptr, 0, nullptr); if (addStatus == Scanner::asSuccess) { // if scanner has successfully added nzb-file to queue, our pNZBInfo is // already removed from queue and destroyed return; } nzbInfo->SetUrlStatus(addStatus == Scanner::asFailed ? NzbInfo::lsScanFailed : NzbInfo::lsScanSkipped); } // the rest of function is only for failed URLs or for failed scans g_QueueScriptCoordinator->EnqueueScript(nzbInfo, QueueScriptCoordinator::qeUrlCompleted); std::unique_ptr oldNzbInfo; { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); // delete URL from queue oldNzbInfo = downloadQueue->GetQueue()->Remove(nzbInfo); // add failed URL to history if (g_Options->GetKeepHistory() > 0 && nzbInfo->GetUrlStatus() != NzbInfo::lsFinished && !nzbInfo->GetAvoidHistory()) { std::unique_ptr historyInfo = std::make_unique(std::move(oldNzbInfo)); historyInfo->SetTime(Util::CurrentTime()); downloadQueue->GetHistory()->Add(std::move(historyInfo), true); downloadQueue->HistoryChanged(); } downloadQueue->Save(); } if (oldNzbInfo) { g_DiskState->DiscardFiles(oldNzbInfo.get()); } } bool UrlCoordinator::DeleteQueueEntry(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, bool avoidHistory) { if (nzbInfo->GetActiveDownloads() > 0) { info("Deleting active URL %s", nzbInfo->GetName()); nzbInfo->SetDeleting(true); nzbInfo->SetAvoidHistory(avoidHistory); for (UrlDownloader* urlDownloader : m_activeDownloads) { if (urlDownloader->GetNzbInfo() == nzbInfo) { urlDownloader->Stop(); return true; } } } info("Deleting URL %s", nzbInfo->GetName()); nzbInfo->SetDeleteStatus(NzbInfo::dsManual); nzbInfo->SetUrlStatus(NzbInfo::lsNone); std::unique_ptr oldNzbInfo = downloadQueue->GetQueue()->Remove(nzbInfo); if (g_Options->GetKeepHistory() > 0 && !avoidHistory) { std::unique_ptr historyInfo = std::make_unique(std::move(oldNzbInfo)); historyInfo->SetTime(Util::CurrentTime()); downloadQueue->GetHistory()->Add(std::move(historyInfo), true); downloadQueue->HistoryChanged(); } else { g_DiskState->DiscardFiles(oldNzbInfo.get()); } return true; } nzbget-19.1/daemon/queue/NzbFile.cpp0000644000175000017500000004665113130203062017177 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "NzbFile.h" #include "Log.h" #include "DownloadInfo.h" #include "Options.h" #include "DiskState.h" #include "Util.h" #include "FileSystem.h" NzbFile::NzbFile(const char* fileName, const char* category) : m_fileName(fileName) { debug("Creating NZBFile"); m_nzbInfo = std::make_unique(); m_nzbInfo->SetFilename(fileName); m_nzbInfo->SetCategory(category); m_nzbInfo->BuildDestDirName(); } void NzbFile::LogDebugInfo() { info(" NZBFile %s", *m_fileName); } void NzbFile::AddArticle(FileInfo* fileInfo, std::unique_ptr articleInfo) { int index = articleInfo->GetPartNumber() - 1; // make Article-List big enough if (index >= (int)fileInfo->GetArticles()->size()) { fileInfo->GetArticles()->resize(index + 1); } (*fileInfo->GetArticles())[index] = std::move(articleInfo); } void NzbFile::AddFileInfo(std::unique_ptr fileInfo) { // calculate file size and delete empty articles int64 size = 0; int64 missedSize = 0; int64 oneSize = 0; int uncountedArticles = 0; int missedArticles = 0; int totalArticles = (int)fileInfo->GetArticles()->size(); int i = 0; for (ArticleList::iterator it = fileInfo->GetArticles()->begin(); it != fileInfo->GetArticles()->end(); ) { ArticleInfo* article = (*it).get(); if (!article) { fileInfo->GetArticles()->erase(it); it = fileInfo->GetArticles()->begin() + i; missedArticles++; if (oneSize > 0) { missedSize += oneSize; } else { uncountedArticles++; } } else { size += article->GetSize(); if (oneSize == 0) { oneSize = article->GetSize(); } it++; i++; } } if (fileInfo->GetArticles()->empty()) { return; } missedSize += uncountedArticles * oneSize; size += missedSize; fileInfo->SetNzbInfo(m_nzbInfo.get()); fileInfo->SetSize(size); fileInfo->SetRemainingSize(size - missedSize); fileInfo->SetMissedSize(missedSize); fileInfo->SetTotalArticles(totalArticles); fileInfo->SetMissedArticles(missedArticles); m_nzbInfo->GetFileList()->Add(std::move(fileInfo)); } void NzbFile::ParseSubject(FileInfo* fileInfo, bool TryQuotes) { // Example subject: some garbage "title" yEnc (10/99) // strip the "yEnc (10/99)"-suffix BString<1024> subject = fileInfo->GetSubject(); char* end = subject + strlen(subject) - 1; if (*end == ')') { end--; while (strchr("0123456789", *end) && end > subject) end--; if (*end == '/') { end--; while (strchr("0123456789", *end) && end > subject) end--; if (end - 6 > subject && !strncmp(end - 6, " yEnc (", 7)) { end[-6] = '\0'; } } } if (TryQuotes) { // try to use the filename in quatation marks char* p = subject; char* start = strchr(p, '\"'); if (start) { start++; char* end = strchr(start + 1, '\"'); if (end) { int len = (int)(end - start); char* point = strchr(start + 1, '.'); if (point && point < end) { BString<1024> filename; filename.Set(start, len); fileInfo->SetFilename(filename); return; } } } } // tokenize subject, considering spaces as separators and quotation // marks as non separatable token delimiters. // then take the last token containing dot (".") as a filename typedef std::vector TokenList; TokenList tokens; // tokenizing char* p = subject; char* start = p; bool quot = false; while (true) { char ch = *p; bool sep = (ch == '\"') || (!quot && ch == ' ') || (ch == '\0'); if (sep) { // end of token int len = (int)(p - start); if (len > 0) { tokens.emplace_back(start, len); } start = p; if (ch != '\"' || quot) { start++; } quot = *start == '\"'; if (quot) { start++; char* q = strchr(start, '\"'); if (q) { p = q - 1; } else { quot = false; } } } if (ch == '\0') { break; } p++; } if (!tokens.empty()) { // finding the best candidate for being a filename char* besttoken = tokens.back(); for (TokenList::reverse_iterator it = tokens.rbegin(); it != tokens.rend(); it++) { char* s = *it; char* p = strchr(s, '.'); if (p && (p[1] != '\0')) { besttoken = s; break; } } fileInfo->SetFilename(besttoken); } else { // subject is empty or contains only separators? debug("Could not extract Filename from Subject: %s. Using Subject as Filename", fileInfo->GetSubject()); fileInfo->SetFilename(fileInfo->GetSubject()); } } bool NzbFile::HasDuplicateFilenames() { for (FileList::iterator it = m_nzbInfo->GetFileList()->begin(); it != m_nzbInfo->GetFileList()->end(); it++) { FileInfo* fileInfo1 = (*it).get(); int dupe = 1; for (FileList::iterator it2 = it + 1; it2 != m_nzbInfo->GetFileList()->end(); it2++) { FileInfo* fileInfo2 = (*it2).get(); if (!strcmp(fileInfo1->GetFilename(), fileInfo2->GetFilename()) && strcmp(fileInfo1->GetSubject(), fileInfo2->GetSubject())) { dupe++; } } // If more than two files have the same parsed filename but different subjects, // this means, that the parsing was not correct. // in this case we take subjects as filenames to prevent // false "duplicate files"-alarm. // It's Ok for just two files to have the same filename, this is // an often case by posting-errors to repost bad files if (dupe > 2 || (dupe == 2 && m_nzbInfo->GetFileList()->size() == 2)) { return true; } } return false; } /** * Generate filenames from subjects and check if the parsing of subject was correct */ void NzbFile::BuildFilenames() { for (FileInfo* fileInfo : m_nzbInfo->GetFileList()) { ParseSubject(fileInfo, true); } if (HasDuplicateFilenames()) { for (FileInfo* fileInfo : m_nzbInfo->GetFileList()) { ParseSubject(fileInfo, false); } } if (HasDuplicateFilenames()) { m_nzbInfo->SetManyDupeFiles(true); for (FileInfo* fileInfo : m_nzbInfo->GetFileList()) { fileInfo->SetFilename(fileInfo->GetSubject()); } } } void NzbFile::CalcHashes() { RawFileList sortedFiles; for (FileInfo* fileInfo : m_nzbInfo->GetFileList()) { sortedFiles.push_back(fileInfo); } std::sort(sortedFiles.begin(), sortedFiles.end(), [](FileInfo* first, FileInfo* second) { return strcmp(first->GetFilename(), second->GetFilename()) > 0; }); uint32 fullContentHash = 0; uint32 filteredContentHash = 0; int useForFilteredCount = 0; for (FileInfo* fileInfo : sortedFiles) { // check file extension bool skip = !fileInfo->GetParFile() && Util::MatchFileExt(fileInfo->GetFilename(), g_Options->GetParIgnoreExt(), ",;"); for (ArticleInfo* article: fileInfo->GetArticles()) { int len = strlen(article->GetMessageId()); fullContentHash = Util::HashBJ96(article->GetMessageId(), len, fullContentHash); if (!skip) { filteredContentHash = Util::HashBJ96(article->GetMessageId(), len, filteredContentHash); useForFilteredCount++; } } } // if filtered hash is based on less than a half of files - do not use filtered hash at all if (useForFilteredCount < (int)sortedFiles.size() / 2) { filteredContentHash = 0; } m_nzbInfo->SetFullContentHash(fullContentHash); m_nzbInfo->SetFilteredContentHash(filteredContentHash); } void NzbFile::ProcessFiles() { BuildFilenames(); for (FileInfo* fileInfo : m_nzbInfo->GetFileList()) { fileInfo->MakeValidFilename(); BString<1024> loFileName = fileInfo->GetFilename(); for (char* p = loFileName; *p; p++) *p = tolower(*p); // convert string to lowercase bool parFile = strstr(loFileName, ".par2"); m_nzbInfo->SetFileCount(m_nzbInfo->GetFileCount() + 1); m_nzbInfo->SetTotalArticles(m_nzbInfo->GetTotalArticles() + fileInfo->GetTotalArticles()); m_nzbInfo->SetFailedArticles(m_nzbInfo->GetFailedArticles() + fileInfo->GetMissedArticles()); m_nzbInfo->SetCurrentFailedArticles(m_nzbInfo->GetCurrentFailedArticles() + fileInfo->GetMissedArticles()); m_nzbInfo->SetSize(m_nzbInfo->GetSize() + fileInfo->GetSize()); m_nzbInfo->SetRemainingSize(m_nzbInfo->GetRemainingSize() + fileInfo->GetRemainingSize()); m_nzbInfo->SetFailedSize(m_nzbInfo->GetFailedSize() + fileInfo->GetMissedSize()); m_nzbInfo->SetCurrentFailedSize(m_nzbInfo->GetFailedSize()); fileInfo->SetParFile(parFile); if (parFile) { m_nzbInfo->SetParSize(m_nzbInfo->GetParSize() + fileInfo->GetSize()); m_nzbInfo->SetParFailedSize(m_nzbInfo->GetParFailedSize() + fileInfo->GetMissedSize()); m_nzbInfo->SetParCurrentFailedSize(m_nzbInfo->GetParFailedSize()); m_nzbInfo->SetRemainingParCount(m_nzbInfo->GetRemainingParCount() + 1); } } m_nzbInfo->UpdateMinMaxTime(); CalcHashes(); if (g_Options->GetSaveQueue() && g_Options->GetServerMode()) { for (FileInfo* fileInfo : m_nzbInfo->GetFileList()) { g_DiskState->SaveFile(fileInfo); fileInfo->GetArticles()->clear(); } } if (m_password) { ReadPassword(); } } /** * Password read using XML-parser may have special characters (such as TAB) stripped. * This function rereads password directly from file to keep all characters intact. */ void NzbFile::ReadPassword() { DiskFile file; if (!file.Open(m_fileName, DiskFile::omRead)) { return; } // obtain file size. file.Seek(0, DiskFile::soEnd); int size = (int)file.Position(); file.Seek(0, DiskFile::soSet); // reading first 4KB of the file CharBuffer buf(4096); size = size < 4096 ? size : 4096; // copy the file into the buffer. file.Read(buf, size); file.Close(); buf[size-1] = '\0'; char* metaPassword = strstr(buf, ""); if (metaPassword) { metaPassword += 22; // length of '' char* end = strstr(metaPassword, ""); if (end) { *end = '\0'; WebUtil::XmlDecode(metaPassword); m_password = metaPassword; } } } #ifdef WIN32 bool NzbFile::Parse() { CoInitialize(nullptr); HRESULT hr; MSXML::IXMLDOMDocumentPtr doc; hr = doc.CreateInstance(MSXML::CLSID_DOMDocument); if (FAILED(hr)) { return false; } // Load the XML document file... doc->put_resolveExternals(VARIANT_FALSE); doc->put_validateOnParse(VARIANT_FALSE); doc->put_async(VARIANT_FALSE); _variant_t vFilename(*WString(*m_fileName)); // 1. first trying to load via filename without URL-encoding (certain charaters doesn't work when encoded) VARIANT_BOOL success = doc->load(vFilename); if (success == VARIANT_FALSE) { // 2. now trying filename encoded as URL char url[2048]; EncodeUrl(m_fileName, url, 2048); debug("url=\"%s\"", url); _variant_t vUrl(url); success = doc->load(vUrl); } if (success == VARIANT_FALSE) { _bstr_t r(doc->GetparseError()->reason); const char* errMsg = r; m_nzbInfo->AddMessage(Message::mkError, BString<1024>("Error parsing nzb-file %s: %s", FileSystem::BaseFileName(m_fileName), errMsg)); return false; } if (!ParseNzb(doc)) { return false; } if (m_nzbInfo->GetFileList()->empty()) { m_nzbInfo->AddMessage(Message::mkError, BString<1024>( "Error parsing nzb-file %s: file has no content", FileSystem::BaseFileName(m_fileName))); return false; } ProcessFiles(); return true; } void NzbFile::EncodeUrl(const char* filename, char* url, int bufLen) { WString widefilename(filename); char* end = url + bufLen; for (wchar_t* p = widefilename; *p && url < end - 3; p++) { wchar_t ch = *p; if (('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ch == '-' || ch == '.' || ch == '_' || ch == '~') { *url++ = (char)ch; } else { *url++ = '%'; uint32 a = (uint32)ch >> 4; *url++ = a > 9 ? a - 10 + 'A' : a + '0'; a = ch & 0xF; *url++ = a > 9 ? a - 10 + 'A' : a + '0'; } } *url = '\0'; } bool NzbFile::ParseNzb(IUnknown* nzb) { MSXML::IXMLDOMDocumentPtr doc = nzb; MSXML::IXMLDOMNodePtr root = doc->documentElement; MSXML::IXMLDOMNodePtr node = root->selectSingleNode("/nzb/head/meta[@type='password']"); if (node) { _bstr_t password(node->Gettext()); m_password = password; } MSXML::IXMLDOMNodeListPtr fileList = root->selectNodes("/nzb/file"); for (int i = 0; i < fileList->Getlength(); i++) { node = fileList->Getitem(i); MSXML::IXMLDOMNodePtr attribute = node->Getattributes()->getNamedItem("subject"); if (!attribute) return false; _bstr_t subject(attribute->Gettext()); std::unique_ptr fileInfo = std::make_unique(); fileInfo->SetSubject(subject); attribute = node->Getattributes()->getNamedItem("date"); if (attribute) { _bstr_t date(attribute->Gettext()); fileInfo->SetTime(atoi(date)); } MSXML::IXMLDOMNodeListPtr groupList = node->selectNodes("groups/group"); for (int g = 0; g < groupList->Getlength(); g++) { MSXML::IXMLDOMNodePtr node = groupList->Getitem(g); _bstr_t group = node->Gettext(); fileInfo->GetGroups()->push_back((const char*)group); } MSXML::IXMLDOMNodeListPtr segmentList = node->selectNodes("segments/segment"); for (int g = 0; g < segmentList->Getlength(); g++) { MSXML::IXMLDOMNodePtr node = segmentList->Getitem(g); _bstr_t bid = node->Gettext(); BString<1024> id("<%s>", (const char*)bid); MSXML::IXMLDOMNodePtr attribute = node->Getattributes()->getNamedItem("number"); if (!attribute) return false; _bstr_t number(attribute->Gettext()); attribute = node->Getattributes()->getNamedItem("bytes"); if (!attribute) return false; _bstr_t bytes(attribute->Gettext()); int partNumber = atoi(number); int lsize = atoi(bytes); if (partNumber > 0) { std::unique_ptr article = std::make_unique(); article->SetPartNumber(partNumber); article->SetMessageId(id); article->SetSize(lsize); AddArticle(fileInfo.get(), std::move(article)); } } AddFileInfo(std::move(fileInfo)); } return true; } #else bool NzbFile::Parse() { xmlSAXHandler SAX_handler = {0}; SAX_handler.startElement = reinterpret_cast(SAX_StartElement); SAX_handler.endElement = reinterpret_cast(SAX_EndElement); SAX_handler.characters = reinterpret_cast(SAX_characters); SAX_handler.error = reinterpret_cast(SAX_error); SAX_handler.getEntity = reinterpret_cast(SAX_getEntity); m_ignoreNextError = false; int ret = xmlSAXUserParseFile(&SAX_handler, this, m_fileName); if (ret != 0) { m_nzbInfo->AddMessage(Message::mkError, BString<1024>( "Error parsing nzb-file %s", FileSystem::BaseFileName(m_fileName))); return false; } if (m_nzbInfo->GetFileList()->empty()) { m_nzbInfo->AddMessage(Message::mkError, BString<1024>( "Error parsing nzb-file %s: file has no content", FileSystem::BaseFileName(m_fileName))); return false; } ProcessFiles(); return true; } void NzbFile::Parse_StartElement(const char *name, const char **atts) { BString<1024> tagAttrMessage("Malformed nzb-file, tag <%s> must have attributes", name); m_tagContent.Clear(); if (!strcmp("file", name)) { m_fileInfo = std::make_unique(); m_fileInfo->SetFilename(m_fileName); if (!atts) { m_nzbInfo->AddMessage(Message::mkWarning, tagAttrMessage); return; } for (int i = 0; atts[i]; i += 2) { const char* attrname = atts[i]; const char* attrvalue = atts[i + 1]; if (!strcmp("subject", attrname)) { m_fileInfo->SetSubject(attrvalue); } if (!strcmp("date", attrname)) { m_fileInfo->SetTime(atoi(attrvalue)); } } } else if (!strcmp("segment", name)) { if (!m_fileInfo) { m_nzbInfo->AddMessage(Message::mkWarning, "Malformed nzb-file, tag without tag "); return; } if (!atts) { m_nzbInfo->AddMessage(Message::mkWarning, tagAttrMessage); return; } int64 lsize = -1; int partNumber = -1; for (int i = 0; atts[i]; i += 2) { const char* attrname = atts[i]; const char* attrvalue = atts[i + 1]; if (!strcmp("bytes", attrname)) { lsize = atol(attrvalue); } if (!strcmp("number", attrname)) { partNumber = atol(attrvalue); } } if (partNumber > 0) { // new segment, add it! std::unique_ptr article = std::make_unique(); article->SetPartNumber(partNumber); article->SetSize(lsize); m_article = article.get(); AddArticle(m_fileInfo.get(), std::move(article)); } } else if (!strcmp("meta", name)) { if (!atts) { m_nzbInfo->AddMessage(Message::mkWarning, tagAttrMessage); return; } m_hasPassword = atts[0] && atts[1] && !strcmp("type", atts[0]) && !strcmp("password", atts[1]); } } void NzbFile::Parse_EndElement(const char *name) { if (!strcmp("file", name)) { // Close the file element, add the new file to file-list AddFileInfo(std::move(m_fileInfo)); m_article = nullptr; } else if (!strcmp("group", name)) { if (!m_fileInfo) { // error: bad nzb-file return; } m_fileInfo->GetGroups()->push_back(*m_tagContent); m_tagContent.Clear(); } else if (!strcmp("segment", name)) { if (!m_fileInfo || !m_article) { // error: bad nzb-file return; } // Get the #text part BString<1024> id("<%s>", *m_tagContent); m_article->SetMessageId(id); m_article = nullptr; } else if (!strcmp("meta", name) && m_hasPassword) { m_password = m_tagContent; } } void NzbFile::Parse_Content(const char *buf, int len) { m_tagContent.Append(buf, len); } void NzbFile::SAX_StartElement(NzbFile* file, const char *name, const char **atts) { file->Parse_StartElement(name, atts); } void NzbFile::SAX_EndElement(NzbFile* file, const char *name) { file->Parse_EndElement(name); } void NzbFile::SAX_characters(NzbFile* file, const char * xmlstr, int len) { char* str = (char*)xmlstr; // trim starting blanks int off = 0; for (int i = 0; i < len; i++) { char ch = str[i]; if (ch == ' ' || ch == 10 || ch == 13 || ch == 9) { off++; } else { break; } } int newlen = len - off; // trim ending blanks for (int i = len - 1; i >= off; i--) { char ch = str[i]; if (ch == ' ' || ch == 10 || ch == 13 || ch == 9) { newlen--; } else { break; } } if (newlen > 0) { // interpret tag content file->Parse_Content(str + off, newlen); } } void* NzbFile::SAX_getEntity(NzbFile* file, const char * name) { xmlEntityPtr e = xmlGetPredefinedEntity((xmlChar* )name); if (!e) { file->m_nzbInfo->AddMessage(Message::mkWarning, "entity not found"); file->m_ignoreNextError = true; } return e; } void NzbFile::SAX_error(NzbFile* file, const char *msg, ...) { if (file->m_ignoreNextError) { file->m_ignoreNextError = false; return; } va_list argp; va_start(argp, msg); char errMsg[1024]; vsnprintf(errMsg, sizeof(errMsg), msg, argp); errMsg[1024-1] = '\0'; va_end(argp); // remove trailing CRLF for (char* pend = errMsg + strlen(errMsg) - 1; pend >= errMsg && (*pend == '\n' || *pend == '\r' || *pend == ' '); pend--) *pend = '\0'; file->m_nzbInfo->AddMessage(Message::mkError, BString<1024>("Error parsing nzb-file: %s", errMsg)); } #endif nzbget-19.1/daemon/queue/QueueEditor.cpp0000644000175000017500000010000313130203062020057 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #include "nzbget.h" #include "DownloadInfo.h" #include "QueueEditor.h" #include "Options.h" #include "Log.h" #include "Util.h" #include "FileSystem.h" #include "QueueCoordinator.h" #include "PrePostProcessor.h" #include "HistoryCoordinator.h" #include "UrlCoordinator.h" #include "ParParser.h" const int MAX_ID = 1000000000; class GroupSorter { public: GroupSorter(NzbList* nzbList, QueueEditor::ItemList* sortItemList) : m_nzbList(nzbList), m_sortItemList(sortItemList) {} bool Execute(const char* sort); bool operator()(const std::unique_ptr& refNzbInfo1, const std::unique_ptr& refNzbInfo2) const; private: enum ESortCriteria { scName, scSize, scRemainingSize, scAge, scCategory, scPriority }; enum ESortOrder { soAscending, soDescending, soAuto }; NzbList* m_nzbList; QueueEditor::ItemList* m_sortItemList; ESortCriteria m_sortCriteria; ESortOrder m_sortOrder; }; bool GroupSorter::Execute(const char* sort) { if (!strcasecmp(sort, "name") || !strcasecmp(sort, "name+") || !strcasecmp(sort, "name-")) { m_sortCriteria = scName; } else if (!strcasecmp(sort, "size") || !strcasecmp(sort, "size+") || !strcasecmp(sort, "size-")) { m_sortCriteria = scSize; } else if (!strcasecmp(sort, "left") || !strcasecmp(sort, "left+") || !strcasecmp(sort, "left-")) { m_sortCriteria = scRemainingSize; } else if (!strcasecmp(sort, "age") || !strcasecmp(sort, "age+") || !strcasecmp(sort, "age-")) { m_sortCriteria = scAge; } else if (!strcasecmp(sort, "category") || !strcasecmp(sort, "category+") || !strcasecmp(sort, "category-")) { m_sortCriteria = scCategory; } else if (!strcasecmp(sort, "priority") || !strcasecmp(sort, "priority+") || !strcasecmp(sort, "priority-")) { m_sortCriteria = scPriority; } else { error("Could not sort groups: incorrect sort order (%s)", sort); return false; } char lastCh = sort[strlen(sort) - 1]; if (lastCh == '+') { m_sortOrder = soAscending; } else if (lastCh == '-') { m_sortOrder = soDescending; } else { m_sortOrder = soAuto; } RawNzbList tempList; for (NzbInfo* nzbInfo : m_nzbList) { tempList.push_back(nzbInfo); } ESortOrder origSortOrder = m_sortOrder; if (m_sortOrder == soAuto && m_sortCriteria == scPriority) { m_sortOrder = soDescending; } std::stable_sort(m_nzbList->begin(), m_nzbList->end(), *this); if (origSortOrder == soAuto && std::equal(tempList.begin(), tempList.end(), m_nzbList->begin(), [](NzbInfo* nzbInfo1, std::unique_ptr& nzbInfo2) { return nzbInfo1 == nzbInfo2.get(); })) { m_sortOrder = m_sortOrder == soDescending ? soAscending : soDescending; std::stable_sort(m_nzbList->begin(), m_nzbList->end(), *this); } return true; } bool GroupSorter::operator()(const std::unique_ptr& refNzbInfo1, const std::unique_ptr& refNzbInfo2) const { NzbInfo* nzbInfo1 = refNzbInfo1.get(); NzbInfo* nzbInfo2 = refNzbInfo2.get(); // if list of ID is empty - sort all items bool sortItem1 = m_sortItemList->empty(); bool sortItem2 = m_sortItemList->empty(); for (QueueEditor::EditItem& item : m_sortItemList) { sortItem1 |= item.m_nzbInfo == nzbInfo1; sortItem2 |= item.m_nzbInfo == nzbInfo2; } if (!sortItem1 || !sortItem2) { return false; } bool ret = false; if (m_sortOrder == soDescending) { std::swap(nzbInfo1, nzbInfo2); } switch (m_sortCriteria) { case scName: ret = strcmp(nzbInfo1->GetName(), nzbInfo2->GetName()) < 0; break; case scSize: ret = nzbInfo1->GetSize() < nzbInfo2->GetSize(); break; case scRemainingSize: ret = nzbInfo1->GetRemainingSize() - nzbInfo1->GetPausedSize() < nzbInfo2->GetRemainingSize() - nzbInfo2->GetPausedSize(); break; case scAge: ret = nzbInfo1->GetMinTime() > nzbInfo2->GetMinTime(); break; case scCategory: ret = strcmp(nzbInfo1->GetCategory(), nzbInfo2->GetCategory()) < 0; break; case scPriority: ret = nzbInfo1->GetPriority() < nzbInfo2->GetPriority(); break; } return ret; } FileInfo* QueueEditor::FindFileInfo(int id) { for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue()) { FileInfo* fileInfo = nzbInfo->GetFileList()->Find(id); if (fileInfo) { return fileInfo; } } return nullptr; } void QueueEditor::PauseUnpauseEntry(FileInfo* fileInfo, bool pause) { fileInfo->SetPaused(pause); } void QueueEditor::DeleteEntry(FileInfo* fileInfo) { if (!fileInfo->GetDeleted()) { fileInfo->GetNzbInfo()->PrintMessage( fileInfo->GetNzbInfo()->GetDeleting() ? Message::mkDetail : Message::mkInfo, "Deleting file %s from download queue", fileInfo->GetFilename()); g_QueueCoordinator->DeleteQueueEntry(m_downloadQueue, fileInfo); } } void QueueEditor::MoveEntry(FileInfo* fileInfo, int offset) { int entry = 0; for (FileInfo* fileInfo2 : fileInfo->GetNzbInfo()->GetFileList()) { if (fileInfo2 == fileInfo) { break; } entry++; } int newEntry = entry + offset; int size = (int)fileInfo->GetNzbInfo()->GetFileList()->size(); if (newEntry < 0) { newEntry = 0; } if (newEntry > size - 1) { newEntry = (int)size - 1; } if (newEntry >= 0 && newEntry <= size - 1) { std::unique_ptr movedFileInfo = std::move(*(fileInfo->GetNzbInfo()->GetFileList()->begin() + entry)); fileInfo->GetNzbInfo()->GetFileList()->erase(fileInfo->GetNzbInfo()->GetFileList()->begin() + entry); fileInfo->GetNzbInfo()->GetFileList()->insert(fileInfo->GetNzbInfo()->GetFileList()->begin() + newEntry, std::move(movedFileInfo)); } } void QueueEditor::MoveGroup(NzbInfo* nzbInfo, int offset) { int entry = 0; for (NzbInfo* nzbInfo2 : m_downloadQueue->GetQueue()) { if (nzbInfo2 == nzbInfo) { break; } entry++; } int newEntry = entry + offset; int size = (int)m_downloadQueue->GetQueue()->size(); if (newEntry < 0) { newEntry = 0; } if (newEntry > size - 1) { newEntry = (int)size - 1; } if (newEntry >= 0 && newEntry <= size - 1) { std::unique_ptr movedNzbInfo = std::move(*(m_downloadQueue->GetQueue()->begin() + entry)); m_downloadQueue->GetQueue()->erase(m_downloadQueue->GetQueue()->begin() + entry); m_downloadQueue->GetQueue()->insert(m_downloadQueue->GetQueue()->begin() + newEntry, std::move(movedNzbInfo)); } } bool QueueEditor::EditEntry(DownloadQueue* downloadQueue, int ID, DownloadQueue::EEditAction action, const char* args) { m_downloadQueue = downloadQueue; IdList cIdList; cIdList.push_back(ID); return InternEditList(nullptr, &cIdList, action, args); } bool QueueEditor::EditList(DownloadQueue* downloadQueue, IdList* idList, NameList* nameList, DownloadQueue::EMatchMode matchMode, DownloadQueue::EEditAction action, const char* args) { if (action == DownloadQueue::eaPostDelete) { return g_PrePostProcessor->EditList(downloadQueue, idList, action, args); } else if (DownloadQueue::eaHistoryDelete <= action && action <= DownloadQueue::eaHistorySetName) { return g_HistoryCoordinator->EditList(downloadQueue, idList, action, args); } m_downloadQueue = downloadQueue; bool ok = true; std::unique_ptr nameIdList; if (nameList) { nameIdList = std::make_unique(); idList = nameIdList.get(); ok = BuildIdListFromNameList(idList, nameList, matchMode, action); } ok = ok && (InternEditList(nullptr, idList, action, args) || matchMode == DownloadQueue::mmRegEx); m_downloadQueue->Save(); return ok; } bool QueueEditor::InternEditList(ItemList* itemList, IdList* idList, DownloadQueue::EEditAction action, const char* args) { ItemList workItems; if (!itemList) { itemList = &workItems; int offset = args && (action == DownloadQueue::eaFileMoveOffset || action == DownloadQueue::eaGroupMoveOffset) ? atoi(args) : 0; PrepareList(itemList, idList, action, offset); } switch (action) { case DownloadQueue::eaFilePauseAllPars: case DownloadQueue::eaFilePauseExtraPars: PauseParsInGroups(itemList, action == DownloadQueue::eaFilePauseExtraPars); break; case DownloadQueue::eaGroupMerge: return MergeGroups(itemList); case DownloadQueue::eaGroupSort: return SortGroups(itemList, args); case DownloadQueue::eaGroupMoveAfter: case DownloadQueue::eaGroupMoveBefore: return MoveGroupsTo(itemList, idList, action == DownloadQueue::eaGroupMoveBefore, args); case DownloadQueue::eaFileSplit: return SplitGroup(itemList, args); case DownloadQueue::eaFileReorder: ReorderFiles(itemList); break; default: for (EditItem& item : itemList) { switch (action) { case DownloadQueue::eaFilePause: PauseUnpauseEntry(item.m_fileInfo, true); break; case DownloadQueue::eaFileResume: PauseUnpauseEntry(item.m_fileInfo, false); break; case DownloadQueue::eaFileMoveOffset: case DownloadQueue::eaFileMoveTop: case DownloadQueue::eaFileMoveBottom: MoveEntry(item.m_fileInfo, item.m_offset); break; case DownloadQueue::eaFileDelete: DeleteEntry(item.m_fileInfo); break; case DownloadQueue::eaGroupSetPriority: SetNzbPriority(item.m_nzbInfo, args); break; case DownloadQueue::eaGroupSetCategory: case DownloadQueue::eaGroupApplyCategory: SetNzbCategory(item.m_nzbInfo, args, action == DownloadQueue::eaGroupApplyCategory); break; case DownloadQueue::eaGroupSetName: SetNzbName(item.m_nzbInfo, args); break; case DownloadQueue::eaGroupSetDupeKey: case DownloadQueue::eaGroupSetDupeScore: case DownloadQueue::eaGroupSetDupeMode: SetNzbDupeParam(item.m_nzbInfo, action, args); break; case DownloadQueue::eaGroupSetParameter: SetNzbParameter(item.m_nzbInfo, args); break; case DownloadQueue::eaGroupMoveTop: case DownloadQueue::eaGroupMoveBottom: case DownloadQueue::eaGroupMoveOffset: MoveGroup(item.m_nzbInfo, item.m_offset); break; case DownloadQueue::eaGroupPause: case DownloadQueue::eaGroupResume: case DownloadQueue::eaGroupPauseAllPars: case DownloadQueue::eaGroupPauseExtraPars: EditGroup(item.m_nzbInfo, action, args); break; case DownloadQueue::eaGroupDelete: case DownloadQueue::eaGroupParkDelete: case DownloadQueue::eaGroupDupeDelete: case DownloadQueue::eaGroupFinalDelete: if (item.m_nzbInfo->GetKind() == NzbInfo::nkUrl) { DeleteUrl(item.m_nzbInfo, action); } else { EditGroup(item.m_nzbInfo, action, args); } break; case DownloadQueue::eaGroupSortFiles: SortGroupFiles(item.m_nzbInfo); break; default: // suppress compiler warning "enumeration not handled in switch" break; } } } return itemList->size() > 0; } void QueueEditor::PrepareList(ItemList* itemList, IdList* idList, DownloadQueue::EEditAction action, int offset) { if (action == DownloadQueue::eaFileMoveTop || action == DownloadQueue::eaGroupMoveTop) { offset = -MAX_ID; } else if (action == DownloadQueue::eaFileMoveBottom || action == DownloadQueue::eaGroupMoveBottom) { offset = MAX_ID; } itemList->reserve(idList->size()); if ((offset != 0) && (action == DownloadQueue::eaFileMoveOffset || action == DownloadQueue::eaFileMoveTop || action == DownloadQueue::eaFileMoveBottom)) { // add IDs to list in order they currently have in download queue for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue()) { int nrEntries = (int)nzbInfo->GetFileList()->size(); int lastDestPos = -1; int start, end, step; if (offset < 0) { start = 0; end = nrEntries; step = 1; } else { start = nrEntries - 1; end = -1; step = -1; } for (int index = start; index != end; index += step) { std::unique_ptr& fileInfo = nzbInfo->GetFileList()->at(index); IdList::iterator it2 = std::find(idList->begin(), idList->end(), fileInfo->GetId()); if (it2 != idList->end()) { int workOffset = offset; int destPos = index + workOffset; if (lastDestPos == -1) { if (destPos < 0) { workOffset = -index; } else if (destPos > nrEntries - 1) { workOffset = nrEntries - 1 - index; } } else { if (workOffset < 0 && destPos <= lastDestPos) { workOffset = lastDestPos - index + 1; } else if (workOffset > 0 && destPos >= lastDestPos) { workOffset = lastDestPos - index - 1; } } lastDestPos = index + workOffset; itemList->emplace_back(fileInfo.get(), nullptr, workOffset); } } } } else if (((offset != 0) && (action == DownloadQueue::eaGroupMoveOffset || action == DownloadQueue::eaGroupMoveTop || action == DownloadQueue::eaGroupMoveBottom)) || action == DownloadQueue::eaGroupMoveBefore || action == DownloadQueue::eaGroupMoveAfter) { // add IDs to list in order they currently have in download queue int nrEntries = (int)m_downloadQueue->GetQueue()->size(); int lastDestPos = -1; int start, end, step; if (offset <= 0) { start = 0; end = nrEntries; step = 1; } else { start = nrEntries - 1; end = -1; step = -1; } for (int index = start; index != end; index += step) { std::unique_ptr& nzbInfo = m_downloadQueue->GetQueue()->at(index); IdList::iterator it2 = std::find(idList->begin(), idList->end(), nzbInfo->GetId()); if (it2 != idList->end()) { int workOffset = offset; int destPos = index + workOffset; if (lastDestPos == -1) { if (destPos < 0) { workOffset = -index; } else if (destPos > nrEntries - 1) { workOffset = nrEntries - 1 - index; } } else { if (workOffset < 0 && destPos <= lastDestPos) { workOffset = lastDestPos - index + 1; } else if (workOffset > 0 && destPos >= lastDestPos) { workOffset = lastDestPos - index - 1; } } lastDestPos = index + workOffset; itemList->emplace_back(nullptr, nzbInfo.get(), workOffset); } } } else if (action < DownloadQueue::eaGroupMoveOffset) { // check ID range int maxId = 0; int minId = MAX_ID; for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue()) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { int ID = fileInfo->GetId(); if (ID > maxId) { maxId = ID; } if (ID < minId) { minId = ID; } } } //add IDs to list in order they were transmitted in command for (int id : *idList) { if (minId <= id && id <= maxId) { FileInfo* fileInfo = FindFileInfo(id); if (fileInfo) { itemList->emplace_back(fileInfo, nullptr, offset); } } } } else { // check ID range int maxId = 0; int minId = MAX_ID; for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue()) { int ID = nzbInfo->GetId(); if (ID > maxId) { maxId = ID; } if (ID < minId) { minId = ID; } } //add IDs to list in order they were transmitted in command for (int id : *idList) { if (minId <= id && id <= maxId) { for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue()) { if (id == nzbInfo->GetId()) { itemList->emplace_back(nullptr, nzbInfo, offset); } } } } } } bool QueueEditor::BuildIdListFromNameList(IdList* idList, NameList* nameList, DownloadQueue::EMatchMode matchMode, DownloadQueue::EEditAction action) { #ifndef HAVE_REGEX_H if (matchMode == mmRegEx) { return false; } #endif std::set uniqueIds; for (CString& name : nameList) { std::unique_ptr regEx; if (matchMode == DownloadQueue::mmRegEx) { regEx = std::make_unique(name); if (!regEx->IsValid()) { return false; } } bool found = false; for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue()) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (action < DownloadQueue::eaGroupMoveOffset) { // file action BString<1024> filename("%s/%s", fileInfo->GetNzbInfo()->GetName(), FileSystem::BaseFileName(fileInfo->GetFilename())); if (((!regEx && !strcmp(filename, name)) || (regEx && regEx->Match(filename))) && (uniqueIds.find(fileInfo->GetId()) == uniqueIds.end())) { uniqueIds.insert(fileInfo->GetId()); idList->push_back(fileInfo->GetId()); found = true; } } } if (action >= DownloadQueue::eaGroupMoveOffset) { // group action const char *filename = nzbInfo->GetName(); if (((!regEx && !strcmp(filename, name)) || (regEx && regEx->Match(filename))) && (uniqueIds.find(nzbInfo->GetId()) == uniqueIds.end())) { uniqueIds.insert(nzbInfo->GetId()); idList->push_back(nzbInfo->GetId()); found = true; } } } if (!found && (matchMode == DownloadQueue::mmName)) { return false; } } return true; } bool QueueEditor::EditGroup(NzbInfo* nzbInfo, DownloadQueue::EEditAction action, const char* args) { ItemList itemList; bool allPaused = true; int id = nzbInfo->GetId(); // collecting files belonging to group for (FileInfo* fileInfo : nzbInfo->GetFileList()) { itemList.emplace_back(fileInfo, nullptr, 0); allPaused &= fileInfo->GetPaused(); } if (action == DownloadQueue::eaGroupDelete || action == DownloadQueue::eaGroupParkDelete || action == DownloadQueue::eaGroupDupeDelete || action == DownloadQueue::eaGroupFinalDelete) { nzbInfo->SetDeleting(true); nzbInfo->SetParking(action == DownloadQueue::eaGroupParkDelete && g_Options->GetKeepHistory() > 0 && !nzbInfo->GetUnpackCleanedUpDisk() && nzbInfo->GetCurrentSuccessArticles() > 0); nzbInfo->SetAvoidHistory(action == DownloadQueue::eaGroupFinalDelete); nzbInfo->SetDeletePaused(allPaused); if (action == DownloadQueue::eaGroupDupeDelete) { nzbInfo->SetDeleteStatus(NzbInfo::dsDupe); } nzbInfo->SetCleanupDisk(action != DownloadQueue::eaGroupParkDelete); } DownloadQueue::EEditAction GroupToFileMap[] = { (DownloadQueue::EEditAction)0, DownloadQueue::eaFileMoveOffset, DownloadQueue::eaFileMoveTop, DownloadQueue::eaFileMoveBottom, DownloadQueue::eaFilePause, DownloadQueue::eaFileResume, DownloadQueue::eaFileDelete, DownloadQueue::eaFilePauseAllPars, DownloadQueue::eaFilePauseExtraPars, DownloadQueue::eaFileReorder, DownloadQueue::eaFileSplit, DownloadQueue::eaFileMoveOffset, DownloadQueue::eaFileMoveTop, DownloadQueue::eaFileMoveBottom, (DownloadQueue::EEditAction)0, (DownloadQueue::EEditAction)0, DownloadQueue::eaFilePause, DownloadQueue::eaFileResume, DownloadQueue::eaFileDelete, DownloadQueue::eaFileDelete, DownloadQueue::eaFileDelete, DownloadQueue::eaFileDelete, DownloadQueue::eaFilePauseAllPars, DownloadQueue::eaFilePauseExtraPars, (DownloadQueue::EEditAction)0, (DownloadQueue::EEditAction)0, (DownloadQueue::EEditAction)0, (DownloadQueue::EEditAction)0 }; bool ok = InternEditList(&itemList, nullptr, GroupToFileMap[action], args); if ((action == DownloadQueue::eaGroupDelete || action == DownloadQueue::eaGroupDupeDelete || action == DownloadQueue::eaGroupFinalDelete) && // NZBInfo could have been destroyed already m_downloadQueue->GetQueue()->Find(id)) { DownloadQueue::Aspect deleteAspect = { DownloadQueue::eaNzbDeleted, m_downloadQueue, nzbInfo, nullptr }; m_downloadQueue->Notify(&deleteAspect); } return ok; } void QueueEditor::PauseParsInGroups(ItemList* itemList, bool extraParsOnly) { while (true) { RawFileList GroupFileList; FileInfo* firstFileInfo = nullptr; for (ItemList::iterator it = itemList->begin(); it != itemList->end(); ) { EditItem& item = *it; if (!firstFileInfo || (firstFileInfo->GetNzbInfo() == item.m_fileInfo->GetNzbInfo())) { GroupFileList.push_back(item.m_fileInfo); if (!firstFileInfo) { firstFileInfo = item.m_fileInfo; } itemList->erase(it); it = itemList->begin(); continue; } it++; } if (!GroupFileList.empty()) { PausePars(&GroupFileList, extraParsOnly); } else { break; } } } /** * If the parameter "bExtraParsOnly" is set to "false", then we pause all par2-files. * If the parameter "bExtraParsOnly" is set to "true", we use the following strategy: * At first we find all par-files, which do not have "vol" in their names, then we pause * all vols and do not affect all just-pars. * In a case, if there are no just-pars, but only vols, we find the smallest vol-file * and do not affect it, but pause all other pars. */ void QueueEditor::PausePars(RawFileList* fileList, bool extraParsOnly) { debug("QueueEditor: Pausing pars"); RawFileList Pars, Vols; for (FileInfo* fileInfo : fileList) { BString<1024> loFileName = fileInfo->GetFilename(); for (char* p = loFileName; *p; p++) *p = tolower(*p); // convert string to lowercase if (strstr(loFileName, ".par2")) { if (!extraParsOnly) { fileInfo->SetPaused(true); } else { if (strstr(loFileName, ".vol")) { Vols.push_back(fileInfo); } else { Pars.push_back(fileInfo); } } } } if (extraParsOnly) { if (!Pars.empty()) { for (FileInfo* fileInfo : Vols) { fileInfo->SetPaused(true); } } else { // pausing all Vol-files except the smallest one FileInfo* smallest = nullptr; for (FileInfo* fileInfo : Vols) { if (!smallest) { smallest = fileInfo; } else if (smallest->GetSize() > fileInfo->GetSize()) { smallest->SetPaused(true); smallest = fileInfo; } else { fileInfo->SetPaused(true); } } } } } void QueueEditor::SetNzbPriority(NzbInfo* nzbInfo, const char* priority) { debug("Setting priority %s for %s", priority, nzbInfo->GetName()); int priorityVal = atoi(priority); nzbInfo->SetPriority(priorityVal); } void QueueEditor::SetNzbCategory(NzbInfo* nzbInfo, const char* category, bool applyParams) { debug("QueueEditor: setting category '%s' for '%s'", category, nzbInfo->GetName()); bool oldUnpack = g_Options->GetUnpack(); const char* oldExtensions = g_Options->GetExtensions(); if (applyParams && !Util::EmptyStr(nzbInfo->GetCategory())) { Options::Category* categoryObj = g_Options->FindCategory(nzbInfo->GetCategory(), false); if (categoryObj) { oldUnpack = categoryObj->GetUnpack(); if (!Util::EmptyStr(categoryObj->GetExtensions())) { oldExtensions = categoryObj->GetExtensions(); } } } g_QueueCoordinator->SetQueueEntryCategory(m_downloadQueue, nzbInfo, category); if (!applyParams) { return; } bool newUnpack = g_Options->GetUnpack(); const char* newExtensions = g_Options->GetExtensions(); if (!Util::EmptyStr(nzbInfo->GetCategory())) { Options::Category* categoryObj = g_Options->FindCategory(nzbInfo->GetCategory(), false); if (categoryObj) { newUnpack = categoryObj->GetUnpack(); if (!Util::EmptyStr(categoryObj->GetExtensions())) { newExtensions = categoryObj->GetExtensions(); } } } if (oldUnpack != newUnpack) { nzbInfo->GetParameters()->SetParameter("*Unpack:", newUnpack ? "yes" : "no"); } if (strcasecmp(oldExtensions, newExtensions)) { // add new params not existed in old category Tokenizer tokNew(newExtensions, ",;"); while (const char* newScriptName = tokNew.Next()) { bool found = false; const char* oldScriptName; Tokenizer tokOld(oldExtensions, ",;"); while ((oldScriptName = tokOld.Next()) && !found) { found = !strcasecmp(newScriptName, oldScriptName); } if (!found) { nzbInfo->GetParameters()->SetParameter(BString<1024>("%s:", newScriptName), "yes"); } } // remove old params not existed in new category Tokenizer tokOld(oldExtensions, ",;"); while (const char* oldScriptName = tokOld.Next()) { bool found = false; const char* newScriptName; Tokenizer tokNew(newExtensions, ",;"); while ((newScriptName = tokNew.Next()) && !found) { found = !strcasecmp(newScriptName, oldScriptName); } if (!found) { nzbInfo->GetParameters()->SetParameter(BString<1024>("%s:", oldScriptName), "no"); } } } } void QueueEditor::SetNzbName(NzbInfo* nzbInfo, const char* name) { debug("QueueEditor: renaming '%s' to '%s'", nzbInfo->GetName(), name); g_QueueCoordinator->SetQueueEntryName(m_downloadQueue, nzbInfo, name); } bool QueueEditor::MergeGroups(ItemList* itemList) { if (itemList->size() == 0) { return false; } bool ok = true; EditItem& destItem = itemList->front(); for (EditItem& item : itemList) { if (item.m_nzbInfo != destItem.m_nzbInfo) { debug("merge %s to %s", item.m_nzbInfo->GetFilename(), destItem.m_nzbInfo->GetFilename()); if (g_QueueCoordinator->MergeQueueEntries(m_downloadQueue, destItem.m_nzbInfo, item.m_nzbInfo)) { ok = false; } } } return ok; } bool QueueEditor::SplitGroup(ItemList* itemList, const char* name) { if (itemList->size() == 0) { return false; } RawFileList fileList; for (EditItem& item : itemList) { fileList.push_back(item.m_fileInfo); } NzbInfo* newNzbInfo = nullptr; bool ok = g_QueueCoordinator->SplitQueueEntries(m_downloadQueue, &fileList, name, &newNzbInfo); return ok; } bool QueueEditor::SortGroups(ItemList* itemList, const char* sort) { AlignGroups(itemList); GroupSorter sorter(m_downloadQueue->GetQueue(), itemList); return sorter.Execute(sort); } void QueueEditor::AlignGroups(ItemList* itemList) { NzbList* nzbList = m_downloadQueue->GetQueue(); NzbInfo* lastNzbInfo = nullptr; uint32 lastNum = 0; uint32 num = 0; while (num < nzbList->size()) { std::unique_ptr& nzbInfo = nzbList->at(num); bool selected = false; for (QueueEditor::EditItem& item : itemList) { if (item.m_nzbInfo == nzbInfo.get()) { selected = true; break; } } if (selected) { if (lastNzbInfo && num - lastNum > 1) { std::unique_ptr movedNzbInfo = std::move(*(nzbList->begin() + num)); nzbList->erase(nzbList->begin() + num); nzbList->insert(nzbList->begin() + lastNum + 1, std::move(movedNzbInfo)); lastNum++; } else { lastNum = num; } lastNzbInfo = nzbInfo.get(); } num++; } } bool QueueEditor::ItemListContainsItem(ItemList* itemList, int id) { return std::find_if(itemList->begin(), itemList->end(), [id](const EditItem& item) { return item.m_nzbInfo->GetId() == id; }) != itemList->end(); }; bool QueueEditor::MoveGroupsTo(ItemList* itemList, IdList* idList, bool before, const char* args) { if (itemList->size() == 0 || Util::EmptyStr(args)) { return false; } int targetId = atoi(args); int offset = 0; // check if target is in list of moved items if (ItemListContainsItem(itemList, targetId)) { // find the next item to use as target-before bool found = false; bool targetSet = false; for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue()) { if (found) { if (!ItemListContainsItem(itemList, nzbInfo->GetId())) { targetId = nzbInfo->GetId(); before = true; targetSet = true; break; } } else if (targetId == nzbInfo->GetId()) { found = true; } } if (!targetSet) { // there are no next item; move to the bottom then offset = MAX_ID; } } AlignGroups(itemList); if (offset == 0) { // calculate offset between first moving item and target int moveId = itemList->at(0).m_nzbInfo->GetId(); bool progress = false; int step = 0; for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue()) { int id = nzbInfo->GetId(); if (id == targetId || id == moveId) { if (!progress) { step = id == targetId ? -1 : 1; offset = (before ? 0 : 1) - (step > 0 ? itemList->size() : 0); progress = true; } else { break; } } if (progress) { offset += step; } } } return InternEditList(nullptr, idList, DownloadQueue::eaGroupMoveOffset, CString::FormatStr("%i", offset)); } void QueueEditor::ReorderFiles(ItemList* itemList) { if (itemList->size() == 0) { return; } EditItem& firstItem = itemList->front(); NzbInfo* nzbInfo = firstItem.m_fileInfo->GetNzbInfo(); uint32 insertPos = 0; // now can reorder for (EditItem& item : itemList) { FileInfo* fileInfo = item.m_fileInfo; // move file item FileList::iterator it2 = nzbInfo->GetFileList()->Find(fileInfo); if (it2 != nzbInfo->GetFileList()->end()) { std::unique_ptr movedFileInfo = std::move(*it2); nzbInfo->GetFileList()->erase(it2); nzbInfo->GetFileList()->insert(nzbInfo->GetFileList()->begin() + insertPos, std::move(movedFileInfo)); insertPos++; } } } void QueueEditor::SetNzbParameter(NzbInfo* nzbInfo, const char* paramString) { debug("QueueEditor: setting nzb parameter '%s' for '%s'", paramString, FileSystem::BaseFileName(nzbInfo->GetFilename())); CString str = paramString; char* value = strchr(str, '='); if (value) { *value = '\0'; value++; nzbInfo->GetParameters()->SetParameter(str, value); } else { error("Could not set nzb parameter for %s: invalid argument: %s", nzbInfo->GetName(), paramString); } } void QueueEditor::SetNzbDupeParam(NzbInfo* nzbInfo, DownloadQueue::EEditAction action, const char* text) { debug("QueueEditor: setting dupe parameter %i='%s' for '%s'", (int)action, text, nzbInfo->GetName()); switch (action) { case DownloadQueue::eaGroupSetDupeKey: nzbInfo->SetDupeKey(text); break; case DownloadQueue::eaGroupSetDupeScore: nzbInfo->SetDupeScore(atoi(text)); break; case DownloadQueue::eaGroupSetDupeMode: { EDupeMode mode = dmScore; if (!strcasecmp(text, "SCORE")) { mode = dmScore; } else if (!strcasecmp(text, "ALL")) { mode = dmAll; } else if (!strcasecmp(text, "FORCE")) { mode = dmForce; } else { error("Could not set duplicate mode for %s: incorrect mode (%s)", nzbInfo->GetName(), text); return; } nzbInfo->SetDupeMode(mode); break; } default: // suppress compiler warning break; } } void QueueEditor::SortGroupFiles(NzbInfo* nzbInfo) { debug("QueueEditor: sorting inner files for '%s'", nzbInfo->GetName()); std::sort(nzbInfo->GetFileList()->begin(), nzbInfo->GetFileList()->end(), [](const std::unique_ptr& fileInfo1, const std::unique_ptr& fileInfo2) { if (!fileInfo1->GetParFile() && !fileInfo2->GetParFile()) { // ".rar"-files are ordered before ".r01", etc. files int len1 = strlen(fileInfo1->GetFilename()); int len2 = strlen(fileInfo2->GetFilename()); const char* ext1 = strrchr(fileInfo1->GetFilename(), '.'); const char* ext2 = strrchr(fileInfo2->GetFilename(), '.'); int ext1len = ext1 ? strlen(ext1) : 0; int ext2len = ext2 ? strlen(ext2) : 0; bool sameBaseName = len1 == len2 && ext1len == 4 && ext2len == 4 && !strncmp(fileInfo1->GetFilename(), fileInfo2->GetFilename(), len1 - 4); if (sameBaseName && !strcmp(ext1, ".rar") && ext2[1] == 'r' && isdigit(ext2[2]) && isdigit(ext2[3])) { return true; } else if (sameBaseName && !strcmp(ext2, ".rar") && ext1[1] == 'r' && isdigit(ext1[2]) && isdigit(ext1[3])) { return false; } } else if (fileInfo1->GetParFile() && fileInfo2->GetParFile() && ParParser::SameParCollection(fileInfo1->GetFilename(), fileInfo2->GetFilename(), fileInfo1->GetFilenameConfirmed() && fileInfo2->GetFilenameConfirmed())) { return fileInfo1->GetSize() < fileInfo2->GetSize(); } else if (!fileInfo1->GetParFile() && fileInfo2->GetParFile()) { return true; } else if (fileInfo1->GetParFile() && !fileInfo2->GetParFile()) { return false; } return strcmp(fileInfo1->GetFilename(), fileInfo2->GetFilename()) < 0; }); } bool QueueEditor::DeleteUrl(NzbInfo* nzbInfo, DownloadQueue::EEditAction action) { return g_UrlCoordinator->DeleteQueueEntry(m_downloadQueue, nzbInfo, action == DownloadQueue::eaGroupFinalDelete); } nzbget-19.1/daemon/queue/UrlCoordinator.h0000644000175000017500000000411513130203062020246 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2012-2016 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, see . */ #ifndef URLCOORDINATOR_H #define URLCOORDINATOR_H #include "NString.h" #include "Log.h" #include "Thread.h" #include "WebDownloader.h" #include "DownloadInfo.h" #include "Observer.h" class UrlDownloader; class UrlCoordinator : public Thread, public Observer, public Debuggable { public: virtual ~UrlCoordinator(); virtual void Run(); virtual void Stop(); void Update(Subject* caller, void* aspect); // Editing the queue bool HasMoreJobs() { return m_hasMoreJobs; } bool DeleteQueueEntry(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, bool avoidHistory); protected: virtual void LogDebugInfo(); private: typedef std::list ActiveDownloads; ActiveDownloads m_activeDownloads; bool m_hasMoreJobs = true; bool m_force; NzbInfo* GetNextUrl(DownloadQueue* downloadQueue); void StartUrlDownload(NzbInfo* nzbInfo); void UrlCompleted(UrlDownloader* urlDownloader); void ResetHangingDownloads(); void WaitJobs(); }; extern UrlCoordinator* g_UrlCoordinator; class UrlDownloader : public WebDownloader { public: void SetNzbInfo(NzbInfo* nzbInfo) { m_nzbInfo = nzbInfo; } NzbInfo* GetNzbInfo() { return m_nzbInfo; } const char* GetCategory() { return m_category; } protected: virtual void ProcessHeader(const char* line); private: NzbInfo* m_nzbInfo; CString m_category; }; #endif nzbget-19.1/daemon/queue/DiskState.cpp0000644000175000017500000016725213130203062017542 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #include "nzbget.h" #include "NString.h" #include "DiskState.h" #include "Options.h" #include "Log.h" #include "Util.h" #include "FileSystem.h" static const char* FORMATVERSION_SIGNATURE = "nzbget diskstate file version "; class StateDiskFile : public DiskFile { public: int64 PrintLine(const char* format, ...) PRINTF_SYNTAX(2); char* ReadLine(char* buffer, int64 size); int ScanLine(const char* format, ...) SCANF_SYNTAX(2); }; int64 StateDiskFile::PrintLine(const char* format, ...) { va_list ap; va_start(ap, format); CString str; str.FormatV(format, ap); va_end(ap); int len = str.Length(); // replacing terminating with str[len++] = '\n'; Write(*str, len); return len; } char* StateDiskFile::ReadLine(char* buffer, int64 size) { if (!DiskFile::ReadLine(buffer, size)) { return nullptr; } // remove traling '\n' if (*buffer) { if (buffer[strlen(buffer) - 1] != '\n') { // the line is longer than "size", scroll file position to the end of the line for (char skipbuf[1024]; DiskFile::ReadLine(skipbuf, 1024) && *skipbuf && skipbuf[strlen(skipbuf) - 1] != '\n'; ) ; } buffer[strlen(buffer) - 1] = 0; } return buffer; } /* * Standard "fscanf" scans beoynd current line if the next line is empty. * This wrapper fixes that. */ int StateDiskFile::ScanLine(const char* format, ...) { char line[1024]; if (!ReadLine(line, sizeof(line))) { return 0; } va_list ap; va_start(ap, format); int res = vsscanf(line, format, ap); va_end(ap); return res; } class StateFile { public: StateFile(const char* filename, int formatVersion, bool transactional); void Discard(); bool FileExists(); StateDiskFile* BeginWrite(); bool FinishWrite(); StateDiskFile* BeginRead(); int GetFileVersion() { return m_fileVersion; } const char* GetDestFilename() { return m_destFilename; } private: BString<1024> m_destFilename; BString<1024> m_tempFilename; bool m_transactional; int m_formatVersion; int m_fileVersion; StateDiskFile m_file; int ParseFormatVersion(const char* formatSignature); }; StateFile::StateFile(const char* filename, int formatVersion, bool transactional) : m_formatVersion(formatVersion), m_transactional(transactional) { m_destFilename.Format("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, filename); if (m_transactional) { m_tempFilename.Format("%s%c%s.new", g_Options->GetQueueDir(), PATH_SEPARATOR, filename); } else { m_tempFilename = *m_destFilename; } } void StateFile::Discard() { FileSystem::DeleteFile(m_destFilename); } /* Parse signature and return format version number */ int StateFile::ParseFormatVersion(const char* formatSignature) { if (strncmp(formatSignature, FORMATVERSION_SIGNATURE, strlen(FORMATVERSION_SIGNATURE))) { return 0; } return atoi(formatSignature + strlen(FORMATVERSION_SIGNATURE)); } bool StateFile::FileExists() { return FileSystem::FileExists(m_destFilename) || (m_transactional && FileSystem::FileExists(m_tempFilename)); } StateDiskFile* StateFile::BeginWrite() { if (!m_file.Open(m_tempFilename, StateDiskFile::omWrite)) { error("Error saving diskstate: Could not create file %s: %s", *m_tempFilename, *FileSystem::GetLastErrorMessage()); return nullptr; } m_file.PrintLine("%s%i", FORMATVERSION_SIGNATURE, m_formatVersion); return &m_file; } bool StateFile::FinishWrite() { if (!m_transactional) { m_file.Close(); return true; } // flush file content before renaming if (g_Options->GetFlushQueue()) { debug("Flushing data for file %s", FileSystem::BaseFileName(m_tempFilename)); m_file.Flush(); CString errmsg; if (!m_file.Sync(errmsg)) { warn("Could not flush file %s into disk: %s", *m_tempFilename, *errmsg); } } m_file.Close(); // now rename to dest file name FileSystem::DeleteFile(m_destFilename); if (!FileSystem::MoveFile(m_tempFilename, m_destFilename)) { error("Error saving diskstate: Could not rename file %s to %s: %s", *m_tempFilename, *m_destFilename, *FileSystem::GetLastErrorMessage()); return false; } // flush directory buffer after renaming if (g_Options->GetFlushQueue()) { debug("Flushing directory for file %s", FileSystem::BaseFileName(m_destFilename)); CString errmsg; if (!FileSystem::FlushDirBuffers(m_destFilename, errmsg)) { warn("Could not flush directory buffers for file %s into disk: %s", *m_destFilename, *errmsg); } } return true; } StateDiskFile* StateFile::BeginRead() { if (!FileSystem::FileExists(m_destFilename) && FileSystem::FileExists(m_tempFilename)) { // disaster recovery: temp-file exists but the dest-file doesn't warn("Restoring diskstate file %s from %s", FileSystem::BaseFileName(m_destFilename), FileSystem::BaseFileName(m_tempFilename)); if (!FileSystem::MoveFile(m_tempFilename, m_destFilename)) { error("Error restoring diskstate: Could not rename file %s to %s: %s", *m_tempFilename, *m_destFilename, *FileSystem::GetLastErrorMessage()); return nullptr; } } if (!m_file.Open(m_destFilename, StateDiskFile::omRead)) { error("Error reading diskstate: could not open file %s: %s", *m_destFilename, *FileSystem::GetLastErrorMessage()); return nullptr; } char FileSignatur[128]; m_file.ReadLine(FileSignatur, sizeof(FileSignatur)); m_fileVersion = ParseFormatVersion(FileSignatur); if (m_fileVersion > m_formatVersion) { error("Could not load diskstate file %s due to file version mismatch", *m_destFilename); m_file.Close(); return nullptr; } return &m_file; } /* Save Download Queue to Disk. * The Disk State consists of file "queue", which contains the order of files, * and of one diskstate-file for each file in download queue. * This function saves file "queue" and files with NZB-info. It does not * save file-infos. */ bool DiskState::SaveDownloadQueue(DownloadQueue* downloadQueue, bool saveHistory) { debug("Saving queue and history to disk"); bool ok = true; { StateFile stateFile("queue", 60, true); if (!downloadQueue->GetQueue()->empty()) { StateDiskFile* outfile = stateFile.BeginWrite(); if (!outfile) { return false; } // save nzb-infos SaveQueue(downloadQueue->GetQueue(), *outfile); // now rename to dest file name ok = stateFile.FinishWrite(); } else { stateFile.Discard(); } } if (saveHistory) { StateFile stateFile("history", 60, true); if (!downloadQueue->GetHistory()->empty()) { StateDiskFile* outfile = stateFile.BeginWrite(); if (!outfile) { return false; } // save history SaveHistory(downloadQueue->GetHistory(), *outfile); // now rename to dest file name ok &= stateFile.FinishWrite(); } else { stateFile.Discard(); } } return ok; } bool DiskState::LoadDownloadQueue(DownloadQueue* downloadQueue, Servers* servers) { debug("Loading queue from disk"); bool ok = false; int formatVersion = 0; { StateFile stateFile("queue", 60, true); if (stateFile.FileExists()) { StateDiskFile* infile = stateFile.BeginRead(); if (!infile) { return false; } formatVersion = stateFile.GetFileVersion(); if (formatVersion < 47) { error("Failed to read queue and history data. Only queue and history from NZBGet v13 or newer can be converted by this NZBGet version. " "Old queue and history data still can be converted using NZBGet v16 as an intermediate version."); goto error; } if (!LoadQueue(downloadQueue->GetQueue(), servers, *infile, formatVersion)) goto error; if (formatVersion < 57) { if (!LoadHistory(downloadQueue->GetHistory(), servers, *infile, formatVersion)) goto error; } } } if (formatVersion == 0 || formatVersion >= 57) { StateFile stateFile("history", 60, true); if (stateFile.FileExists()) { StateDiskFile* infile = stateFile.BeginRead(); if (!infile) { return false; } if (!LoadHistory(downloadQueue->GetHistory(), servers, *infile, stateFile.GetFileVersion())) goto error; } } CleanupQueueDir(downloadQueue); if (!LoadAllFileStates(downloadQueue, servers)) goto error; ok = true; error: if (!ok) { error("Error reading diskstate for download queue and history"); } NzbInfo::ResetGenId(true); FileInfo::ResetGenId(true); CalcFileStats(downloadQueue, formatVersion); return ok; } void DiskState::SaveQueue(NzbList* queue, StateDiskFile& outfile) { debug("Saving nzb list to disk"); outfile.PrintLine("%i", (int)queue->size()); for (NzbInfo* nzbInfo : queue) { SaveNzbInfo(nzbInfo, outfile); } } bool DiskState::LoadQueue(NzbList* queue, Servers* servers, StateDiskFile& infile, int formatVersion) { debug("Loading nzb list from disk"); // load nzb-infos int size; if (infile.ScanLine("%i", &size) != 1) goto error; for (int i = 0; i < size; i++) { std::unique_ptr nzbInfo = std::make_unique(); if (!LoadNzbInfo(nzbInfo.get(), servers, infile, formatVersion)) goto error; queue->push_back(std::move(nzbInfo)); } return true; error: error("Error reading nzb list from disk"); return false; } void DiskState::SaveNzbInfo(NzbInfo* nzbInfo, StateDiskFile& outfile) { outfile.PrintLine("%i", nzbInfo->GetId()); outfile.PrintLine("%i", (int)nzbInfo->GetKind()); outfile.PrintLine("%s", nzbInfo->GetUrl()); outfile.PrintLine("%s", nzbInfo->GetFilename()); outfile.PrintLine("%s", nzbInfo->GetDestDir()); outfile.PrintLine("%s", nzbInfo->GetFinalDir()); outfile.PrintLine("%s", nzbInfo->GetQueuedFilename()); outfile.PrintLine("%s", nzbInfo->GetName()); outfile.PrintLine("%s", nzbInfo->GetCategory()); outfile.PrintLine("%i,%i,%i,%i,%i", (int)nzbInfo->GetPriority(), nzbInfo->GetPostInfo() ? (int)nzbInfo->GetPostInfo()->GetStage() + 1 : 0, (int)nzbInfo->GetDeletePaused(), (int)nzbInfo->GetManyDupeFiles(), nzbInfo->GetFeedId()); outfile.PrintLine("%i,%i,%i,%i,%i,%i,%i,%i,%i", (int)nzbInfo->GetParStatus(), (int)nzbInfo->GetUnpackStatus(), (int)nzbInfo->GetMoveStatus(), (int)nzbInfo->GetParRenameStatus(), (int)nzbInfo->GetRarRenameStatus(), (int)nzbInfo->GetDirectRenameStatus(), (int)nzbInfo->GetDeleteStatus(), (int)nzbInfo->GetMarkStatus(), (int)nzbInfo->GetUrlStatus()); outfile.PrintLine("%i,%i,%i", (int)nzbInfo->GetUnpackCleanedUpDisk(), (int)nzbInfo->GetHealthPaused(), (int)nzbInfo->GetAddUrlPaused()); outfile.PrintLine("%i,%i,%i", nzbInfo->GetFileCount(), nzbInfo->GetParkedFileCount(), nzbInfo->GetMessageCount()); outfile.PrintLine("%i,%i", (int)nzbInfo->GetMinTime(), (int)nzbInfo->GetMaxTime()); outfile.PrintLine("%i,%i,%i,%i", (int)nzbInfo->GetParFull(), nzbInfo->GetPostInfo() ? (int)nzbInfo->GetPostInfo()->GetForceParFull() : 0, nzbInfo->GetPostInfo() ? (int)nzbInfo->GetPostInfo()->GetForceRepair() : 0, nzbInfo->GetExtraParBlocks()); outfile.PrintLine("%u,%u", nzbInfo->GetFullContentHash(), nzbInfo->GetFilteredContentHash()); uint32 High1, Low1, High2, Low2, High3, Low3; Util::SplitInt64(nzbInfo->GetSize(), &High1, &Low1); Util::SplitInt64(nzbInfo->GetSuccessSize(), &High2, &Low2); Util::SplitInt64(nzbInfo->GetFailedSize(), &High3, &Low3); outfile.PrintLine("%u,%u,%u,%u,%u,%u", High1, Low1, High2, Low2, High3, Low3); Util::SplitInt64(nzbInfo->GetParSize(), &High1, &Low1); Util::SplitInt64(nzbInfo->GetParSuccessSize(), &High2, &Low2); Util::SplitInt64(nzbInfo->GetParFailedSize(), &High3, &Low3); outfile.PrintLine("%u,%u,%u,%u,%u,%u", High1, Low1, High2, Low2, High3, Low3); outfile.PrintLine("%i,%i,%i", nzbInfo->GetTotalArticles(), nzbInfo->GetSuccessArticles(), nzbInfo->GetFailedArticles()); outfile.PrintLine("%s", nzbInfo->GetDupeKey()); outfile.PrintLine("%i,%i", (int)nzbInfo->GetDupeMode(), nzbInfo->GetDupeScore()); Util::SplitInt64(nzbInfo->GetDownloadedSize(), &High1, &Low1); outfile.PrintLine("%u,%u,%i,%i,%i,%i,%i", High1, Low1, nzbInfo->GetDownloadSec(), nzbInfo->GetPostTotalSec(), nzbInfo->GetParSec(), nzbInfo->GetRepairSec(), nzbInfo->GetUnpackSec()); outfile.PrintLine("%i", (int)nzbInfo->GetCompletedFiles()->size()); for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { outfile.PrintLine("%i,%i,%u,%i,%s,%s,%s", completedFile.GetId(), (int)completedFile.GetStatus(), completedFile.GetCrc(), (int)completedFile.GetParFile(), completedFile.GetHash16k() ? completedFile.GetHash16k() : "", completedFile.GetParSetId() ? completedFile.GetParSetId() : "", completedFile.GetFilename()); } outfile.PrintLine("%i", (int)nzbInfo->GetParameters()->size()); for (NzbParameter& parameter : nzbInfo->GetParameters()) { outfile.PrintLine("%s=%s", parameter.GetName(), parameter.GetValue()); } outfile.PrintLine("%i", (int)nzbInfo->GetScriptStatuses()->size()); for (ScriptStatus& scriptStatus : nzbInfo->GetScriptStatuses()) { outfile.PrintLine("%i,%s", scriptStatus.GetStatus(), scriptStatus.GetName()); } SaveServerStats(nzbInfo->GetServerStats(), outfile); // save file-infos int size = 0; for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (!fileInfo->GetDeleted()) { size++; } } outfile.PrintLine("%i", size); for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (!fileInfo->GetDeleted()) { outfile.PrintLine("%i,%i,%i", fileInfo->GetId(), (int)fileInfo->GetPaused(), (int)fileInfo->GetExtraPriority()); } } } bool DiskState::LoadNzbInfo(NzbInfo* nzbInfo, Servers* servers, StateDiskFile& infile, int formatVersion) { char buf[10240]; int id; if (infile.ScanLine("%i", &id) != 1) goto error; nzbInfo->SetId(id); int kind; if (infile.ScanLine("%i", &kind) != 1) goto error; nzbInfo->SetKind((NzbInfo::EKind)kind); if (!infile.ReadLine(buf, sizeof(buf))) goto error; nzbInfo->SetUrl(buf); if (!infile.ReadLine(buf, sizeof(buf))) goto error; nzbInfo->SetFilename(buf); if (!infile.ReadLine(buf, sizeof(buf))) goto error; nzbInfo->SetDestDir(buf); if (!infile.ReadLine(buf, sizeof(buf))) goto error; nzbInfo->SetFinalDir(buf); if (!infile.ReadLine(buf, sizeof(buf))) goto error; nzbInfo->SetQueuedFilename(buf); if (!infile.ReadLine(buf, sizeof(buf))) goto error; if (strlen(buf) > 0) { nzbInfo->SetName(buf); } if (!infile.ReadLine(buf, sizeof(buf))) goto error; nzbInfo->SetCategory(buf); int priority, postStage, deletePaused, manyDupeFiles, feedId; if (formatVersion >= 54) { if (infile.ScanLine("%i,%i,%i,%i,%i", &priority, &postStage, &deletePaused, &manyDupeFiles, &feedId) != 5) goto error; } else { if (infile.ScanLine("%i,%i,%i,%i", &priority, &postStage, &deletePaused, &manyDupeFiles) != 4) goto error; feedId = 0; } nzbInfo->SetPriority(priority); nzbInfo->SetDeletePaused((bool)deletePaused); nzbInfo->SetManyDupeFiles((bool)manyDupeFiles); if (postStage > 0) { nzbInfo->EnterPostProcess(); if (formatVersion < 59 && postStage == 6) { postStage++; } else if (formatVersion < 59 && postStage > 6) { postStage += 2; } nzbInfo->GetPostInfo()->SetStage((PostInfo::EStage)postStage); } nzbInfo->SetFeedId(feedId); int parStatus, unpackStatus, moveStatus, parRenameStatus, rarRenameStatus, directRenameStatus, deleteStatus, markStatus, urlStatus; if (formatVersion >= 60) { if (infile.ScanLine("%i,%i,%i,%i,%i,%i,%i,%i,%i", &parStatus, &unpackStatus, &moveStatus, &parRenameStatus, &rarRenameStatus, &directRenameStatus, &deleteStatus, &markStatus, &urlStatus) != 9) goto error; } else if (formatVersion >= 58) { directRenameStatus = 0; if (infile.ScanLine("%i,%i,%i,%i,%i,%i,%i,%i", &parStatus, &unpackStatus, &moveStatus, &parRenameStatus, &rarRenameStatus, &deleteStatus, &markStatus, &urlStatus) != 8) goto error; } else { rarRenameStatus = directRenameStatus = 0; if (infile.ScanLine("%i,%i,%i,%i,%i,%i,%i", &parStatus, &unpackStatus, &moveStatus, &parRenameStatus, &deleteStatus, &markStatus, &urlStatus) != 7) goto error; } nzbInfo->SetParStatus((NzbInfo::EParStatus)parStatus); nzbInfo->SetUnpackStatus((NzbInfo::EPostUnpackStatus)unpackStatus); nzbInfo->SetMoveStatus((NzbInfo::EMoveStatus)moveStatus); nzbInfo->SetParRenameStatus((NzbInfo::EPostRenameStatus)parRenameStatus); nzbInfo->SetRarRenameStatus((NzbInfo::EPostRenameStatus)rarRenameStatus); nzbInfo->SetDirectRenameStatus((NzbInfo::EDirectRenameStatus)directRenameStatus); nzbInfo->SetDeleteStatus((NzbInfo::EDeleteStatus)deleteStatus); nzbInfo->SetMarkStatus((NzbInfo::EMarkStatus)markStatus); if (nzbInfo->GetKind() == NzbInfo::nkNzb || (NzbInfo::EUrlStatus)urlStatus >= NzbInfo::lsFailed || (NzbInfo::EUrlStatus)urlStatus >= NzbInfo::lsScanSkipped) { nzbInfo->SetUrlStatus((NzbInfo::EUrlStatus)urlStatus); } int unpackCleanedUpDisk, healthPaused, addUrlPaused; if (infile.ScanLine("%i,%i,%i", &unpackCleanedUpDisk, &healthPaused, &addUrlPaused) != 3) goto error; nzbInfo->SetUnpackCleanedUpDisk((bool)unpackCleanedUpDisk); nzbInfo->SetHealthPaused((bool)healthPaused); nzbInfo->SetAddUrlPaused((bool)addUrlPaused); int fileCount, parkedFileCount, messageCount; if (formatVersion >= 52) { if (infile.ScanLine("%i,%i,%i", &fileCount, &parkedFileCount, &messageCount) != 3) goto error; } else { if (infile.ScanLine("%i,%i", &fileCount, &parkedFileCount) != 2) goto error; messageCount = 0; } nzbInfo->SetFileCount(fileCount); nzbInfo->SetParkedFileCount(parkedFileCount); nzbInfo->SetMessageCount(messageCount); int minTime, maxTime; if (infile.ScanLine("%i,%i", &minTime, &maxTime) != 2) goto error; nzbInfo->SetMinTime((time_t)minTime); nzbInfo->SetMaxTime((time_t)maxTime); if (formatVersion >= 51) { int parFull, forceParFull, forceRepair, extraParBlocks = 0; if (formatVersion >= 55) { if (infile.ScanLine("%i,%i,%i,%i", &parFull, &forceParFull, &forceRepair, &extraParBlocks) != 4) goto error; } else { if (infile.ScanLine("%i,%i,%i", &parFull, &forceParFull, &forceRepair) != 3) goto error; } nzbInfo->SetParFull((bool)parFull); nzbInfo->SetExtraParBlocks(extraParBlocks); if (nzbInfo->GetPostInfo()) { nzbInfo->GetPostInfo()->SetForceParFull((bool)forceParFull); nzbInfo->GetPostInfo()->SetForceRepair((bool)forceRepair); } } uint32 fullContentHash, filteredContentHash; if (infile.ScanLine("%u,%u", &fullContentHash, &filteredContentHash) != 2) goto error; nzbInfo->SetFullContentHash(fullContentHash); nzbInfo->SetFilteredContentHash(filteredContentHash); uint32 High1, Low1, High2, Low2, High3, Low3; if (infile.ScanLine("%u,%u,%u,%u,%u,%u", &High1, &Low1, &High2, &Low2, &High3, &Low3) != 6) goto error; nzbInfo->SetSize(Util::JoinInt64(High1, Low1)); nzbInfo->SetSuccessSize(Util::JoinInt64(High2, Low2)); nzbInfo->SetFailedSize(Util::JoinInt64(High3, Low3)); nzbInfo->SetCurrentSuccessSize(nzbInfo->GetSuccessSize()); nzbInfo->SetCurrentFailedSize(nzbInfo->GetFailedSize()); if (infile.ScanLine("%u,%u,%u,%u,%u,%u", &High1, &Low1, &High2, &Low2, &High3, &Low3) != 6) goto error; nzbInfo->SetParSize(Util::JoinInt64(High1, Low1)); nzbInfo->SetParSuccessSize(Util::JoinInt64(High2, Low2)); nzbInfo->SetParFailedSize(Util::JoinInt64(High3, Low3)); nzbInfo->SetParCurrentSuccessSize(nzbInfo->GetParSuccessSize()); nzbInfo->SetParCurrentFailedSize(nzbInfo->GetParFailedSize()); int totalArticles, successArticles, failedArticles; if (infile.ScanLine("%i,%i,%i", &totalArticles, &successArticles, &failedArticles) != 3) goto error; nzbInfo->SetTotalArticles(totalArticles); nzbInfo->SetSuccessArticles(successArticles); nzbInfo->SetFailedArticles(failedArticles); nzbInfo->SetCurrentSuccessArticles(successArticles); nzbInfo->SetCurrentFailedArticles(failedArticles); if (!infile.ReadLine(buf, sizeof(buf))) goto error; nzbInfo->SetDupeKey(buf); int dupeMode, dupeScore; if (infile.ScanLine("%i,%i", &dupeMode, &dupeScore) != 2) goto error; nzbInfo->SetDupeMode((EDupeMode)dupeMode); nzbInfo->SetDupeScore(dupeScore); if (formatVersion >= 48) { uint32 High1, Low1, downloadSec, postTotalSec, parSec, repairSec, unpackSec; if (infile.ScanLine("%u,%u,%i,%i,%i,%i,%i", &High1, &Low1, &downloadSec, &postTotalSec, &parSec, &repairSec, &unpackSec) != 7) goto error; nzbInfo->SetDownloadedSize(Util::JoinInt64(High1, Low1)); nzbInfo->SetDownloadSec(downloadSec); nzbInfo->SetPostTotalSec(postTotalSec); nzbInfo->SetParSec(parSec); nzbInfo->SetRepairSec(repairSec); nzbInfo->SetUnpackSec(unpackSec); } if (infile.ScanLine("%i", &fileCount) != 1) goto error; for (int i = 0; i < fileCount; i++) { if (!infile.ReadLine(buf, sizeof(buf))) goto error; int id = 0; char* fileName = buf; int status = 0; uint32 crc = 0; int parFile = 0; char* hash16k = nullptr; char* parSetId = nullptr; if (formatVersion >= 49) { if (formatVersion >= 60) { if (sscanf(buf, "%i,%i,%u,%i", &id, &status, &crc, &parFile) != 4) goto error; hash16k = strchr(buf, ','); if (hash16k) hash16k = strchr(hash16k+1, ','); if (hash16k) hash16k = strchr(hash16k+1, ','); if (hash16k) hash16k = strchr(hash16k+1, ','); if (hash16k) { parSetId = strchr(++hash16k, ','); if (parSetId) { *parSetId++ = '\0'; fileName = strchr(parSetId, ','); if (fileName) *fileName = '\0'; } } } else if (formatVersion >= 50) { if (sscanf(buf, "%i,%i,%u", &id, &status, &crc) != 3) goto error; fileName = strchr(buf, ','); if (fileName) fileName = strchr(fileName+1, ','); if (fileName) fileName = strchr(fileName+1, ','); } else { if (sscanf(buf, "%i,%u", &status, &crc) != 2) goto error; fileName = strchr(buf + 2, ','); } if (fileName) { fileName++; } } nzbInfo->GetCompletedFiles()->emplace_back(id, fileName, (CompletedFile::EStatus)status, crc, (bool)parFile, Util::EmptyStr(hash16k) ? nullptr : hash16k, Util::EmptyStr(parSetId) ? nullptr : parSetId); } int parameterCount; if (infile.ScanLine("%i", ¶meterCount) != 1) goto error; for (int i = 0; i < parameterCount; i++) { if (!infile.ReadLine(buf, sizeof(buf))) goto error; char* value = strchr(buf, '='); if (value) { *value = '\0'; value++; nzbInfo->GetParameters()->SetParameter(buf, value); } } int scriptCount; if (infile.ScanLine("%i", &scriptCount) != 1) goto error; for (int i = 0; i < scriptCount; i++) { if (!infile.ReadLine(buf, sizeof(buf))) goto error; char* scriptName = strchr(buf, ','); if (scriptName) { scriptName++; int status = atoi(buf); if (status > 1 && formatVersion < 25) status--; nzbInfo->GetScriptStatuses()->emplace_back(scriptName, (ScriptStatus::EStatus)status); } } if (!LoadServerStats(nzbInfo->GetServerStats(), servers, infile)) goto error; nzbInfo->GetCurrentServerStats()->ListOp(nzbInfo->GetServerStats(), ServerStatList::soSet); if (formatVersion < 52) { int logCount; if (infile.ScanLine("%i", &logCount) != 1) goto error; for (int i = 0; i < logCount; i++) { if (!infile.ReadLine(buf, sizeof(buf))) goto error; } } if (infile.ScanLine("%i", &fileCount) != 1) goto error; for (int i = 0; i < fileCount; i++) { uint32 id, paused, time; int extraPriority; if (formatVersion >= 56) { if (infile.ScanLine("%i,%i,%i", &id, &paused, &extraPriority) != 3) goto error; } else { if (infile.ScanLine("%i,%i,%i,%i", &id, &paused, &time, &extraPriority) != 4) goto error; } std::unique_ptr fileInfo = std::make_unique(); fileInfo->SetId(id); bool res = LoadFile(fileInfo.get(), true, false); if (res) { fileInfo->SetPaused(paused); if (formatVersion < 56) { fileInfo->SetTime(time); } fileInfo->SetExtraPriority((bool)extraPriority); fileInfo->SetNzbInfo(nzbInfo); nzbInfo->GetFileList()->Add(std::move(fileInfo)); } } return true; error: error("Error reading nzb info from disk"); return false; } void DiskState::SaveServerStats(ServerStatList* serverStatList, StateDiskFile& outfile) { outfile.PrintLine("%i", (int)serverStatList->size()); for (ServerStat& serverStat : serverStatList) { outfile.PrintLine("%i,%i,%i", serverStat.GetServerId(), serverStat.GetSuccessArticles(), serverStat.GetFailedArticles()); } } bool DiskState::LoadServerStats(ServerStatList* serverStatList, Servers* servers, StateDiskFile& infile) { int statCount; if (infile.ScanLine("%i", &statCount) != 1) goto error; for (int i = 0; i < statCount; i++) { int serverId, successArticles, failedArticles; if (infile.ScanLine("%i,%i,%i", &serverId, &successArticles, &failedArticles) != 3) goto error; if (servers) { // find server (id could change if config file was edited) for (NewsServer* newsServer : servers) { if (newsServer->GetStateId() == serverId) { serverStatList->StatOp(newsServer->GetId(), successArticles, failedArticles, ServerStatList::soSet); } } } } return true; error: error("Error reading server stats from disk"); return false; } bool DiskState::SaveFile(FileInfo* fileInfo) { debug("Saving FileInfo %i to disk", fileInfo->GetId()); BString<100> filename("%i", fileInfo->GetId()); StateFile stateFile(filename, 5, false); StateDiskFile* outfile = stateFile.BeginWrite(); if (!outfile) { return false; } return SaveFileInfo(fileInfo, *outfile) && stateFile.FinishWrite(); } bool DiskState::SaveFileInfo(FileInfo* fileInfo, StateDiskFile& outfile) { outfile.PrintLine("%s", fileInfo->GetSubject()); outfile.PrintLine("%s", fileInfo->GetFilename()); outfile.PrintLine("%i,%i", (int)fileInfo->GetFilenameConfirmed(), (int)fileInfo->GetTime()); uint32 High, Low; Util::SplitInt64(fileInfo->GetSize(), &High, &Low); outfile.PrintLine("%u,%u", High, Low); Util::SplitInt64(fileInfo->GetMissedSize(), &High, &Low); outfile.PrintLine("%u,%u", High, Low); outfile.PrintLine("%i", (int)fileInfo->GetParFile()); outfile.PrintLine("%i,%i", fileInfo->GetTotalArticles(), fileInfo->GetMissedArticles()); outfile.PrintLine("%i", (int)fileInfo->GetGroups()->size()); for (CString& group : fileInfo->GetGroups()) { outfile.PrintLine("%s", *group); } outfile.PrintLine("%i", (int)fileInfo->GetArticles()->size()); for (ArticleInfo* articleInfo : fileInfo->GetArticles()) { outfile.PrintLine("%i,%i", articleInfo->GetPartNumber(), articleInfo->GetSize()); outfile.PrintLine("%s", articleInfo->GetMessageId()); } return true; } bool DiskState::LoadArticles(FileInfo* fileInfo) { return LoadFile(fileInfo, false, true); } bool DiskState::LoadFile(FileInfo* fileInfo, bool fileSummary, bool articles) { debug("Loading FileInfo %i from disk", fileInfo->GetId()); BString<100> filename("%i", fileInfo->GetId()); StateFile stateFile(filename, 5, false); StateDiskFile* infile = stateFile.BeginRead(); if (!infile) { return false; } return LoadFileInfo(fileInfo, *infile, stateFile.GetFileVersion(), fileSummary, articles); } bool DiskState::LoadFileInfo(FileInfo* fileInfo, StateDiskFile& infile, int formatVersion, bool fileSummary, bool articles) { char buf[1024]; if (!infile.ReadLine(buf, sizeof(buf))) goto error; if (fileSummary) fileInfo->SetSubject(buf); if (!infile.ReadLine(buf, sizeof(buf))) goto error; if (fileSummary) fileInfo->SetFilename(buf); if (formatVersion >= 5) { int time, filenameConfirmed; if (infile.ScanLine("%i,%i", &filenameConfirmed, &time) != 2) goto error; if (fileSummary) fileInfo->SetFilenameConfirmed((bool)filenameConfirmed); if (fileSummary) fileInfo->SetTime((time_t)time); } else if (formatVersion >= 4) { int time; if (infile.ScanLine("%i", &time) != 1) goto error; if (fileSummary) fileInfo->SetTime((time_t)time); } uint32 High, Low; if (infile.ScanLine("%u,%u", &High, &Low) != 2) goto error; if (fileSummary) fileInfo->SetSize(Util::JoinInt64(High, Low)); if (fileSummary) fileInfo->SetRemainingSize(fileInfo->GetSize()); if (infile.ScanLine("%u,%u", &High, &Low) != 2) goto error; if (fileSummary) fileInfo->SetMissedSize(Util::JoinInt64(High, Low)); if (fileSummary) fileInfo->SetRemainingSize(fileInfo->GetSize() - fileInfo->GetMissedSize()); int parFile; if (infile.ScanLine("%i", &parFile) != 1) goto error; if (fileSummary) fileInfo->SetParFile((bool)parFile); int totalArticles, missedArticles; if (infile.ScanLine("%i,%i", &totalArticles, &missedArticles) != 2) goto error; if (fileSummary) fileInfo->SetTotalArticles(totalArticles); if (fileSummary) fileInfo->SetMissedArticles(missedArticles); int size; if (infile.ScanLine("%i", &size) != 1) goto error; for (int i = 0; i < size; i++) { if (!infile.ReadLine(buf, sizeof(buf))) goto error; if (fileSummary) fileInfo->GetGroups()->push_back(buf); } if (infile.ScanLine("%i", &size) != 1) goto error; if (articles) { for (int i = 0; i < size; i++) { int PartNumber, PartSize; if (infile.ScanLine("%i,%i", &PartNumber, &PartSize) != 2) goto error; if (!infile.ReadLine(buf, sizeof(buf))) goto error; std::unique_ptr articleInfo = std::make_unique(); articleInfo->SetPartNumber(PartNumber); articleInfo->SetSize(PartSize); articleInfo->SetMessageId(buf); fileInfo->GetArticles()->push_back(std::move(articleInfo)); } } return true; error: error("Error reading diskstate for file %i", fileInfo->GetId()); return false; } bool DiskState::SaveFileState(FileInfo* fileInfo, bool completed) { debug("Saving FileState %i to disk", fileInfo->GetId()); BString<100> filename("%i%s", fileInfo->GetId(), completed ? "c" : "s"); StateFile stateFile(filename, 5, false); StateDiskFile* outfile = stateFile.BeginWrite(); if (!outfile) { return false; } return SaveFileState(fileInfo, *outfile, completed); } bool DiskState::SaveFileState(FileInfo* fileInfo, StateDiskFile& outfile, bool completed) { outfile.PrintLine("%i,%i", fileInfo->GetSuccessArticles(), fileInfo->GetFailedArticles()); uint32 High1, Low1, High2, Low2, High3, Low3; Util::SplitInt64(fileInfo->GetRemainingSize(), &High1, &Low1); Util::SplitInt64(fileInfo->GetSuccessSize(), &High2, &Low2); Util::SplitInt64(fileInfo->GetFailedSize(), &High3, &Low3); outfile.PrintLine("%u,%u,%u,%u,%u,%u", High1, Low1, High2, Low2, High3, Low3); outfile.PrintLine("%s", fileInfo->GetFilename()); outfile.PrintLine("%s", fileInfo->GetHash16k()); outfile.PrintLine("%i", (int)fileInfo->GetParFile()); SaveServerStats(fileInfo->GetServerStats(), outfile); outfile.PrintLine("%i", (int)fileInfo->GetArticles()->size()); for (ArticleInfo* articleInfo : fileInfo->GetArticles()) { outfile.PrintLine("%i,%u,%i,%u", (int)articleInfo->GetStatus(), (uint32)articleInfo->GetSegmentOffset(), articleInfo->GetSegmentSize(), (uint32)articleInfo->GetCrc()); } outfile.Close(); return true; } bool DiskState::LoadFileState(FileInfo* fileInfo, Servers* servers, bool completed) { debug("Loading FileInfo %i from disk", fileInfo->GetId()); BString<100> filename("%i%s", fileInfo->GetId(), completed ? "c" : "s"); StateFile stateFile(filename, 5, false); StateDiskFile* infile = stateFile.BeginRead(); if (!infile) { return false; } return LoadFileState(fileInfo, servers, *infile, stateFile.GetFileVersion(), completed); } bool DiskState::LoadFileState(FileInfo* fileInfo, Servers* servers, StateDiskFile& infile, int formatVersion, bool completed) { bool hasArticles = !fileInfo->GetArticles()->empty(); int successArticles, failedArticles; if (infile.ScanLine("%i,%i", &successArticles, &failedArticles) != 2) goto error; fileInfo->SetSuccessArticles(successArticles); fileInfo->SetFailedArticles(failedArticles); uint32 High1, Low1, High2, Low2, High3, Low3; if (infile.ScanLine("%u,%u,%u,%u,%u,%u", &High1, &Low1, &High2, &Low2, &High3, &Low3) != 6) goto error; fileInfo->SetRemainingSize(Util::JoinInt64(High1, Low1)); fileInfo->SetSuccessSize(Util::JoinInt64(High2, Low2)); fileInfo->SetFailedSize(Util::JoinInt64(High3, Low3)); char buf[1024]; if (formatVersion >= 4) { if (!infile.ReadLine(buf, sizeof(buf))) goto error; fileInfo->SetFilename(buf); } if (formatVersion >= 5) { if (!infile.ReadLine(buf, sizeof(buf))) goto error; fileInfo->SetHash16k(*buf ? buf : nullptr); int parFile = 0; if (infile.ScanLine("%i", &parFile) != 1) goto error; fileInfo->SetParFile((bool)parFile); } if (!LoadServerStats(fileInfo->GetServerStats(), servers, infile)) goto error; int completedArticles; completedArticles = 0; //clang requires initialization in a separate line (due to goto statements) int size; if (infile.ScanLine("%i", &size) != 1) goto error; for (int i = 0; i < size; i++) { if (!hasArticles) { fileInfo->GetArticles()->push_back(std::make_unique()); } std::unique_ptr& pa = fileInfo->GetArticles()->at(i); int statusInt; if (formatVersion >= 2) { uint32 segmentOffset, crc; int segmentSize; if (infile.ScanLine("%i,%u,%i,%u", &statusInt, &segmentOffset, &segmentSize, &crc) != 4) goto error; pa->SetSegmentOffset(segmentOffset); pa->SetSegmentSize(segmentSize); pa->SetCrc(crc); } else { if (infile.ScanLine("%i", &statusInt) != 1) goto error; } ArticleInfo::EStatus status = (ArticleInfo::EStatus)statusInt; if (status == ArticleInfo::aiRunning) { status = ArticleInfo::aiUndefined; } if (status == ArticleInfo::aiFinished && !g_Options->GetDirectWrite() && !fileInfo->GetForceDirectWrite() && !pa->GetResultFilename()) { pa->SetResultFilename(BString<1024>("%s%c%i.%03i", g_Options->GetTempDir(), PATH_SEPARATOR, fileInfo->GetId(), pa->GetPartNumber())); } // don't allow all articles be completed or the file will stuck. // such states should never be saved on disk but just in case. if (completedArticles == size - 1 && !completed) { status = ArticleInfo::aiUndefined; } if (status != ArticleInfo::aiUndefined) { completedArticles++; } pa->SetStatus(status); } fileInfo->SetCompletedArticles(completedArticles); infile.Close(); return true; error: infile.Close(); error("Error reading diskstate for file %i", fileInfo->GetId()); return false; } void DiskState::DiscardFiles(NzbInfo* nzbInfo, bool deleteLog) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { DiscardFile(fileInfo->GetId(), true, true, true); } for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { if (completedFile.GetStatus() != CompletedFile::cfSuccess) { DiscardFile(completedFile.GetId(), true, true, true); } } if (deleteLog) { BString<1024> filename; filename.Format("%s%cn%i.log", g_Options->GetQueueDir(), PATH_SEPARATOR, nzbInfo->GetId()); FileSystem::DeleteFile(filename); } } void DiskState::SaveDupInfo(DupInfo* dupInfo, StateDiskFile& outfile) { uint32 High, Low; Util::SplitInt64(dupInfo->GetSize(), &High, &Low); outfile.PrintLine("%i,%u,%u,%u,%u,%i,%i", (int)dupInfo->GetStatus(), High, Low, dupInfo->GetFullContentHash(), dupInfo->GetFilteredContentHash(), dupInfo->GetDupeScore(), (int)dupInfo->GetDupeMode()); outfile.PrintLine("%s", dupInfo->GetName()); outfile.PrintLine("%s", dupInfo->GetDupeKey()); } bool DiskState::LoadDupInfo(DupInfo* dupInfo, StateDiskFile& infile, int formatVersion) { char buf[1024]; int status; uint32 High, Low; uint32 fullContentHash, filteredContentHash = 0; int dupeScore, dupeMode; if (infile.ScanLine("%i,%u,%u,%u,%u,%i,%i", &status, &High, &Low, &fullContentHash, &filteredContentHash, &dupeScore, &dupeMode) != 7) goto error; dupInfo->SetStatus((DupInfo::EStatus)status); dupInfo->SetFullContentHash(fullContentHash); dupInfo->SetFilteredContentHash(filteredContentHash); dupInfo->SetSize(Util::JoinInt64(High, Low)); dupInfo->SetDupeScore(dupeScore); dupInfo->SetDupeMode((EDupeMode)dupeMode); if (!infile.ReadLine(buf, sizeof(buf))) goto error; dupInfo->SetName(buf); if (!infile.ReadLine(buf, sizeof(buf))) goto error; dupInfo->SetDupeKey(buf); return true; error: return false; } void DiskState::SaveHistory(HistoryList* history, StateDiskFile& outfile) { debug("Saving history to disk"); outfile.PrintLine("%i", (int)history->size()); for (HistoryInfo* historyInfo : history) { outfile.PrintLine("%i,%i,%i", historyInfo->GetId(), (int)historyInfo->GetKind(), (int)historyInfo->GetTime()); if (historyInfo->GetKind() == HistoryInfo::hkNzb || historyInfo->GetKind() == HistoryInfo::hkUrl) { SaveNzbInfo(historyInfo->GetNzbInfo(), outfile); } else if (historyInfo->GetKind() == HistoryInfo::hkDup) { SaveDupInfo(historyInfo->GetDupInfo(), outfile); } } } bool DiskState::LoadHistory(HistoryList* history, Servers* servers, StateDiskFile& infile, int formatVersion) { debug("Loading history from disk"); int size; if (infile.ScanLine("%i", &size) != 1) goto error; for (int i = 0; i < size; i++) { std::unique_ptr historyInfo; HistoryInfo::EKind kind = HistoryInfo::hkNzb; int id = 0; int time; int kindval = 0; if (infile.ScanLine("%i,%i,%i", &id, &kindval, &time) != 3) goto error; kind = (HistoryInfo::EKind)kindval; if (kind == HistoryInfo::hkNzb) { std::unique_ptr nzbInfo = std::make_unique(); if (!LoadNzbInfo(nzbInfo.get(), servers, infile, formatVersion)) goto error; nzbInfo->LeavePostProcess(); historyInfo = std::make_unique(std::move(nzbInfo)); } else if (kind == HistoryInfo::hkUrl) { std::unique_ptr nzbInfo = std::make_unique(); if (!LoadNzbInfo(nzbInfo.get(), servers, infile, formatVersion)) goto error; historyInfo = std::make_unique(std::move(nzbInfo)); } else if (kind == HistoryInfo::hkDup) { std::unique_ptr dupInfo = std::make_unique(); if (!LoadDupInfo(dupInfo.get(), infile, formatVersion)) goto error; dupInfo->SetId(id); historyInfo = std::make_unique(std::move(dupInfo)); } historyInfo->SetTime((time_t)time); history->push_back(std::move(historyInfo)); } return true; error: error("Error reading diskstate for history"); return false; } /* * Deletes whole download queue including history. */ void DiskState::DiscardDownloadQueue() { debug("Discarding queue"); BString<1024> fullFilename("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, "queue"); FileSystem::DeleteFile(fullFilename); fullFilename.Format("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, "history"); FileSystem::DeleteFile(fullFilename); DirBrowser dir(g_Options->GetQueueDir()); while (const char* filename = dir.Next()) { // delete all files whose names have only characters '0'..'9' bool onlyNums = true; for (const char* p = filename; *p != '\0'; p++) { if (!('0' <= *p && *p <= '9')) { onlyNums = false; break; } } if (onlyNums) { fullFilename.Format("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, filename); FileSystem::DeleteFile(fullFilename); // delete file state file fullFilename.Format("%s%c%ss", g_Options->GetQueueDir(), PATH_SEPARATOR, filename); FileSystem::DeleteFile(fullFilename); // delete failed info file fullFilename.Format("%s%c%sc", g_Options->GetQueueDir(), PATH_SEPARATOR, filename); FileSystem::DeleteFile(fullFilename); } } } bool DiskState::DownloadQueueExists() { debug("Checking if a saved queue exists on disk"); return FileSystem::FileExists(BString<1024>("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, "queue")) || FileSystem::FileExists(BString<1024>("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, "history")); } void DiskState::DiscardFile(int fileId, bool deleteData, bool deletePartialState, bool deleteCompletedState) { BString<1024> fileName; // info and articles file if (deleteData) { fileName.Format("%s%c%i", g_Options->GetQueueDir(), PATH_SEPARATOR, fileId); FileSystem::DeleteFile(fileName); } // partial state file if (deletePartialState) { fileName.Format("%s%c%is", g_Options->GetQueueDir(), PATH_SEPARATOR, fileId); FileSystem::DeleteFile(fileName); } // completed state file if (deleteCompletedState) { fileName.Format("%s%c%ic", g_Options->GetQueueDir(), PATH_SEPARATOR, fileId); FileSystem::DeleteFile(fileName); } } void DiskState::CleanupTempDir(DownloadQueue* downloadQueue) { DirBrowser dir(g_Options->GetTempDir()); while (const char* filename = dir.Next()) { bool garbage = strstr(filename, ".tmp") || strstr(filename, ".dec"); int id, part; if (!garbage && sscanf(filename, "%i.%i", &id, &part) == 2) { garbage = true; for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { if (nzbInfo->GetFileList()->Find(id)) { garbage = false; break; } } } if (garbage) { BString<1024> fullFilename("%s%c%s", g_Options->GetTempDir(), PATH_SEPARATOR, filename); FileSystem::DeleteFile(fullFilename); } } } void DiskState::CleanupQueueDir(DownloadQueue* downloadQueue) { int deletedFiles = 0; DirBrowser dir(g_Options->GetQueueDir()); while (const char* filename = dir.Next()) { bool del = false; int id; char suffix; if ((sscanf(filename, "%i%c", &id, &suffix) == 2 && (suffix == 's' || suffix == 'c')) || (sscanf(filename, "%i", &id) == 1 && !strchr(filename, '.'))) { for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (fileInfo->GetId() == id) { goto next; } } for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { if (completedFile.GetId() == id) { goto next; } } } for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { if (historyInfo->GetKind() == HistoryInfo::hkNzb) { NzbInfo* nzbInfo = historyInfo->GetNzbInfo(); for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (fileInfo->GetId() == id) { goto next; } } for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { if (completedFile.GetId() == id) { goto next; } } } } del = true; } if (!del && sscanf(filename, "n%i.log", &id) == 1) { for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { if (nzbInfo->GetId() == id) { goto next; } } for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { if (historyInfo->GetKind() == HistoryInfo::hkNzb) { if (historyInfo->GetNzbInfo()->GetId() == id) { goto next; } } } del = true; } if (del) { BString<1024> fullFilename("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, filename); detail("Deleting orphaned diskstate file %s", filename); FileSystem::DeleteFile(fullFilename); deletedFiles++; } next:; } if (deletedFiles > 0) { info("Deleted %i orphaned diskstate file(s)", deletedFiles); } } /* For safety: * - first save to temp-file (feeds.new) * - then delete feeds * - then rename feeds.new to feeds */ bool DiskState::SaveFeeds(Feeds* feeds, FeedHistory* feedHistory) { debug("Saving feeds state to disk"); StateFile stateFile("feeds", 3, true); if (feeds->empty() && feedHistory->empty()) { stateFile.Discard(); return true; } StateDiskFile* outfile = stateFile.BeginWrite(); if (!outfile) { return false; } // save status SaveFeedStatus(feeds, *outfile); // save history SaveFeedHistory(feedHistory, *outfile); // now rename to dest file name return stateFile.FinishWrite(); } bool DiskState::LoadFeeds(Feeds* feeds, FeedHistory* feedHistory) { debug("Loading feeds state from disk"); StateFile stateFile("feeds", 3, true); if (!stateFile.FileExists()) { return true; } StateDiskFile* infile = stateFile.BeginRead(); if (!infile) { return false; } bool ok = false; int formatVersion = stateFile.GetFileVersion(); // load feed status if (!LoadFeedStatus(feeds, *infile, formatVersion)) goto error; // load feed history if (!LoadFeedHistory(feedHistory, *infile, formatVersion)) goto error; ok = true; error: if (!ok) { error("Error reading diskstate for feeds"); } return ok; } bool DiskState::SaveFeedStatus(Feeds* feeds, StateDiskFile& outfile) { debug("Saving feed status to disk"); outfile.PrintLine("%i", (int)feeds->size()); for (FeedInfo* feedInfo : feeds) { outfile.PrintLine("%s", feedInfo->GetUrl()); outfile.PrintLine("%u", feedInfo->GetFilterHash()); outfile.PrintLine("%i", (int)feedInfo->GetLastUpdate()); } return true; } bool DiskState::LoadFeedStatus(Feeds* feeds, StateDiskFile& infile, int formatVersion) { debug("Loading feed status from disk"); int size; if (infile.ScanLine("%i", &size) != 1) goto error; for (int i = 0; i < size; i++) { char url[1024]; if (!infile.ReadLine(url, sizeof(url))) goto error; char filter[1024]; if (formatVersion == 2) { if (!infile.ReadLine(filter, sizeof(filter))) goto error; } uint32 filterHash = 0; if (formatVersion >= 3) { if (infile.ScanLine("%u", &filterHash) != 1) goto error; } int lastUpdate = 0; if (infile.ScanLine("%i", &lastUpdate) != 1) goto error; for (FeedInfo* feedInfo : feeds) { if (!strcmp(feedInfo->GetUrl(), url) && ((formatVersion == 1) || (formatVersion == 2 && !strcmp(feedInfo->GetFilter(), filter)) || (formatVersion >= 3 && feedInfo->GetFilterHash() == filterHash))) { feedInfo->SetLastUpdate((time_t)lastUpdate); } } } return true; error: error("Error reading feed status from disk"); return false; } bool DiskState::SaveFeedHistory(FeedHistory* feedHistory, StateDiskFile& outfile) { debug("Saving feed history to disk"); outfile.PrintLine("%i", (int)feedHistory->size()); for (FeedHistoryInfo& feedHistoryInfo : feedHistory) { outfile.PrintLine("%i,%i", (int)feedHistoryInfo.GetStatus(), (int)feedHistoryInfo.GetLastSeen()); outfile.PrintLine("%s", feedHistoryInfo.GetUrl()); } return true; } bool DiskState::LoadFeedHistory(FeedHistory* feedHistory, StateDiskFile& infile, int formatVersion) { debug("Loading feed history from disk"); int size; if (infile.ScanLine("%i", &size) != 1) goto error; for (int i = 0; i < size; i++) { int status = 0; int lastSeen = 0; int r = infile.ScanLine("%i,%i", &status, &lastSeen); if (r != 2) goto error; char url[1024]; if (!infile.ReadLine(url, sizeof(url))) goto error; feedHistory->emplace_back(url, (FeedHistoryInfo::EStatus)(status), (time_t)(lastSeen)); } return true; error: error("Error reading feed history from disk"); return false; } void DiskState::CalcFileStats(DownloadQueue* downloadQueue, int formatVersion) { for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { nzbInfo->UpdateCurrentStats(); } } bool DiskState::LoadAllFileStates(DownloadQueue* downloadQueue, Servers* servers) { BString<1024> cacheFlagFilename("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, "acache"); bool cacheWasActive = FileSystem::FileExists(cacheFlagFilename); DirBrowser dir(g_Options->GetQueueDir()); while (const char* filename = dir.Next()) { int id; char suffix; if (sscanf(filename, "%i%c", &id, &suffix) == 2) { if (suffix == 'c' || (suffix == 's' && g_Options->GetContinuePartial() && !cacheWasActive)) { for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (fileInfo->GetId() == id) { if (!LoadFileState(fileInfo, servers, suffix == 'c')) goto error; fileInfo->GetArticles()->clear(); fileInfo->SetPartialState(suffix == 'c' ? FileInfo::psCompleted : FileInfo::psPartial); goto next; } } } } else { BString<1024> fullFilename("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, filename); FileSystem::DeleteFile(fullFilename); } } next:; } return true; error: return false; } bool DiskState::SaveStats(Servers* servers, ServerVolumes* serverVolumes) { debug("Saving stats to disk"); StateFile stateFile("stats", 3, true); if (servers->empty()) { stateFile.Discard(); return true; } StateDiskFile* outfile = stateFile.BeginWrite(); if (!outfile) { return false; } // save server names SaveServerInfo(servers, *outfile); // save stat SaveVolumeStat(serverVolumes, *outfile); // now rename to dest file name return stateFile.FinishWrite(); } bool DiskState::LoadStats(Servers* servers, ServerVolumes* serverVolumes, bool* perfectMatch) { debug("Loading stats from disk"); StateFile stateFile("stats", 3, true); if (!stateFile.FileExists()) { return true; } StateDiskFile* infile = stateFile.BeginRead(); if (!infile) { return false; } bool ok = false; int formatVersion = stateFile.GetFileVersion(); if (!LoadServerInfo(servers, *infile, formatVersion, perfectMatch)) goto error; if (formatVersion >=2) { if (!LoadVolumeStat(servers, serverVolumes, *infile, formatVersion)) goto error; } ok = true; error: if (!ok) { error("Error reading diskstate for statistics"); } return ok; } bool DiskState::SaveServerInfo(Servers* servers, StateDiskFile& outfile) { debug("Saving server info to disk"); outfile.PrintLine("%i", (int)servers->size()); for (NewsServer* newsServer : servers) { outfile.PrintLine("%s", newsServer->GetName()); outfile.PrintLine("%s", newsServer->GetHost()); outfile.PrintLine("%i", newsServer->GetPort()); outfile.PrintLine("%s", newsServer->GetUser()); } return true; } /* *************************************************************************************** * Server matching */ class ServerRef { public: int m_stateId; CString m_name; CString m_host; int m_port; CString m_user; bool m_matched; bool m_perfect; int GetStateId() { return m_stateId; } const char* GetName() { return m_name; } const char* GetHost() { return m_host; } int GetPort() { return m_port; } const char* GetUser() { return m_user; } bool GetMatched() { return m_matched; } void SetMatched(bool matched) { m_matched = matched; } bool GetPerfect() { return m_perfect; } void SetPerfect(bool perfect) { m_perfect = perfect; } }; typedef std::vector ServerRefList; class OwnedServerRefList : public ServerRefList { public: ~OwnedServerRefList() { for (ServerRef* ref : this) { delete ref; } } }; enum ECriteria { name, host, port, user }; void FindCandidates(NewsServer* newsServer, ServerRefList* refs, ECriteria criteria, bool keepIfNothing) { ServerRefList originalRefs; originalRefs.insert(originalRefs.begin(), refs->begin(), refs->end()); refs->erase(std::remove_if(refs->begin(), refs->end(), [newsServer, criteria](ServerRef* ref) { bool match = false; switch(criteria) { case name: match = !strcasecmp(newsServer->GetName(), ref->GetName()); break; case host: match = !strcasecmp(newsServer->GetHost(), ref->GetHost()); break; case port: match = newsServer->GetPort() == ref->GetPort(); break; case user: match = !strcasecmp(newsServer->GetUser(), ref->GetUser()); break; } return !match || ref->GetMatched(); }), refs->end()); if (refs->size() == 0 && keepIfNothing) { refs->insert(refs->begin(), originalRefs.begin(), originalRefs.end()); } } void MatchServers(Servers* servers, ServerRefList* serverRefs) { // Step 1: trying perfect match for (NewsServer* newsServer : servers) { ServerRefList matchedRefs; matchedRefs.insert(matchedRefs.begin(), serverRefs->begin(), serverRefs->end()); FindCandidates(newsServer, &matchedRefs, name, false); FindCandidates(newsServer, &matchedRefs, host, false); FindCandidates(newsServer, &matchedRefs, port, false); FindCandidates(newsServer, &matchedRefs, user, false); if (matchedRefs.size() == 1) { ServerRef* ref = matchedRefs.front(); newsServer->SetStateId(ref->GetStateId()); ref->SetMatched(true); ref->SetPerfect(true); } } // Step 2: matching host, port, username and server-name for (NewsServer* newsServer : servers) { if (!newsServer->GetStateId()) { ServerRefList matchedRefs; matchedRefs.insert(matchedRefs.begin(), serverRefs->begin(), serverRefs->end()); FindCandidates(newsServer, &matchedRefs, host, false); if (matchedRefs.size() > 1) { FindCandidates(newsServer, &matchedRefs, name, true); } if (matchedRefs.size() > 1) { FindCandidates(newsServer, &matchedRefs, user, true); } if (matchedRefs.size() > 1) { FindCandidates(newsServer, &matchedRefs, port, true); } if (!matchedRefs.empty()) { ServerRef* ref = matchedRefs.front(); newsServer->SetStateId(ref->GetStateId()); ref->SetMatched(true); } } } } /* * END: Server matching *************************************************************************************** */ bool DiskState::LoadServerInfo(Servers* servers, StateDiskFile& infile, int formatVersion, bool* perfectMatch) { debug("Loading server info from disk"); OwnedServerRefList serverRefs; *perfectMatch = true; int size; if (infile.ScanLine("%i", &size) != 1) goto error; for (int i = 0; i < size; i++) { char name[1024]; if (!infile.ReadLine(name, sizeof(name))) goto error; char host[200]; if (!infile.ReadLine(host, sizeof(host))) goto error; int port; if (infile.ScanLine("%i", &port) != 1) goto error; char user[100]; if (!infile.ReadLine(user, sizeof(user))) goto error; std::unique_ptr ref = std::make_unique(); ref->m_stateId = i + 1; ref->m_name = name; ref->m_host = host; ref->m_port = port; ref->m_user = user; ref->m_matched = false; ref->m_perfect = false; serverRefs.push_back(ref.release()); } MatchServers(servers, &serverRefs); for (ServerRef* ref : serverRefs) { *perfectMatch = *perfectMatch && ref->GetPerfect(); } debug("******** MATCHING NEWS-SERVERS **********"); for (NewsServer* newsServer : servers) { *perfectMatch = *perfectMatch && newsServer->GetStateId(); debug("Server %i -> %i", newsServer->GetId(), newsServer->GetStateId()); debug("Server %i.Name: %s", newsServer->GetId(), newsServer->GetName()); debug("Server %i.Host: %s:%i", newsServer->GetId(), newsServer->GetHost(), newsServer->GetPort()); } debug("All servers perfectly matched: %s", *perfectMatch ? "yes" : "no"); return true; error: error("Error reading server info from disk"); return false; } bool DiskState::SaveVolumeStat(ServerVolumes* serverVolumes, StateDiskFile& outfile) { debug("Saving volume stats to disk"); outfile.PrintLine("%i", (int)serverVolumes->size()); for (ServerVolume& serverVolume : serverVolumes) { outfile.PrintLine("%i,%i,%i", serverVolume.GetFirstDay(), (int)serverVolume.GetDataTime(), (int)serverVolume.GetCustomTime()); uint32 High1, Low1, High2, Low2; Util::SplitInt64(serverVolume.GetTotalBytes(), &High1, &Low1); Util::SplitInt64(serverVolume.GetCustomBytes(), &High2, &Low2); outfile.PrintLine("%u,%u,%u,%u", High1, Low1, High2, Low2); ServerVolume::VolumeArray* VolumeArrays[] = { serverVolume.BytesPerSeconds(), serverVolume.BytesPerMinutes(), serverVolume.BytesPerHours(), serverVolume.BytesPerDays() }; for (int i=0; i < 4; i++) { ServerVolume::VolumeArray* volumeArray = VolumeArrays[i]; outfile.PrintLine("%i", (int)volumeArray->size()); for (int64 bytes : *volumeArray) { Util::SplitInt64(bytes, &High1, &Low1); outfile.PrintLine("%u,%u", High1, Low1); } } } return true; } bool DiskState::LoadVolumeStat(Servers* servers, ServerVolumes* serverVolumes, StateDiskFile& infile, int formatVersion) { debug("Loading volume stats from disk"); int size; if (infile.ScanLine("%i", &size) != 1) goto error; for (int i = 0; i < size; i++) { ServerVolume* serverVolume = nullptr; if (i == 0) { serverVolume = &serverVolumes->at(0); } else { for (NewsServer* newsServer : servers) { if (newsServer->GetStateId() == i) { serverVolume = &serverVolumes->at(newsServer->GetId()); } } } int firstDay, dataTime, customTime; uint32 High1, Low1, High2 = 0, Low2 = 0; if (formatVersion >= 3) { if (infile.ScanLine("%i,%i,%i", &firstDay, &dataTime,&customTime) != 3) goto error; if (infile.ScanLine("%u,%u,%u,%u", &High1, &Low1, &High2, &Low2) != 4) goto error; if (serverVolume) serverVolume->SetCustomTime((time_t)customTime); } else { if (infile.ScanLine("%i,%i", &firstDay, &dataTime) != 2) goto error; if (infile.ScanLine("%u,%u", &High1, &Low1) != 2) goto error; } if (serverVolume) serverVolume->SetFirstDay(firstDay); if (serverVolume) serverVolume->SetDataTime((time_t)dataTime); if (serverVolume) serverVolume->SetTotalBytes(Util::JoinInt64(High1, Low1)); if (serverVolume) serverVolume->SetCustomBytes(Util::JoinInt64(High2, Low2)); ServerVolume::VolumeArray* VolumeArrays[] = { serverVolume ? serverVolume->BytesPerSeconds() : nullptr, serverVolume ? serverVolume->BytesPerMinutes() : nullptr, serverVolume ? serverVolume->BytesPerHours() : nullptr, serverVolume ? serverVolume->BytesPerDays() : nullptr }; for (int k=0; k < 4; k++) { ServerVolume::VolumeArray* volumeArray = VolumeArrays[k]; int arrSize; if (infile.ScanLine("%i", &arrSize) != 1) goto error; if (volumeArray) volumeArray->resize(arrSize); for (int j = 0; j < arrSize; j++) { if (infile.ScanLine("%u,%u", &High1, &Low1) != 2) goto error; if (volumeArray) (*volumeArray)[j] = Util::JoinInt64(High1, Low1); } } } return true; error: error("Error reading volume stats from disk"); return false; } void DiskState::WriteCacheFlag() { BString<1024> flagFilename("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, "acache"); StateDiskFile outfile; if (!outfile.Open(flagFilename, StateDiskFile::omWrite)) { error("Error saving diskstate: Could not create file %s", *flagFilename); return; } outfile.Close(); } void DiskState::DeleteCacheFlag() { BString<1024> flagFilename("%s%c%s", g_Options->GetQueueDir(), PATH_SEPARATOR, "acache"); FileSystem::DeleteFile(flagFilename); } void DiskState::AppendNzbMessage(int nzbId, Message::EKind kind, const char* text) { BString<1024> logFilename("%s%cn%i.log", g_Options->GetQueueDir(), PATH_SEPARATOR, nzbId); StateDiskFile outfile; if (!outfile.Open(logFilename, StateDiskFile::omAppend)) { error("Error saving log: Could not create file %s", *logFilename); return; } const char* messageType[] = { "INFO", "WARNING", "ERROR", "DEBUG", "DETAIL"}; BString<1024> tmp2; tmp2 = text; // replace bad chars for (char* p = tmp2; *p; p++) { char ch = *p; if (ch == '\n' || ch == '\r' || ch == '\t') { *p = ' '; } } time_t tm = Util::CurrentTime(); time_t rawtime = tm + g_Options->GetTimeCorrection(); BString<100> time; Util::FormatTime(rawtime, time, 100); outfile.Print("%s\t%u\t%s\t%s%s", *time, (int)tm, messageType[kind], *tmp2, LINE_ENDING); outfile.Close(); } void DiskState::LoadNzbMessages(int nzbId, MessageList* messages) { // Important: // - Other threads may be writing into the log-file at any time; // - The log-file may also be deleted from another thread; BString<1024> logFilename("%s%cn%i.log", g_Options->GetQueueDir(), PATH_SEPARATOR, nzbId); if (!FileSystem::FileExists(logFilename)) { return; } StateDiskFile infile; if (!infile.Open(logFilename, StateDiskFile::omRead)) { error("Error reading log: could not open file %s", *logFilename); return; } int id = 0; char line[1024]; while (infile.ReadLine(line, sizeof(line))) { Util::TrimRight(line); // time (skip formatted time first) char* p = strchr(line, '\t'); if (!p) goto exit; int time = atoi(p + 1); // kind p = strchr(p + 1, '\t'); if (!p) goto exit; char* kindStr = p + 1; Message::EKind kind = Message::mkError; if (!strncmp(kindStr, "INFO", 4)) { kind = Message::mkInfo; } else if (!strncmp(kindStr, "WARNING", 7)) { kind = Message::mkWarning; } else if (!strncmp(kindStr, "ERROR", 5)) { kind = Message::mkError; } else if (!strncmp(kindStr, "DETAIL", 6)) { kind = Message::mkDetail; } else if (!strncmp(kindStr, "DEBUG", 5)) { kind = Message::mkDebug; } // text p = strchr(p + 1, '\t'); if (!p) goto exit; char* text = p + 1; messages->emplace_back(++id, kind, (time_t)time, text); } exit: infile.Close(); return; } nzbget-19.1/daemon/queue/DupeCoordinator.cpp0000644000175000017500000005124313130203062020740 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "Options.h" #include "Log.h" #include "Util.h" #include "NzbFile.h" #include "HistoryCoordinator.h" #include "DupeCoordinator.h" #include "QueueScript.h" bool DupeCoordinator::SameNameOrKey(const char* name1, const char* dupeKey1, const char* name2, const char* dupeKey2) { bool hasDupeKeys = !Util::EmptyStr(dupeKey1) && !Util::EmptyStr(dupeKey2); return (hasDupeKeys && !strcmp(dupeKey1, dupeKey2)) || (!hasDupeKeys && !strcmp(name1, name2)); } /** Check if the title was already downloaded or is already queued: - if there is a duplicate with exactly same content (via hash-check) in queue or in history - the new item is skipped; - if there is a duplicate marked as good in history - the new item is skipped; - if there is a duplicate with success-status in dup-history but there are no duplicates in recent history - the new item is skipped; - if queue has a duplicate with the same or higher score - the new item is moved to history as dupe-backup; - if queue has a duplicate with lower score - the existing item is moved to history as dupe-backup (unless it is in post-processing stage) and the new item is added to queue; - if queue doesn't have duplicates - the new item is added to queue. */ void DupeCoordinator::NzbFound(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) { debug("Checking duplicates for %s", nzbInfo->GetName()); // find duplicates in download queue with exactly same content for (NzbInfo* queuedNzbInfo : downloadQueue->GetQueue()) { bool sameContent = (nzbInfo->GetFullContentHash() > 0 && nzbInfo->GetFullContentHash() == queuedNzbInfo->GetFullContentHash()) || (nzbInfo->GetFilteredContentHash() > 0 && nzbInfo->GetFilteredContentHash() == queuedNzbInfo->GetFilteredContentHash()); // if there is a duplicate with exactly same content (via hash-check) // in queue - the new item is skipped if (queuedNzbInfo != nzbInfo && sameContent && nzbInfo->GetKind() == NzbInfo::nkNzb) { BString<1024> message; if (!strcmp(nzbInfo->GetName(), queuedNzbInfo->GetName())) { message.Format("Skipping duplicate %s, already queued", nzbInfo->GetName()); } else { message.Format("Skipping duplicate %s, already queued as %s", nzbInfo->GetName(), queuedNzbInfo->GetName()); } if (nzbInfo->GetFeedId()) { warn("%s", *message); // Flag saying QueueCoordinator to skip nzb-file nzbInfo->SetDeleteStatus(NzbInfo::dsManual); g_HistoryCoordinator->DeleteDiskFiles(nzbInfo); } else { nzbInfo->SetDeleteStatus(NzbInfo::dsCopy); nzbInfo->AddMessage(Message::mkWarning, message); } return; } } // if download has empty dupekey and empty dupescore - check if download queue // or history have an item with the same name and non empty dupekey or dupescore and // take these properties from this item if (Util::EmptyStr(nzbInfo->GetDupeKey()) && nzbInfo->GetDupeScore() == 0) { for (NzbInfo* queuedNzbInfo : downloadQueue->GetQueue()) { if (!strcmp(queuedNzbInfo->GetName(), nzbInfo->GetName()) && (!Util::EmptyStr(queuedNzbInfo->GetDupeKey()) || queuedNzbInfo->GetDupeScore() != 0)) { nzbInfo->SetDupeKey(queuedNzbInfo->GetDupeKey()); nzbInfo->SetDupeScore(queuedNzbInfo->GetDupeScore()); info("Assigning dupekey %s and dupescore %i to %s from existing queue item with the same name", nzbInfo->GetDupeKey(), nzbInfo->GetDupeScore(), nzbInfo->GetName()); break; } } } if (Util::EmptyStr(nzbInfo->GetDupeKey()) && nzbInfo->GetDupeScore() == 0) { for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { if (historyInfo->GetKind() == HistoryInfo::hkNzb && !strcmp(historyInfo->GetNzbInfo()->GetName(), nzbInfo->GetName()) && (!Util::EmptyStr(historyInfo->GetNzbInfo()->GetDupeKey()) || historyInfo->GetNzbInfo()->GetDupeScore() != 0)) { nzbInfo->SetDupeKey(historyInfo->GetNzbInfo()->GetDupeKey()); nzbInfo->SetDupeScore(historyInfo->GetNzbInfo()->GetDupeScore()); info("Assigning dupekey %s and dupescore %i to %s from existing history item with the same name", nzbInfo->GetDupeKey(), nzbInfo->GetDupeScore(), nzbInfo->GetName()); break; } if (historyInfo->GetKind() == HistoryInfo::hkDup && !strcmp(historyInfo->GetDupInfo()->GetName(), nzbInfo->GetName()) && (!Util::EmptyStr(historyInfo->GetDupInfo()->GetDupeKey()) || historyInfo->GetDupInfo()->GetDupeScore() != 0)) { nzbInfo->SetDupeKey(historyInfo->GetDupInfo()->GetDupeKey()); nzbInfo->SetDupeScore(historyInfo->GetDupInfo()->GetDupeScore()); info("Assigning dupekey %s and dupescore %i to %s from existing history item with the same name", nzbInfo->GetDupeKey(), nzbInfo->GetDupeScore(), nzbInfo->GetName()); break; } } } // find duplicates in history bool skip = false; bool good = false; bool sameContent = false; const char* dupeName = nullptr; // find duplicates in history having exactly same content // also: nzb-files having duplicates marked as good are skipped // also (only in score mode): nzb-files having success-duplicates in dup-history but not having duplicates in recent history are skipped for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { if (historyInfo->GetKind() == HistoryInfo::hkNzb && ((nzbInfo->GetFullContentHash() > 0 && nzbInfo->GetFullContentHash() == historyInfo->GetNzbInfo()->GetFullContentHash()) || (nzbInfo->GetFilteredContentHash() > 0 && nzbInfo->GetFilteredContentHash() == historyInfo->GetNzbInfo()->GetFilteredContentHash()))) { skip = true; sameContent = true; dupeName = historyInfo->GetNzbInfo()->GetName(); break; } if (historyInfo->GetKind() == HistoryInfo::hkDup && ((nzbInfo->GetFullContentHash() > 0 && nzbInfo->GetFullContentHash() == historyInfo->GetDupInfo()->GetFullContentHash()) || (nzbInfo->GetFilteredContentHash() > 0 && nzbInfo->GetFilteredContentHash() == historyInfo->GetDupInfo()->GetFilteredContentHash()))) { skip = true; sameContent = true; dupeName = historyInfo->GetDupInfo()->GetName(); break; } if (historyInfo->GetKind() == HistoryInfo::hkNzb && historyInfo->GetNzbInfo()->GetDupeMode() != dmForce && historyInfo->GetNzbInfo()->GetMarkStatus() == NzbInfo::ksGood && SameNameOrKey(historyInfo->GetNzbInfo()->GetName(), historyInfo->GetNzbInfo()->GetDupeKey(), nzbInfo->GetName(), nzbInfo->GetDupeKey())) { skip = true; good = true; dupeName = historyInfo->GetNzbInfo()->GetName(); break; } if (historyInfo->GetKind() == HistoryInfo::hkDup && historyInfo->GetDupInfo()->GetDupeMode() != dmForce && (historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsGood || (nzbInfo->GetDupeMode() == dmScore && historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsSuccess && nzbInfo->GetDupeScore() <= historyInfo->GetDupInfo()->GetDupeScore())) && SameNameOrKey(historyInfo->GetDupInfo()->GetName(), historyInfo->GetDupInfo()->GetDupeKey(), nzbInfo->GetName(), nzbInfo->GetDupeKey())) { skip = true; good = historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsGood; dupeName = historyInfo->GetDupInfo()->GetName(); break; } } if (!sameContent && !good && nzbInfo->GetDupeMode() == dmScore) { // nzb-files having success-duplicates in recent history (with different content) are added to history for backup for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { if (historyInfo->GetKind() == HistoryInfo::hkNzb && historyInfo->GetNzbInfo()->GetDupeMode() != dmForce && SameNameOrKey(historyInfo->GetNzbInfo()->GetName(), historyInfo->GetNzbInfo()->GetDupeKey(), nzbInfo->GetName(), nzbInfo->GetDupeKey()) && nzbInfo->GetDupeScore() <= historyInfo->GetNzbInfo()->GetDupeScore() && historyInfo->GetNzbInfo()->IsDupeSuccess()) { // Flag saying QueueCoordinator to skip nzb-file nzbInfo->SetDeleteStatus(NzbInfo::dsDupe); info("Collection %s is a duplicate to %s", nzbInfo->GetName(), historyInfo->GetNzbInfo()->GetName()); return; } } } if (skip) { BString<1024> message; if (!strcmp(nzbInfo->GetName(), dupeName)) { message.Format("Skipping duplicate %s, found in history with %s", nzbInfo->GetName(), sameContent ? "exactly same content" : good ? "good status" : "success status"); } else { message.Format("Skipping duplicate %s, found in history %s with %s", nzbInfo->GetName(), dupeName, sameContent ? "exactly same content" : good ? "good status" : "success status"); } if (nzbInfo->GetFeedId()) { warn("%s", *message); // Flag saying QueueCoordinator to skip nzb-file nzbInfo->SetDeleteStatus(NzbInfo::dsManual); g_HistoryCoordinator->DeleteDiskFiles(nzbInfo); } else { nzbInfo->SetDeleteStatus(sameContent ? NzbInfo::dsCopy : NzbInfo::dsGood); nzbInfo->AddMessage(Message::mkWarning, message); } return; } // find duplicates in download queue and post-queue and handle both items according to their scores: // only one item remains in queue and another one is moved to history as dupe-backup if (nzbInfo->GetDupeMode() == dmScore) { // find duplicates in download queue int index = 0; for (NzbList::iterator it = downloadQueue->GetQueue()->begin(); it != downloadQueue->GetQueue()->end(); index++) { NzbInfo* queuedNzbInfo = (*it++).get(); if (queuedNzbInfo != nzbInfo && queuedNzbInfo->GetKind() == NzbInfo::nkNzb && queuedNzbInfo->GetDupeMode() != dmForce && SameNameOrKey(queuedNzbInfo->GetName(), queuedNzbInfo->GetDupeKey(), nzbInfo->GetName(), nzbInfo->GetDupeKey())) { // if queue has a duplicate with the same or higher score - the new item // is moved to history as dupe-backup if (nzbInfo->GetDupeScore() <= queuedNzbInfo->GetDupeScore()) { // Flag saying QueueCoordinator to skip nzb-file nzbInfo->SetDeleteStatus(NzbInfo::dsDupe); info("Collection %s is a duplicate to %s", nzbInfo->GetName(), queuedNzbInfo->GetName()); return; } // if queue has a duplicate with lower score - the existing item is moved // to history as dupe-backup (unless it is in post-processing stage) and // the new item is added to queue (unless it is in post-processing stage) if (!queuedNzbInfo->GetPostInfo()) { // the existing queue item is moved to history as dupe-backup info("Moving collection %s with lower duplicate score to history", queuedNzbInfo->GetName()); queuedNzbInfo->SetDeleteStatus(NzbInfo::dsDupe); downloadQueue->EditEntry(queuedNzbInfo->GetId(), DownloadQueue::eaGroupDelete, nullptr); it = downloadQueue->GetQueue()->begin() + index; } } } } } /** - if download of an item fails and there are duplicates in history - return the best duplicate from history to queue for download; - if download of an item completes successfully - nothing extra needs to be done; */ void DupeCoordinator::NzbCompleted(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) { debug("Processing duplicates for %s", nzbInfo->GetName()); if (nzbInfo->GetDupeMode() == dmScore && !nzbInfo->IsDupeSuccess()) { ReturnBestDupe(downloadQueue, nzbInfo, nzbInfo->GetName(), nzbInfo->GetDupeKey()); } } /** Returns the best duplicate from history to download queue. */ void DupeCoordinator::ReturnBestDupe(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, const char* nzbName, const char* dupeKey) { // check if history (recent or dup) has other success-duplicates or good-duplicates bool dupeFound = false; int historyScore = 0; for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { bool goodDupe = false; if (historyInfo->GetKind() == HistoryInfo::hkNzb && historyInfo->GetNzbInfo()->GetDupeMode() != dmForce && historyInfo->GetNzbInfo()->IsDupeSuccess() && SameNameOrKey(historyInfo->GetNzbInfo()->GetName(), historyInfo->GetNzbInfo()->GetDupeKey(), nzbName, dupeKey)) { if (!dupeFound || historyInfo->GetNzbInfo()->GetDupeScore() > historyScore) { historyScore = historyInfo->GetNzbInfo()->GetDupeScore(); } dupeFound = true; goodDupe = historyInfo->GetNzbInfo()->GetMarkStatus() == NzbInfo::ksGood; } if (historyInfo->GetKind() == HistoryInfo::hkDup && historyInfo->GetDupInfo()->GetDupeMode() != dmForce && (historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsSuccess || historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsGood) && SameNameOrKey(historyInfo->GetDupInfo()->GetName(), historyInfo->GetDupInfo()->GetDupeKey(), nzbName, dupeKey)) { if (!dupeFound || historyInfo->GetDupInfo()->GetDupeScore() > historyScore) { historyScore = historyInfo->GetDupInfo()->GetDupeScore(); } dupeFound = true; goodDupe = historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsGood; } if (goodDupe) { // another duplicate with good-status exists - exit without moving other dupes to queue return; } } // check if duplicates exist in download queue bool queueDupe = false; int queueScore = 0; for (NzbInfo* queuedNzbInfo : downloadQueue->GetQueue()) { if (queuedNzbInfo != nzbInfo && queuedNzbInfo->GetKind() == NzbInfo::nkNzb && queuedNzbInfo->GetDupeMode() != dmForce && SameNameOrKey(queuedNzbInfo->GetName(), queuedNzbInfo->GetDupeKey(), nzbName, dupeKey) && (!queueDupe || queuedNzbInfo->GetDupeScore() > queueScore)) { queueScore = queuedNzbInfo->GetDupeScore(); queueDupe = true; } } // find dupe-backup with highest score, whose score is also higher than other // success-duplicates and higher than already queued items HistoryInfo* historyDupe = nullptr; for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { if (historyInfo->GetKind() == HistoryInfo::hkNzb && historyInfo->GetNzbInfo()->GetDupeMode() != dmForce && historyInfo->GetNzbInfo()->GetDeleteStatus() == NzbInfo::dsDupe && historyInfo->GetNzbInfo()->CalcHealth() >= historyInfo->GetNzbInfo()->CalcCriticalHealth(true) && historyInfo->GetNzbInfo()->GetMarkStatus() != NzbInfo::ksBad && (!dupeFound || historyInfo->GetNzbInfo()->GetDupeScore() > historyScore) && (!queueDupe || historyInfo->GetNzbInfo()->GetDupeScore() > queueScore) && (!historyDupe || historyInfo->GetNzbInfo()->GetDupeScore() > historyDupe->GetNzbInfo()->GetDupeScore()) && SameNameOrKey(historyInfo->GetNzbInfo()->GetName(), historyInfo->GetNzbInfo()->GetDupeKey(), nzbName, dupeKey)) { historyDupe = historyInfo; } } // move that dupe-backup from history to download queue if (historyDupe) { info("Found duplicate %s for %s", historyDupe->GetNzbInfo()->GetName(), nzbName); g_HistoryCoordinator->Redownload(downloadQueue, historyDupe); } } void DupeCoordinator::HistoryMark(DownloadQueue* downloadQueue, HistoryInfo* historyInfo, NzbInfo::EMarkStatus markStatus) { const char* markStatusName[] = { "NONE", "bad", "good", "success" }; info("Marking %s as %s", historyInfo->GetName(), markStatusName[markStatus]); if (historyInfo->GetKind() == HistoryInfo::hkNzb) { historyInfo->GetNzbInfo()->SetMarkStatus(markStatus); g_QueueScriptCoordinator->EnqueueScript(historyInfo->GetNzbInfo(), QueueScriptCoordinator::qeNzbMarked); } else if (historyInfo->GetKind() == HistoryInfo::hkDup) { historyInfo->GetDupInfo()->SetStatus( markStatus == NzbInfo::ksGood ? DupInfo::dsGood : markStatus == NzbInfo::ksSuccess ? DupInfo::dsSuccess : DupInfo::dsBad); } else { error("Could not mark %s as bad: history item has wrong type", historyInfo->GetName()); return; } if (!g_Options->GetDupeCheck() || (historyInfo->GetKind() == HistoryInfo::hkNzb && historyInfo->GetNzbInfo()->GetDupeMode() == dmForce) || (historyInfo->GetKind() == HistoryInfo::hkDup && historyInfo->GetDupInfo()->GetDupeMode() == dmForce)) { return; } if (markStatus == NzbInfo::ksGood) { // mark as good // moving all duplicates from history to dup-history HistoryCleanup(downloadQueue, historyInfo); } else if (markStatus == NzbInfo::ksBad) { // mark as bad const char* dupeKey = historyInfo->GetKind() == HistoryInfo::hkNzb ? historyInfo->GetNzbInfo()->GetDupeKey() : historyInfo->GetKind() == HistoryInfo::hkDup ? historyInfo->GetDupInfo()->GetDupeKey() : nullptr; ReturnBestDupe(downloadQueue, nullptr, historyInfo->GetName(), dupeKey); } } void DupeCoordinator::HistoryCleanup(DownloadQueue* downloadQueue, HistoryInfo* markHistoryInfo) { const char* dupeKey = markHistoryInfo->GetKind() == HistoryInfo::hkNzb ? markHistoryInfo->GetNzbInfo()->GetDupeKey() : markHistoryInfo->GetKind() == HistoryInfo::hkDup ? markHistoryInfo->GetDupInfo()->GetDupeKey() : nullptr; const char* nzbName = markHistoryInfo->GetKind() == HistoryInfo::hkNzb ? markHistoryInfo->GetNzbInfo()->GetName() : markHistoryInfo->GetKind() == HistoryInfo::hkDup ? markHistoryInfo->GetDupInfo()->GetName() : nullptr; bool changed = false; int index = 0; // traversing in a reverse order to delete items in order they were added to history // (just to produce the log-messages in a more logical order) for (HistoryList::reverse_iterator it = downloadQueue->GetHistory()->rbegin(); it != downloadQueue->GetHistory()->rend(); ) { HistoryInfo* historyInfo = (*it).get(); if (historyInfo->GetKind() == HistoryInfo::hkNzb && historyInfo->GetNzbInfo()->GetDupeMode() != dmForce && historyInfo->GetNzbInfo()->GetDeleteStatus() == NzbInfo::dsDupe && historyInfo != markHistoryInfo && SameNameOrKey(historyInfo->GetNzbInfo()->GetName(), historyInfo->GetNzbInfo()->GetDupeKey(), nzbName, dupeKey)) { g_HistoryCoordinator->HistoryHide(downloadQueue, historyInfo, index); index++; it = downloadQueue->GetHistory()->rbegin() + index; changed = true; } else { it++; index++; } } if (changed) { downloadQueue->HistoryChanged(); downloadQueue->Save(); } } DupeCoordinator::EDupeStatus DupeCoordinator::GetDupeStatus(DownloadQueue* downloadQueue, const char* name, const char* dupeKey) { EDupeStatus statuses = dsNone; // find duplicates in download queue for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { if (SameNameOrKey(name, dupeKey, nzbInfo->GetName(), nzbInfo->GetDupeKey())) { if (nzbInfo->GetSuccessArticles() + nzbInfo->GetFailedArticles() > 0) { statuses = (EDupeStatus)(statuses | dsDownloading); } else { statuses = (EDupeStatus)(statuses | dsQueued); } } } // find duplicates in history for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { if (historyInfo->GetKind() == HistoryInfo::hkNzb && SameNameOrKey(name, dupeKey, historyInfo->GetNzbInfo()->GetName(), historyInfo->GetNzbInfo()->GetDupeKey())) { const char* textStatus = historyInfo->GetNzbInfo()->MakeTextStatus(true); if (!strncasecmp(textStatus, "SUCCESS", 7)) { statuses = (EDupeStatus)(statuses | dsSuccess); } else if (!strncasecmp(textStatus, "FAILURE", 7)) { statuses = (EDupeStatus)(statuses | dsFailure); } else if (!strncasecmp(textStatus, "WARNING", 7)) { statuses = (EDupeStatus)(statuses | dsWarning); } } if (historyInfo->GetKind() == HistoryInfo::hkDup && SameNameOrKey(name, dupeKey, historyInfo->GetDupInfo()->GetName(), historyInfo->GetDupInfo()->GetDupeKey())) { if (historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsSuccess || historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsGood) { statuses = (EDupeStatus)(statuses | dsSuccess); } else if (historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsFailed || historyInfo->GetDupInfo()->GetStatus() == DupInfo::dsBad) { statuses = (EDupeStatus)(statuses | dsFailure); } } } return statuses; } RawNzbList DupeCoordinator::ListHistoryDupes(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) { RawNzbList dupeList; if (nzbInfo->GetDupeMode() == dmForce) { return dupeList; } // find duplicates in history for (HistoryInfo* historyInfo : downloadQueue->GetHistory()) { if (historyInfo->GetKind() == HistoryInfo::hkNzb && historyInfo->GetNzbInfo()->GetDupeMode() != dmForce && SameNameOrKey(historyInfo->GetNzbInfo()->GetName(), historyInfo->GetNzbInfo()->GetDupeKey(), nzbInfo->GetName(), nzbInfo->GetDupeKey())) { dupeList.push_back(historyInfo->GetNzbInfo()); } } return dupeList; } nzbget-19.1/daemon/queue/DupeCoordinator.h0000644000175000017500000000343613130203062020406 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2016 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, see . */ #ifndef DUPECOORDINATOR_H #define DUPECOORDINATOR_H #include "DownloadInfo.h" class DupeCoordinator { public: enum EDupeStatus { dsNone = 0, dsQueued = 1, dsDownloading = 2, dsSuccess = 4, dsWarning = 8, dsFailure = 16 }; void NzbCompleted(DownloadQueue* downloadQueue, NzbInfo* nzbInfo); void NzbFound(DownloadQueue* downloadQueue, NzbInfo* nzbInfo); void HistoryMark(DownloadQueue* downloadQueue, HistoryInfo* historyInfo, NzbInfo::EMarkStatus markStatus); EDupeStatus GetDupeStatus(DownloadQueue* downloadQueue, const char* name, const char* dupeKey); RawNzbList ListHistoryDupes(DownloadQueue* downloadQueue, NzbInfo* nzbInfo); private: void ReturnBestDupe(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, const char* nzbName, const char* dupeKey); void HistoryCleanup(DownloadQueue* downloadQueue, HistoryInfo* markHistoryInfo); bool SameNameOrKey(const char* name1, const char* dupeKey1, const char* name2, const char* dupeKey2); }; extern DupeCoordinator* g_DupeCoordinator; #endif nzbget-19.1/daemon/queue/QueueEditor.h0000644000175000017500000000613313130203062017535 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #ifndef QUEUEEDITOR_H #define QUEUEEDITOR_H #include "DownloadInfo.h" class QueueEditor { public: bool EditEntry(DownloadQueue* downloadQueue, int ID, DownloadQueue::EEditAction action, const char* args); bool EditList(DownloadQueue* downloadQueue, IdList* idList, NameList* nameList, DownloadQueue::EMatchMode matchMode, DownloadQueue::EEditAction action, const char* args); private: class EditItem { public: int m_offset; FileInfo* m_fileInfo; NzbInfo* m_nzbInfo; EditItem(FileInfo* fileInfo, NzbInfo* nzbInfo, int offset) : m_fileInfo(fileInfo), m_nzbInfo(nzbInfo), m_offset(offset) {} }; typedef std::vector ItemList; DownloadQueue* m_downloadQueue; FileInfo* FindFileInfo(int id); bool InternEditList(ItemList* itemList, IdList* idList, DownloadQueue::EEditAction action, const char* args); void PrepareList(ItemList* itemList, IdList* idList, DownloadQueue::EEditAction action, int offset); bool BuildIdListFromNameList(IdList* idList, NameList* nameList, DownloadQueue::EMatchMode matchMode, DownloadQueue::EEditAction action); bool EditGroup(NzbInfo* nzbInfo, DownloadQueue::EEditAction action, const char* args); void PauseParsInGroups(ItemList* itemList, bool extraParsOnly); void PausePars(RawFileList* fileList, bool extraParsOnly); void SetNzbPriority(NzbInfo* nzbInfo, const char* priority); void SetNzbCategory(NzbInfo* nzbInfo, const char* category, bool applyParams); void SetNzbName(NzbInfo* nzbInfo, const char* name); bool MergeGroups(ItemList* itemList); bool SortGroups(ItemList* itemList, const char* sort); void AlignGroups(ItemList* itemList); bool MoveGroupsTo(ItemList* itemList, IdList* idList, bool before, const char* args); bool SplitGroup(ItemList* itemList, const char* name); bool DeleteUrl(NzbInfo* nzbInfo, DownloadQueue::EEditAction action); void ReorderFiles(ItemList* itemList); void SetNzbParameter(NzbInfo* nzbInfo, const char* paramString); void SetNzbDupeParam(NzbInfo* nzbInfo, DownloadQueue::EEditAction action, const char* args); void PauseUnpauseEntry(FileInfo* fileInfo, bool pause); void DeleteEntry(FileInfo* fileInfo); void MoveEntry(FileInfo* fileInfo, int offset); void MoveGroup(NzbInfo* nzbInfo, int offset); void SortGroupFiles(NzbInfo* nzbInfo); bool ItemListContainsItem(ItemList* itemList, int id); friend class GroupSorter; }; #endif nzbget-19.1/daemon/queue/Scanner.h0000644000175000017500000001007013130203062016666 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2016 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, see . * */ #ifndef SCANNER_H #define SCANNER_H #include "NString.h" #include "DownloadInfo.h" #include "Thread.h" #include "Service.h" class Scanner : public Service { public: enum EAddStatus { asSkipped, asSuccess, asFailed }; void InitOptions(); void ScanNzbDir(bool syncMode); EAddStatus AddExternalFile(const char* nzbName, const char* category, int priority, const char* dupeKey, int dupeScore, EDupeMode dupeMode, NzbParameterList* parameters, bool addTop, bool addPaused, NzbInfo* urlInfo, const char* fileName, const char* buffer, int bufSize, int* nzbId); void InitPPParameters(const char* category, NzbParameterList* parameters, bool reset); protected: virtual int ServiceInterval() { return 200; } virtual void ServiceWork(); private: class FileData { public: FileData(const char* filename, int64 size, time_t lastChange) : m_filename(filename), m_size(size), m_lastChange(lastChange) {} const char* GetFilename() { return m_filename; } int64 GetSize() { return m_size; } void SetSize(int64 size) { m_size = size; } time_t GetLastChange() { return m_lastChange; } void SetLastChange(time_t lastChange) { m_lastChange = lastChange; } private: CString m_filename; int64 m_size; time_t m_lastChange; }; typedef std::deque FileList; class QueueData { public: QueueData(const char* filename, const char* nzbName, const char* category, int priority, const char* dupeKey, int dupeScore, EDupeMode dupeMode, NzbParameterList* parameters, bool addTop, bool addPaused, NzbInfo* urlInfo, EAddStatus* addStatus, int* nzbId); const char* GetFilename() { return m_filename; } const char* GetNzbName() { return m_nzbName; } const char* GetCategory() { return m_category; } int GetPriority() { return m_priority; } const char* GetDupeKey() { return m_dupeKey; } int GetDupeScore() { return m_dupeScore; } EDupeMode GetDupeMode() { return m_dupeMode; } NzbParameterList* GetParameters() { return &m_parameters; } bool GetAddTop() { return m_addTop; } bool GetAddPaused() { return m_addPaused; } NzbInfo* GetUrlInfo() { return m_urlInfo; } void SetAddStatus(EAddStatus addStatus); void SetNzbId(int nzbId); private: CString m_filename; CString m_nzbName; CString m_category; int m_priority; CString m_dupeKey; int m_dupeScore; EDupeMode m_dupeMode; NzbParameterList m_parameters; bool m_addTop; bool m_addPaused; NzbInfo* m_urlInfo; EAddStatus* m_addStatus; int* m_nzbId; }; typedef std::deque QueueList; bool m_requestedNzbDirScan = false; int m_nzbDirInterval = 0; bool m_scanScript = false; int m_pass = 0; FileList m_fileList; QueueList m_queueList; bool m_scanning = false; Mutex m_scanMutex; static int m_idGen; void CheckIncomingNzbs(const char* directory, const char* category, bool checkStat); bool AddFileToQueue(const char* filename, const char* nzbName, const char* category, int priority, const char* dupeKey, int dupeScore, EDupeMode dupeMode, NzbParameterList* parameters, bool addTop, bool addPaused, NzbInfo* urlInfo, int* nzbId); void ProcessIncomingFile(const char* directory, const char* baseFilename, const char* fullFilename, const char* category); bool CanProcessFile(const char* fullFilename, bool checkStat); void DropOldFiles(); }; extern Scanner* g_Scanner; #endif nzbget-19.1/daemon/queue/DownloadInfo.h0000644000175000017500000010645013130203062017670 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2017 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, see . */ #ifndef DOWNLOADINFO_H #define DOWNLOADINFO_H #include "NString.h" #include "Container.h" #include "Observer.h" #include "Log.h" #include "Thread.h" class NzbInfo; class DownloadQueue; class PostInfo; class ServerStat { public: ServerStat(int serverId) : m_serverId(serverId) {} int GetServerId() { return m_serverId; } int GetSuccessArticles() { return m_successArticles; } void SetSuccessArticles(int successArticles) { m_successArticles = successArticles; } int GetFailedArticles() { return m_failedArticles; } void SetFailedArticles(int failedArticles) { m_failedArticles = failedArticles; } private: int m_serverId; int m_successArticles = 0; int m_failedArticles = 0; }; typedef std::vector ServerStatListBase; class ServerStatList : public ServerStatListBase { public: enum EStatOperation { soSet, soAdd, soSubtract }; void StatOp(int serverId, int successArticles, int failedArticles, EStatOperation statOperation); void ListOp(ServerStatList* serverStats, EStatOperation statOperation); }; class SegmentData { public: virtual char* GetData() = 0; virtual ~SegmentData() {} }; class ArticleInfo { public: enum EStatus { aiUndefined, aiRunning, aiFinished, aiFailed }; void SetPartNumber(int s) { m_partNumber = s; } int GetPartNumber() { return m_partNumber; } const char* GetMessageId() { return m_messageId; } void SetMessageId(const char* messageId) { m_messageId = messageId; } void SetSize(int size) { m_size = size; } int GetSize() { return m_size; } void AttachSegment(std::unique_ptr content, int64 offset, int size); void DiscardSegment(); const char* GetSegmentContent() { return m_segmentContent ? m_segmentContent->GetData() : nullptr; } void SetSegmentOffset(int64 segmentOffset) { m_segmentOffset = segmentOffset; } int64 GetSegmentOffset() { return m_segmentOffset; } void SetSegmentSize(int segmentSize) { m_segmentSize = segmentSize; } int GetSegmentSize() { return m_segmentSize; } EStatus GetStatus() { return m_status; } void SetStatus(EStatus Status) { m_status = Status; } const char* GetResultFilename() { return m_resultFilename; } void SetResultFilename(const char* resultFilename) { m_resultFilename = resultFilename; } uint32 GetCrc() { return m_crc; } void SetCrc(uint32 crc) { m_crc = crc; } private: int m_partNumber; CString m_messageId; int m_size = 0; std::unique_ptr m_segmentContent; int64 m_segmentOffset = 0; int m_segmentSize = 0; EStatus m_status = aiUndefined; CString m_resultFilename; uint32 m_crc = 0; }; typedef std::vector> ArticleList; class FileInfo { public: enum EPartialState { psNone, psPartial, psCompleted }; typedef std::vector Groups; FileInfo(int id = 0) : m_id(id ? id : ++m_idGen) {} int GetId() { return m_id; } void SetId(int id); static void ResetGenId(bool max); NzbInfo* GetNzbInfo() { return m_nzbInfo; } void SetNzbInfo(NzbInfo* nzbInfo) { m_nzbInfo = nzbInfo; } ArticleList* GetArticles() { return &m_articles; } Groups* GetGroups() { return &m_groups; } const char* GetSubject() { return m_subject; } void SetSubject(const char* subject) { m_subject = subject; } const char* GetFilename() { return m_filename; } void SetFilename(const char* filename) { m_filename = filename; } void MakeValidFilename(); bool GetFilenameConfirmed() { return m_filenameConfirmed; } void SetFilenameConfirmed(bool filenameConfirmed) { m_filenameConfirmed = filenameConfirmed; } void SetSize(int64 size) { m_size = size; m_remainingSize = size; } int64 GetSize() { return m_size; } int64 GetRemainingSize() { return m_remainingSize; } void SetRemainingSize(int64 remainingSize) { m_remainingSize = remainingSize; } int64 GetMissedSize() { return m_missedSize; } void SetMissedSize(int64 missedSize) { m_missedSize = missedSize; } int64 GetSuccessSize() { return m_successSize; } void SetSuccessSize(int64 successSize) { m_successSize = successSize; } int64 GetFailedSize() { return m_failedSize; } void SetFailedSize(int64 failedSize) { m_failedSize = failedSize; } int GetTotalArticles() { return m_totalArticles; } void SetTotalArticles(int totalArticles) { m_totalArticles = totalArticles; } int GetMissedArticles() { return m_missedArticles; } void SetMissedArticles(int missedArticles) { m_missedArticles = missedArticles; } int GetFailedArticles() { return m_failedArticles; } void SetFailedArticles(int failedArticles) { m_failedArticles = failedArticles; } int GetSuccessArticles() { return m_successArticles; } void SetSuccessArticles(int successArticles) { m_successArticles = successArticles; } time_t GetTime() { return m_time; } void SetTime(time_t time) { m_time = time; } bool GetPaused() { return m_paused; } void SetPaused(bool paused); bool GetDeleted() { return m_deleted; } void SetDeleted(bool Deleted) { m_deleted = Deleted; } int GetCompletedArticles() { return m_completedArticles; } void SetCompletedArticles(int completedArticles) { m_completedArticles = completedArticles; } bool GetParFile() { return m_parFile; } void SetParFile(bool parFile) { m_parFile = parFile; } Guard GuardOutputFile() { return Guard(m_outputFileMutex); } const char* GetOutputFilename() { return m_outputFilename; } void SetOutputFilename(const char* outputFilename) { m_outputFilename = outputFilename; } bool GetOutputInitialized() { return m_outputInitialized; } void SetOutputInitialized(bool outputInitialized) { m_outputInitialized = outputInitialized; } bool GetExtraPriority() { return m_extraPriority; } void SetExtraPriority(bool extraPriority) { m_extraPriority = extraPriority; } int GetActiveDownloads() { return m_activeDownloads; } void SetActiveDownloads(int activeDownloads); bool GetDupeDeleted() { return m_dupeDeleted; } void SetDupeDeleted(bool dupeDeleted) { m_dupeDeleted = dupeDeleted; } int GetCachedArticles() { return m_cachedArticles; } void SetCachedArticles(int cachedArticles) { m_cachedArticles = cachedArticles; } bool GetPartialChanged() { return m_partialChanged; } void SetPartialChanged(bool partialChanged) { m_partialChanged = partialChanged; } bool GetForceDirectWrite() { return m_forceDirectWrite; } void SetForceDirectWrite(bool forceDirectWrite) { m_forceDirectWrite = forceDirectWrite; } EPartialState GetPartialState() { return m_partialState; } void SetPartialState(EPartialState partialState) { m_partialState = partialState; } uint32 GetCrc() { return m_crc; } void SetCrc(uint32 crc) { m_crc = crc; } const char* GetHash16k() { return m_hash16k; } void SetHash16k(const char* hash16k) { m_hash16k = hash16k; } const char* GetParSetId() { return m_parSetId; } void SetParSetId(const char* parSetId) { m_parSetId = parSetId; } ServerStatList* GetServerStats() { return &m_serverStats; } private: int m_id; NzbInfo* m_nzbInfo = nullptr; ArticleList m_articles; Groups m_groups; ServerStatList m_serverStats; CString m_subject; CString m_filename; int64 m_size = 0; int64 m_remainingSize = 0; int64 m_successSize = 0; int64 m_failedSize = 0; int64 m_missedSize = 0; int m_totalArticles = 0; int m_missedArticles = 0; int m_failedArticles = 0; int m_successArticles = 0; time_t m_time = 0; bool m_paused = false; bool m_deleted = false; bool m_filenameConfirmed = false; bool m_parFile = false; int m_completedArticles = 0; bool m_outputInitialized = false; CString m_outputFilename; std::unique_ptr m_outputFileMutex; bool m_extraPriority = false; int m_activeDownloads = 0; bool m_dupeDeleted = false; int m_cachedArticles = 0; bool m_partialChanged = false; bool m_forceDirectWrite = false; EPartialState m_partialState = psNone; uint32 m_crc = 0; CString m_hash16k; CString m_parSetId; static int m_idGen; static int m_idMax; friend class CompletedFile; }; typedef UniqueDeque FileList; typedef std::vector RawFileList; class CompletedFile { public: enum EStatus { cfNone, cfSuccess, cfPartial, cfFailure }; CompletedFile(int id, const char* filename, EStatus status, uint32 crc, bool parFile, const char* hash16k, const char* parSetId); int GetId() { return m_id; } void SetFilename(const char* filename) { m_filename = filename; } const char* GetFilename() { return m_filename; } bool GetParFile() { return m_parFile; } EStatus GetStatus() { return m_status; } uint32 GetCrc() { return m_crc; } const char* GetHash16k() { return m_hash16k; } void SetHash16k(const char* hash16k) { m_hash16k = hash16k; } const char* GetParSetId() { return m_parSetId; } void SetParSetId(const char* parSetId) { m_parSetId = parSetId; } private: int m_id; CString m_filename; EStatus m_status; uint32 m_crc; bool m_parFile; CString m_hash16k; CString m_parSetId; }; typedef std::deque CompletedFileList; class NzbParameter { public: NzbParameter(const char* name, const char* value) : m_name(name), m_value(value) {} const char* GetName() { return m_name; } const char* GetValue() { return m_value; } private: CString m_name; CString m_value; void SetValue(const char* value) { m_value = value; } friend class NzbParameterList; }; typedef std::deque NzbParameterListBase; class NzbParameterList : public NzbParameterListBase { public: void SetParameter(const char* name, const char* value); NzbParameter* Find(const char* name, bool caseSensitive); void CopyFrom(NzbParameterList* sourceParameters); }; class ScriptStatus { public: enum EStatus { srNone, srFailure, srSuccess }; ScriptStatus(const char* name, EStatus status) : m_name(name), m_status(status) {} const char* GetName() { return m_name; } EStatus GetStatus() { return m_status; } private: CString m_name; EStatus m_status; friend class ScriptStatusList; }; typedef std::deque ScriptStatusListBase; class ScriptStatusList : public ScriptStatusListBase { public: ScriptStatus::EStatus CalcTotalStatus(); }; enum EDupeMode { dmScore, dmAll, dmForce }; class NzbInfo { public: enum EDirectRenameStatus { tsNone, tsRunning, tsFailure, tsSuccess }; enum EPostRenameStatus { rsNone, rsSkipped, rsNothing, rsSuccess }; enum EParStatus { psNone, psSkipped, psFailure, psSuccess, psRepairPossible, psManual }; enum EDirectUnpackStatus { nsNone, nsRunning, nsFailure, nsSuccess }; enum EPostUnpackStatus { usNone, usSkipped, usFailure, usSuccess, usSpace, usPassword }; enum ECleanupStatus { csNone, csFailure, csSuccess }; enum EMoveStatus { msNone, msFailure, msSuccess }; enum EDeleteStatus { dsNone, dsManual, dsHealth, dsDupe, dsBad, dsGood, dsCopy, dsScan }; enum EMarkStatus { ksNone, ksBad, ksGood, ksSuccess }; enum EUrlStatus { lsNone, lsRunning, lsFinished, lsFailed, lsRetry, lsScanSkipped, lsScanFailed }; enum EKind { nkNzb, nkUrl }; int GetId() { return m_id; } void SetId(int id); static void ResetGenId(bool max); static int GenerateId(); EKind GetKind() { return m_kind; } void SetKind(EKind kind) { m_kind = kind; } const char* GetUrl() { return m_url; } void SetUrl(const char* url); const char* GetFilename() { return m_filename; } void SetFilename(const char* filename); static CString MakeNiceNzbName(const char* nzbFilename, bool removeExt); static CString MakeNiceUrlName(const char* url, const char* nzbFilename); const char* GetDestDir() { return m_destDir; } void SetDestDir(const char* destDir) { m_destDir = destDir; } const char* GetFinalDir() { return m_finalDir; } void SetFinalDir(const char* finalDir) { m_finalDir = finalDir; } const char* GetCategory() { return m_category; } void SetCategory(const char* category) { m_category = category; } const char* GetName() { return m_name; } void SetName(const char* name) { m_name = name; } int GetFileCount() { return m_fileCount; } void SetFileCount(int fileCount) { m_fileCount = fileCount; } int GetParkedFileCount() { return m_parkedFileCount; } void SetParkedFileCount(int parkedFileCount) { m_parkedFileCount = parkedFileCount; } int64 GetSize() { return m_size; } void SetSize(int64 size) { m_size = size; } int64 GetRemainingSize() { return m_remainingSize; } void SetRemainingSize(int64 remainingSize) { m_remainingSize = remainingSize; } int64 GetPausedSize() { return m_pausedSize; } void SetPausedSize(int64 pausedSize) { m_pausedSize = pausedSize; } int GetPausedFileCount() { return m_pausedFileCount; } void SetPausedFileCount(int pausedFileCount) { m_pausedFileCount = pausedFileCount; } int GetRemainingParCount() { return m_remainingParCount; } void SetRemainingParCount(int remainingParCount) { m_remainingParCount = remainingParCount; } int GetActiveDownloads() { return m_activeDownloads; } void SetActiveDownloads(int activeDownloads); int64 GetSuccessSize() { return m_successSize; } void SetSuccessSize(int64 successSize) { m_successSize = successSize; } int64 GetFailedSize() { return m_failedSize; } void SetFailedSize(int64 failedSize) { m_failedSize = failedSize; } int64 GetCurrentSuccessSize() { return m_currentSuccessSize; } void SetCurrentSuccessSize(int64 currentSuccessSize) { m_currentSuccessSize = currentSuccessSize; } int64 GetCurrentFailedSize() { return m_currentFailedSize; } void SetCurrentFailedSize(int64 currentFailedSize) { m_currentFailedSize = currentFailedSize; } int64 GetParSize() { return m_parSize; } void SetParSize(int64 parSize) { m_parSize = parSize; } int64 GetParSuccessSize() { return m_parSuccessSize; } void SetParSuccessSize(int64 parSuccessSize) { m_parSuccessSize = parSuccessSize; } int64 GetParFailedSize() { return m_parFailedSize; } void SetParFailedSize(int64 parFailedSize) { m_parFailedSize = parFailedSize; } int64 GetParCurrentSuccessSize() { return m_parCurrentSuccessSize; } void SetParCurrentSuccessSize(int64 parCurrentSuccessSize) { m_parCurrentSuccessSize = parCurrentSuccessSize; } int64 GetParCurrentFailedSize() { return m_parCurrentFailedSize; } void SetParCurrentFailedSize(int64 parCurrentFailedSize) { m_parCurrentFailedSize = parCurrentFailedSize; } int GetTotalArticles() { return m_totalArticles; } void SetTotalArticles(int totalArticles) { m_totalArticles = totalArticles; } int GetSuccessArticles() { return m_successArticles; } void SetSuccessArticles(int successArticles) { m_successArticles = successArticles; } int GetFailedArticles() { return m_failedArticles; } void SetFailedArticles(int failedArticles) { m_failedArticles = failedArticles; } int GetCurrentSuccessArticles() { return m_currentSuccessArticles; } void SetCurrentSuccessArticles(int currentSuccessArticles) { m_currentSuccessArticles = currentSuccessArticles; } int GetCurrentFailedArticles() { return m_currentFailedArticles; } void SetCurrentFailedArticles(int currentFailedArticles) { m_currentFailedArticles = currentFailedArticles; } int GetPriority() { return m_priority; } void SetPriority(int priority) { m_priority = priority; } bool GetForcePriority() { return m_priority >= FORCE_PRIORITY; } time_t GetMinTime() { return m_minTime; } void SetMinTime(time_t minTime) { m_minTime = minTime; } time_t GetMaxTime() { return m_maxTime; } void SetMaxTime(time_t maxTime) { m_maxTime = maxTime; } void BuildDestDirName(); CString BuildFinalDirName(); CompletedFileList* GetCompletedFiles() { return &m_completedFiles; } void SetDirectRenameStatus(EDirectRenameStatus renameStatus) { m_directRenameStatus = renameStatus; } EDirectRenameStatus GetDirectRenameStatus() { return m_directRenameStatus; } EPostRenameStatus GetParRenameStatus() { return m_parRenameStatus; } void SetParRenameStatus(EPostRenameStatus renameStatus) { m_parRenameStatus = renameStatus; } EPostRenameStatus GetRarRenameStatus() { return m_rarRenameStatus; } void SetRarRenameStatus(EPostRenameStatus renameStatus) { m_rarRenameStatus = renameStatus; } EParStatus GetParStatus() { return m_parStatus; } void SetParStatus(EParStatus parStatus) { m_parStatus = parStatus; } EDirectUnpackStatus GetDirectUnpackStatus() { return m_directUnpackStatus; } void SetDirectUnpackStatus(EDirectUnpackStatus directUnpackStatus) { m_directUnpackStatus = directUnpackStatus; } EPostUnpackStatus GetUnpackStatus() { return m_unpackStatus; } void SetUnpackStatus(EPostUnpackStatus unpackStatus) { m_unpackStatus = unpackStatus; } ECleanupStatus GetCleanupStatus() { return m_cleanupStatus; } void SetCleanupStatus(ECleanupStatus cleanupStatus) { m_cleanupStatus = cleanupStatus; } EMoveStatus GetMoveStatus() { return m_moveStatus; } void SetMoveStatus(EMoveStatus moveStatus) { m_moveStatus = moveStatus; } EDeleteStatus GetDeleteStatus() { return m_deleteStatus; } void SetDeleteStatus(EDeleteStatus deleteStatus) { m_deleteStatus = deleteStatus; } EMarkStatus GetMarkStatus() { return m_markStatus; } void SetMarkStatus(EMarkStatus markStatus) { m_markStatus = markStatus; } EUrlStatus GetUrlStatus() { return m_urlStatus; } int GetExtraParBlocks() { return m_extraParBlocks; } void SetExtraParBlocks(int extraParBlocks) { m_extraParBlocks = extraParBlocks; } void SetUrlStatus(EUrlStatus urlStatus) { m_urlStatus = urlStatus; } const char* GetQueuedFilename() { return m_queuedFilename; } void SetQueuedFilename(const char* queuedFilename) { m_queuedFilename = queuedFilename; } bool GetDeleting() { return m_deleting; } void SetDeleting(bool deleting) { m_deleting = deleting; } bool GetParking() { return m_parking; } void SetParking(bool parking) { m_parking = parking; } bool GetDeletePaused() { return m_deletePaused; } void SetDeletePaused(bool deletePaused) { m_deletePaused = deletePaused; } bool GetManyDupeFiles() { return m_manyDupeFiles; } void SetManyDupeFiles(bool manyDupeFiles) { m_manyDupeFiles = manyDupeFiles; } bool GetAvoidHistory() { return m_avoidHistory; } void SetAvoidHistory(bool avoidHistory) { m_avoidHistory = avoidHistory; } bool GetHealthPaused() { return m_healthPaused; } void SetHealthPaused(bool healthPaused) { m_healthPaused = healthPaused; } bool GetCleanupDisk() { return m_cleanupDisk; } void SetCleanupDisk(bool cleanupDisk) { m_cleanupDisk = cleanupDisk; } bool GetUnpackCleanedUpDisk() { return m_unpackCleanedUpDisk; } void SetUnpackCleanedUpDisk(bool unpackCleanedUpDisk) { m_unpackCleanedUpDisk = unpackCleanedUpDisk; } bool GetAddUrlPaused() { return m_addUrlPaused; } void SetAddUrlPaused(bool addUrlPaused) { m_addUrlPaused = addUrlPaused; } FileList* GetFileList() { return &m_fileList; } NzbParameterList* GetParameters() { return &m_ppParameters; } ScriptStatusList* GetScriptStatuses() { return &m_scriptStatuses; } ServerStatList* GetServerStats() { return &m_serverStats; } ServerStatList* GetCurrentServerStats() { return &m_currentServerStats; } int CalcHealth(); int CalcCriticalHealth(bool allowEstimation); const char* GetDupeKey() { return m_dupeKey; } void SetDupeKey(const char* dupeKey) { m_dupeKey = dupeKey ? dupeKey : ""; } int GetDupeScore() { return m_dupeScore; } void SetDupeScore(int dupeScore) { m_dupeScore = dupeScore; } EDupeMode GetDupeMode() { return m_dupeMode; } void SetDupeMode(EDupeMode dupeMode) { m_dupeMode = dupeMode; } uint32 GetFullContentHash() { return m_fullContentHash; } void SetFullContentHash(uint32 fullContentHash) { m_fullContentHash = fullContentHash; } uint32 GetFilteredContentHash() { return m_filteredContentHash; } void SetFilteredContentHash(uint32 filteredContentHash) { m_filteredContentHash = filteredContentHash; } int64 GetDownloadedSize() { return m_downloadedSize; } void SetDownloadedSize(int64 downloadedSize) { m_downloadedSize = downloadedSize; } int GetDownloadSec() { return m_downloadSec; } void SetDownloadSec(int downloadSec) { m_downloadSec = downloadSec; } int GetPostTotalSec() { return m_postTotalSec; } void SetPostTotalSec(int postTotalSec) { m_postTotalSec = postTotalSec; } int GetParSec() { return m_parSec; } void SetParSec(int parSec) { m_parSec = parSec; } int GetRepairSec() { return m_repairSec; } void SetRepairSec(int repairSec) { m_repairSec = repairSec; } int GetUnpackSec() { return m_unpackSec; } void SetUnpackSec(int unpackSec) { m_unpackSec = unpackSec; } time_t GetDownloadStartTime() { return m_downloadStartTime; } void SetDownloadStartTime(time_t downloadStartTime) { m_downloadStartTime = downloadStartTime; } bool GetChanged() { return m_changed; } void SetChanged(bool changed) { m_changed = changed; } void SetReprocess(bool reprocess) { m_reprocess = reprocess; } bool GetReprocess() { return m_reprocess; } time_t GetQueueScriptTime() { return m_queueScriptTime; } void SetQueueScriptTime(time_t queueScriptTime) { m_queueScriptTime = queueScriptTime; } void SetParFull(bool parFull) { m_parFull = parFull; } bool GetParFull() { return m_parFull; } int GetFeedId() { return m_feedId; } void SetFeedId(int feedId) { m_feedId = feedId; } void MoveFileList(NzbInfo* srcNzbInfo); void UpdateMinMaxTime(); PostInfo* GetPostInfo() { return m_postInfo.get(); } void EnterPostProcess(); void LeavePostProcess(); bool IsDupeSuccess(); const char* MakeTextStatus(bool ignoreScriptStatus); void AddMessage(Message::EKind kind, const char* text); void PrintMessage(Message::EKind kind, const char* format, ...) PRINTF_SYNTAX(3); int GetMessageCount() { return m_messageCount; } void SetMessageCount(int messageCount) { m_messageCount = messageCount; } int GetCachedMessageCount() { return m_cachedMessageCount; } GuardedMessageList GuardCachedMessages() { return GuardedMessageList(&m_messages, &m_logMutex); } bool GetAllFirst() { return m_allFirst; } void SetAllFirst(bool allFirst) { m_allFirst = allFirst; } bool GetWaitingPar() { return m_waitingPar; } void SetWaitingPar(bool waitingPar) { m_waitingPar = waitingPar; } bool GetLoadingPar() { return m_loadingPar; } void SetLoadingPar(bool loadingPar) { m_loadingPar = loadingPar; } Thread* GetUnpackThread() { return m_unpackThread; } void SetUnpackThread(Thread* unpackThread) { m_unpackThread = unpackThread; } void UpdateCurrentStats(); void UpdateCompletedStats(FileInfo* fileInfo); void UpdateDeletedStats(FileInfo* fileInfo); bool IsDownloadCompleted(bool ignorePausedPars); static const int FORCE_PRIORITY = 900; private: int m_id = ++m_idGen; EKind m_kind = nkNzb; CString m_url = ""; CString m_filename = ""; CString m_name; CString m_destDir = ""; CString m_finalDir = ""; CString m_category = ""; int m_fileCount = 0; int m_parkedFileCount = 0; int64 m_size = 0; int64 m_remainingSize = 0; int m_pausedFileCount = 0; int64 m_pausedSize = 0; int m_remainingParCount = 0; int m_activeDownloads = 0; int64 m_successSize = 0; int64 m_failedSize = 0; int64 m_currentSuccessSize = 0; int64 m_currentFailedSize = 0; int64 m_parSize = 0; int64 m_parSuccessSize = 0; int64 m_parFailedSize = 0; int64 m_parCurrentSuccessSize = 0; int64 m_parCurrentFailedSize = 0; int m_totalArticles = 0; int m_successArticles = 0; int m_failedArticles = 0; int m_currentSuccessArticles = 0; int m_currentFailedArticles = 0; time_t m_minTime = 0; time_t m_maxTime = 0; int m_priority = 0; CompletedFileList m_completedFiles; EDirectRenameStatus m_directRenameStatus = tsNone; EPostRenameStatus m_parRenameStatus = rsNone; EPostRenameStatus m_rarRenameStatus = rsNone; EParStatus m_parStatus = psNone; EDirectUnpackStatus m_directUnpackStatus = nsNone; EPostUnpackStatus m_unpackStatus = usNone; ECleanupStatus m_cleanupStatus = csNone; EMoveStatus m_moveStatus = msNone; EDeleteStatus m_deleteStatus = dsNone; EMarkStatus m_markStatus = ksNone; EUrlStatus m_urlStatus = lsNone; int m_extraParBlocks = 0; bool m_addUrlPaused = false; bool m_deletePaused = false; bool m_manyDupeFiles = false; CString m_queuedFilename = ""; bool m_deleting = false; bool m_parking = false; bool m_avoidHistory = false; bool m_healthPaused = false; bool m_parManual = false; bool m_cleanupDisk = false; bool m_unpackCleanedUpDisk = false; CString m_dupeKey = ""; int m_dupeScore = 0; EDupeMode m_dupeMode = dmScore; uint32 m_fullContentHash = 0; uint32 m_filteredContentHash = 0; FileList m_fileList; NzbParameterList m_ppParameters; ScriptStatusList m_scriptStatuses; ServerStatList m_serverStats; ServerStatList m_currentServerStats; Mutex m_logMutex; MessageList m_messages; int m_idMessageGen = 0; std::unique_ptr m_postInfo; int64 m_downloadedSize = 0; time_t m_downloadStartTime = 0; int m_downloadStartSec = 0; int m_downloadSec = 0; int m_postTotalSec = 0; int m_parSec = 0; int m_repairSec = 0; int m_unpackSec = 0; bool m_reprocess = false; bool m_changed = false; time_t m_queueScriptTime = 0; bool m_parFull = false; int m_messageCount = 0; int m_cachedMessageCount = 0; int m_feedId = 0; bool m_allFirst = false; bool m_waitingPar = false; bool m_loadingPar = false; Thread* m_unpackThread = nullptr; static int m_idGen; static int m_idMax; void ClearMessages(); friend class DupInfo; }; typedef UniqueDeque NzbList; typedef std::vector RawNzbList; class PostInfo { public: enum EStage { ptQueued, ptLoadingPars, ptVerifyingSources, ptRepairing, ptVerifyingRepaired, ptParRenaming, ptRarRenaming, ptUnpacking, ptCleaningUp, ptMoving, ptExecutingScript, ptFinished }; typedef std::vector ParredFiles; typedef std::vector ExtractedArchives; NzbInfo* GetNzbInfo() { return m_nzbInfo; } void SetNzbInfo(NzbInfo* nzbInfo) { m_nzbInfo = nzbInfo; } EStage GetStage() { return m_stage; } void SetStage(EStage stage) { m_stage = stage; } void SetProgressLabel(const char* progressLabel) { m_progressLabel = progressLabel; } const char* GetProgressLabel() { return m_progressLabel; } int GetFileProgress() { return m_fileProgress; } void SetFileProgress(int fileProgress) { m_fileProgress = fileProgress; } int GetStageProgress() { return m_stageProgress; } void SetStageProgress(int stageProgress) { m_stageProgress = stageProgress; } time_t GetStartTime() { return m_startTime; } void SetStartTime(time_t startTime) { m_startTime = startTime; } time_t GetStageTime() { return m_stageTime; } void SetStageTime(time_t stageTime) { m_stageTime = stageTime; } bool GetWorking() { return m_working; } void SetWorking(bool working) { m_working = working; } bool GetDeleted() { return m_deleted; } void SetDeleted(bool deleted) { m_deleted = deleted; } bool GetRequestParCheck() { return m_requestParCheck; } void SetRequestParCheck(bool requestParCheck) { m_requestParCheck = requestParCheck; } bool GetForceParFull() { return m_forceParFull; } void SetForceParFull(bool forceParFull) { m_forceParFull = forceParFull; } bool GetForceRepair() { return m_forceRepair; } void SetForceRepair(bool forceRepair) { m_forceRepair = forceRepair; } bool GetParRepaired() { return m_parRepaired; } void SetParRepaired(bool parRepaired) { m_parRepaired = parRepaired; } bool GetUnpackTried() { return m_unpackTried; } void SetUnpackTried(bool unpackTried) { m_unpackTried = unpackTried; } bool GetPassListTried() { return m_passListTried; } void SetPassListTried(bool passListTried) { m_passListTried = passListTried; } int GetLastUnpackStatus() { return m_lastUnpackStatus; } void SetLastUnpackStatus(int unpackStatus) { m_lastUnpackStatus = unpackStatus; } bool GetNeedParCheck() { return m_needParCheck; } void SetNeedParCheck(bool needParCheck) { m_needParCheck = needParCheck; } Thread* GetPostThread() { return m_postThread; } void SetPostThread(Thread* postThread) { m_postThread = postThread; } ParredFiles* GetParredFiles() { return &m_parredFiles; } ExtractedArchives* GetExtractedArchives() { return &m_extractedArchives; } private: NzbInfo* m_nzbInfo = nullptr; bool m_working = false; bool m_deleted = false; bool m_requestParCheck = false; bool m_forceParFull = false; bool m_forceRepair = false; bool m_parRepaired = false; bool m_unpackTried = false; bool m_passListTried = false; int m_lastUnpackStatus = 0; bool m_needParCheck = false; EStage m_stage = ptQueued; CString m_progressLabel = ""; int m_fileProgress = 0; int m_stageProgress = 0; time_t m_startTime = 0; time_t m_stageTime = 0; Thread* m_postThread = nullptr; ParredFiles m_parredFiles; ExtractedArchives m_extractedArchives; }; typedef std::vector IdList; typedef std::vector NameList; class DupInfo { public: enum EStatus { dsUndefined, dsSuccess, dsFailed, dsDeleted, dsDupe, dsBad, dsGood }; int GetId() { return m_id; } void SetId(int id); const char* GetName() { return m_name; } void SetName(const char* name) { m_name = name; } const char* GetDupeKey() { return m_dupeKey; } void SetDupeKey(const char* dupeKey) { m_dupeKey = dupeKey; } int GetDupeScore() { return m_dupeScore; } void SetDupeScore(int dupeScore) { m_dupeScore = dupeScore; } EDupeMode GetDupeMode() { return m_dupeMode; } void SetDupeMode(EDupeMode dupeMode) { m_dupeMode = dupeMode; } int64 GetSize() { return m_size; } void SetSize(int64 size) { m_size = size; } uint32 GetFullContentHash() { return m_fullContentHash; } void SetFullContentHash(uint32 fullContentHash) { m_fullContentHash = fullContentHash; } uint32 GetFilteredContentHash() { return m_filteredContentHash; } void SetFilteredContentHash(uint32 filteredContentHash) { m_filteredContentHash = filteredContentHash; } EStatus GetStatus() { return m_status; } void SetStatus(EStatus Status) { m_status = Status; } private: int m_id = 0; CString m_name; CString m_dupeKey; int m_dupeScore = 0; EDupeMode m_dupeMode = dmScore; int64 m_size = 0; uint32 m_fullContentHash = 0; uint32 m_filteredContentHash = 0; EStatus m_status = dsUndefined; }; class HistoryInfo { public: enum EKind { hkUnknown, hkNzb, hkUrl, hkDup }; HistoryInfo(std::unique_ptr nzbInfo) : m_info(nzbInfo.release()), m_kind(nzbInfo->GetKind() == NzbInfo::nkNzb ? hkNzb : hkUrl) {} HistoryInfo(std::unique_ptr dupInfo) : m_info(dupInfo.release()), m_kind(hkDup) {} ~HistoryInfo(); EKind GetKind() { return m_kind; } int GetId(); NzbInfo* GetNzbInfo() { return (NzbInfo*)m_info; } DupInfo* GetDupInfo() { return (DupInfo*)m_info; } void DiscardNzbInfo() { m_info = nullptr; } time_t GetTime() { return m_time; } void SetTime(time_t time) { m_time = time; } const char* GetName(); private: EKind m_kind; void* m_info; time_t m_time = 0; }; typedef UniqueDeque HistoryList; typedef GuardedPtr GuardedDownloadQueue; class DownloadQueue : public Subject { public: enum EAspectAction { eaNzbFound, eaNzbAdded, eaNzbDeleted, eaNzbNamed, eaFileCompleted, eaFileDeleted, eaUrlCompleted }; struct Aspect { EAspectAction action; DownloadQueue* downloadQueue; NzbInfo* nzbInfo; FileInfo* fileInfo; }; enum EEditAction { eaFileMoveOffset = 1, // move files to m_iOffset relative to the current position in download-queue eaFileMoveTop, // move files to the top of download-queue eaFileMoveBottom, // move files to the bottom of download-queue eaFilePause, // pause files eaFileResume, // resume (unpause) files eaFileDelete, // delete files eaFilePauseAllPars, // pause only (all) pars (does not affect other files) eaFilePauseExtraPars, // pause (almost all) pars, except main par-file (does not affect other files) eaFileReorder, // set file order eaFileSplit, // split - create new group from selected files eaGroupMoveOffset, // move group to offset relative to the current position in download-queue eaGroupMoveTop, // move group to the top of download-queue eaGroupMoveBottom, // move group to the bottom of download-queue eaGroupMoveBefore, // move group to a certain position eaGroupMoveAfter, // move group to a certain position eaGroupPause, // pause group eaGroupResume, // resume (unpause) group eaGroupDelete, // delete group and put to history, delete already downloaded files eaGroupParkDelete, // delete group and put to history, keep already downloaded files eaGroupDupeDelete, // delete group, put to history and mark as duplicate, delete already downloaded files eaGroupFinalDelete, // delete group without adding to history, delete already downloaded files eaGroupPauseAllPars, // pause only (all) pars (does not affect other files) in group eaGroupPauseExtraPars, // pause only (almost all) pars in group, except main par-file (does not affect other files) eaGroupSetPriority, // set priority for groups eaGroupSetCategory, // set or change category for a group eaGroupApplyCategory, // set or change category for a group and reassign pp-params according to category settings eaGroupMerge, // merge groups eaGroupSetParameter, // set post-process parameter for group eaGroupSetName, // set group name (rename group) eaGroupSetDupeKey, // set duplicate key eaGroupSetDupeScore, // set duplicate score eaGroupSetDupeMode, // set duplicate mode eaGroupSort, // sort groups eaGroupSortFiles, // sort files for optimal download order eaPostDelete, // cancel post-processing eaHistoryDelete, // hide history-item eaHistoryFinalDelete, // delete history-item eaHistoryReturn, // move history-item back to download queue eaHistoryProcess, // move history-item back to download queue and start postprocessing eaHistoryRedownload, // move history-item back to download queue for full redownload eaHistoryRetryFailed, // move history-item back to download queue for redownload of failed articles eaHistorySetParameter, // set post-process parameter for history-item eaHistorySetDupeKey, // set duplicate key eaHistorySetDupeScore, // set duplicate score eaHistorySetDupeMode, // set duplicate mode eaHistorySetDupeBackup, // set duplicate backup flag eaHistoryMarkBad, // mark history-item as bad (and download other duplicate) eaHistoryMarkGood, // mark history-item as good (and push it into dup-history) eaHistoryMarkSuccess, // mark history-item as success (and do nothing more) eaHistorySetCategory, // set or change category for history-item eaHistorySetName // set history-item name (rename) }; enum EMatchMode { mmId = 1, mmName, mmRegEx }; static bool IsLoaded() { return g_Loaded; } static GuardedDownloadQueue Guard() { return GuardedDownloadQueue(g_DownloadQueue, &g_DownloadQueue->m_lockMutex); } NzbList* GetQueue() { return &m_queue; } HistoryList* GetHistory() { return &m_history; } virtual bool EditEntry(int ID, EEditAction action, const char* args) = 0; virtual bool EditList(IdList* idList, NameList* nameList, EMatchMode matchMode, EEditAction action, const char* args) = 0; virtual void HistoryChanged() = 0; virtual void Save() = 0; void CalcRemainingSize(int64* remaining, int64* remainingForced); protected: DownloadQueue() {} static void Init(DownloadQueue* globalInstance) { g_DownloadQueue = globalInstance; } static void Final() { g_DownloadQueue = nullptr; } static void Loaded() { g_Loaded = true; } private: NzbList m_queue; HistoryList m_history; Mutex m_lockMutex; static DownloadQueue* g_DownloadQueue; static bool g_Loaded; }; #endif nzbget-19.1/daemon/queue/DiskState.h0000644000175000017500000001022213130203062017167 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2016 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, see . */ #ifndef DISKSTATE_H #define DISKSTATE_H #include "DownloadInfo.h" #include "FeedInfo.h" #include "NewsServer.h" #include "StatMeter.h" #include "FileSystem.h" #include "Log.h" class StateDiskFile; class DiskState { public: bool DownloadQueueExists(); bool SaveDownloadQueue(DownloadQueue* downloadQueue, bool saveHistory); bool LoadDownloadQueue(DownloadQueue* downloadQueue, Servers* servers); bool SaveFile(FileInfo* fileInfo); bool LoadFile(FileInfo* fileInfo, bool fileSummary, bool articles); bool SaveFileState(FileInfo* fileInfo, bool completed); bool LoadFileState(FileInfo* fileInfo, Servers* servers, bool completed); bool LoadArticles(FileInfo* fileInfo); void DiscardDownloadQueue(); void DiscardFile(int fileId, bool deleteData, bool deletePartialState, bool deleteCompletedState); void DiscardFiles(NzbInfo* nzbInfo, bool deleteLog = true); bool SaveFeeds(Feeds* feeds, FeedHistory* feedHistory); bool LoadFeeds(Feeds* feeds, FeedHistory* feedHistory); bool SaveStats(Servers* servers, ServerVolumes* serverVolumes); bool LoadStats(Servers* servers, ServerVolumes* serverVolumes, bool* perfectMatch); void CleanupTempDir(DownloadQueue* downloadQueue); void WriteCacheFlag(); void DeleteCacheFlag(); void AppendNzbMessage(int nzbId, Message::EKind kind, const char* text); void LoadNzbMessages(int nzbId, MessageList* messages); private: bool SaveFileInfo(FileInfo* fileInfo, StateDiskFile& outfile); bool LoadFileInfo(FileInfo* fileInfo, StateDiskFile& outfile, int formatVersion, bool fileSummary, bool articles); bool SaveFileState(FileInfo* fileInfo, StateDiskFile& outfile, bool completed); bool LoadFileState(FileInfo* fileInfo, Servers* servers, StateDiskFile& infile, int formatVersion, bool completed); void SaveQueue(NzbList* queue, StateDiskFile& outfile); bool LoadQueue(NzbList* queue, Servers* servers, StateDiskFile& infile, int formatVersion); void SaveNzbInfo(NzbInfo* nzbInfo, StateDiskFile& outfile); bool LoadNzbInfo(NzbInfo* nzbInfo, Servers* servers, StateDiskFile& infile, int formatVersion); void SaveDupInfo(DupInfo* dupInfo, StateDiskFile& outfile); bool LoadDupInfo(DupInfo* dupInfo, StateDiskFile& infile, int formatVersion); void SaveHistory(HistoryList* history, StateDiskFile& outfile); bool LoadHistory(HistoryList* history, Servers* servers, StateDiskFile& infile, int formatVersion); bool SaveFeedStatus(Feeds* feeds, StateDiskFile& outfile); bool LoadFeedStatus(Feeds* feeds, StateDiskFile& infile, int formatVersion); bool SaveFeedHistory(FeedHistory* feedHistory, StateDiskFile& outfile); bool LoadFeedHistory(FeedHistory* feedHistory, StateDiskFile& infile, int formatVersion); bool SaveServerInfo(Servers* servers, StateDiskFile& outfile); bool LoadServerInfo(Servers* servers, StateDiskFile& infile, int formatVersion, bool* perfectMatch); bool SaveVolumeStat(ServerVolumes* serverVolumes, StateDiskFile& outfile); bool LoadVolumeStat(Servers* servers, ServerVolumes* serverVolumes, StateDiskFile& infile, int formatVersion); void CalcFileStats(DownloadQueue* downloadQueue, int formatVersion); bool LoadAllFileStates(DownloadQueue* downloadQueue, Servers* servers); void SaveServerStats(ServerStatList* serverStatList, StateDiskFile& outfile); bool LoadServerStats(ServerStatList* serverStatList, Servers* servers, StateDiskFile& infile); void CleanupQueueDir(DownloadQueue* downloadQueue); }; extern DiskState* g_DiskState; #endif nzbget-19.1/daemon/queue/HistoryCoordinator.cpp0000644000175000017500000006751013130203062021510 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #include "nzbget.h" #include "HistoryCoordinator.h" #include "Options.h" #include "Log.h" #include "QueueCoordinator.h" #include "DiskState.h" #include "Util.h" #include "FileSystem.h" #include "NzbFile.h" #include "DupeCoordinator.h" #include "ParParser.h" #include "PrePostProcessor.h" #include "DupeCoordinator.h" #include "ServerPool.h" /** * Removes old entries from (recent) history */ void HistoryCoordinator::ServiceWork() { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); time_t minTime = Util::CurrentTime() - g_Options->GetKeepHistory() * 60*60*24; bool changed = false; int index = 0; // traversing in a reverse order to delete items in order they were added to history // (just to produce the log-messages in a more logical order) for (HistoryList::reverse_iterator it = downloadQueue->GetHistory()->rbegin(); it != downloadQueue->GetHistory()->rend(); ) { HistoryInfo* historyInfo = (*it).get(); if (historyInfo->GetKind() != HistoryInfo::hkDup && historyInfo->GetTime() < minTime) { if (g_Options->GetDupeCheck() && historyInfo->GetKind() == HistoryInfo::hkNzb) { // replace history element HistoryHide(downloadQueue, historyInfo, index); index++; } else { if (historyInfo->GetKind() == HistoryInfo::hkNzb) { DeleteDiskFiles(historyInfo->GetNzbInfo()); } info("Collection %s removed from history", historyInfo->GetName()); downloadQueue->GetHistory()->erase(downloadQueue->GetHistory()->end() - 1 - index); } it = downloadQueue->GetHistory()->rbegin() + index; changed = true; } else { it++; index++; } } if (changed) { downloadQueue->HistoryChanged(); downloadQueue->Save(); } } void HistoryCoordinator::DeleteDiskFiles(NzbInfo* nzbInfo) { if (g_Options->GetSaveQueue() && g_Options->GetServerMode()) { // delete parked files g_DiskState->DiscardFiles(nzbInfo); } nzbInfo->GetFileList()->clear(); // delete nzb-file if (!g_Options->GetNzbCleanupDisk()) { return; } // QueuedFile may contain one filename or several filenames separated // with "|"-character (for merged groups) CString filename = nzbInfo->GetQueuedFilename(); char* end = filename - 1; while (end) { char* name1 = end + 1; end = strchr(name1, '|'); if (end) *end = '\0'; if (FileSystem::FileExists(name1)) { info("Deleting file %s", name1); FileSystem::DeleteFile(name1); } } } void HistoryCoordinator::AddToHistory(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) { std::unique_ptr oldNzbInfo = downloadQueue->GetQueue()->Remove(nzbInfo); std::unique_ptr historyInfo = std::make_unique(std::move(oldNzbInfo)); historyInfo->SetTime(Util::CurrentTime()); downloadQueue->GetHistory()->Add(std::move(historyInfo), true); downloadQueue->HistoryChanged(); // park remaining files for (FileInfo* fileInfo : nzbInfo->GetFileList()) { nzbInfo->UpdateCompletedStats(fileInfo); nzbInfo->GetCompletedFiles()->emplace_back(fileInfo->GetId(), fileInfo->GetFilename(), CompletedFile::cfNone, 0, fileInfo->GetParFile(), fileInfo->GetHash16k(), fileInfo->GetParSetId()); } // Cleaning up parked files if par-check was successful or unpack was successful or // health is 100% (if unpack and par-check were not performed) or if deleted bool cleanupParkedFiles = ((nzbInfo->GetParStatus() == NzbInfo::psSuccess || nzbInfo->GetParStatus() == NzbInfo::psRepairPossible) && nzbInfo->GetUnpackStatus() != NzbInfo::usFailure && nzbInfo->GetUnpackStatus() != NzbInfo::usSpace && nzbInfo->GetUnpackStatus() != NzbInfo::usPassword) || (nzbInfo->GetUnpackStatus() == NzbInfo::usSuccess && nzbInfo->GetParStatus() != NzbInfo::psFailure) || (nzbInfo->GetUnpackStatus() <= NzbInfo::usSkipped && nzbInfo->GetParStatus() != NzbInfo::psFailure && nzbInfo->GetFailedSize() - nzbInfo->GetParFailedSize() == 0) || (nzbInfo->GetDeleteStatus() != NzbInfo::dsNone); // Do not cleanup when parking cleanupParkedFiles &= !nzbInfo->GetParking(); // Parking not possible if files were already deleted cleanupParkedFiles |= nzbInfo->GetUnpackCleanedUpDisk(); if (cleanupParkedFiles) { g_DiskState->DiscardFiles(nzbInfo, false); nzbInfo->GetCompletedFiles()->clear(); } nzbInfo->SetParkedFileCount(0); for (CompletedFile& completedFile : nzbInfo->GetCompletedFiles()) { if (completedFile.GetStatus() == CompletedFile::cfNone || // consider last completed file with partial status not completely tried (completedFile.GetStatus() == CompletedFile::cfPartial && &completedFile == &*nzbInfo->GetCompletedFiles()->rbegin())) { nzbInfo->PrintMessage(Message::mkDetail, "Parking file %s", completedFile.GetFilename()); nzbInfo->SetParkedFileCount(nzbInfo->GetParkedFileCount() + 1); } } nzbInfo->GetFileList()->clear(); nzbInfo->SetRemainingParCount(0); nzbInfo->SetParking(false); if (nzbInfo->GetDirectRenameStatus() == NzbInfo::tsRunning) { nzbInfo->SetDirectRenameStatus(NzbInfo::tsFailure); } nzbInfo->PrintMessage(Message::mkInfo, "Collection %s added to history", nzbInfo->GetName()); } void HistoryCoordinator::HistoryHide(DownloadQueue* downloadQueue, HistoryInfo* historyInfo, int rindex) { // replace history element std::unique_ptr dupInfo = std::make_unique(); dupInfo->SetId(historyInfo->GetNzbInfo()->GetId()); dupInfo->SetName(historyInfo->GetNzbInfo()->GetName()); dupInfo->SetDupeKey(historyInfo->GetNzbInfo()->GetDupeKey()); dupInfo->SetDupeScore(historyInfo->GetNzbInfo()->GetDupeScore()); dupInfo->SetDupeMode(historyInfo->GetNzbInfo()->GetDupeMode()); dupInfo->SetSize(historyInfo->GetNzbInfo()->GetSize()); dupInfo->SetFullContentHash(historyInfo->GetNzbInfo()->GetFullContentHash()); dupInfo->SetFilteredContentHash(historyInfo->GetNzbInfo()->GetFilteredContentHash()); dupInfo->SetStatus( historyInfo->GetNzbInfo()->GetMarkStatus() == NzbInfo::ksGood ? DupInfo::dsGood : historyInfo->GetNzbInfo()->GetMarkStatus() == NzbInfo::ksBad ? DupInfo::dsBad : historyInfo->GetNzbInfo()->GetMarkStatus() == NzbInfo::ksSuccess ? DupInfo::dsSuccess : historyInfo->GetNzbInfo()->GetDeleteStatus() == NzbInfo::dsDupe ? DupInfo::dsDupe : historyInfo->GetNzbInfo()->GetDeleteStatus() == NzbInfo::dsManual || historyInfo->GetNzbInfo()->GetDeleteStatus() == NzbInfo::dsGood || historyInfo->GetNzbInfo()->GetDeleteStatus() == NzbInfo::dsCopy ? DupInfo::dsDeleted : historyInfo->GetNzbInfo()->IsDupeSuccess() ? DupInfo::dsSuccess : DupInfo::dsFailed); std::unique_ptr newHistoryInfo = std::make_unique(std::move(dupInfo)); newHistoryInfo->SetTime(historyInfo->GetTime()); DeleteDiskFiles(historyInfo->GetNzbInfo()); info("Collection %s removed from history", historyInfo->GetName()); (*downloadQueue->GetHistory())[downloadQueue->GetHistory()->size() - 1 - rindex] = std::move(newHistoryInfo); } void HistoryCoordinator::PrepareEdit(DownloadQueue* downloadQueue, IdList* idList, DownloadQueue::EEditAction action) { // First pass: when marking multiple items - mark them bad without performing the mark-logic, // this will later (on second step) avoid moving other items to download queue, if they are marked bad too. if (action == DownloadQueue::eaHistoryMarkBad) { for (int id : *idList) { HistoryInfo* historyInfo = downloadQueue->GetHistory()->Find(id); if (historyInfo && historyInfo->GetKind() == HistoryInfo::hkNzb) { historyInfo->GetNzbInfo()->SetMarkStatus(NzbInfo::ksBad); } } } } bool HistoryCoordinator::EditList(DownloadQueue* downloadQueue, IdList* idList, DownloadQueue::EEditAction action, const char* args) { bool ok = false; PrepareEdit(downloadQueue, idList, action); for (int id : *idList) { for (HistoryList::iterator itHistory = downloadQueue->GetHistory()->begin(); itHistory != downloadQueue->GetHistory()->end(); itHistory++) { HistoryInfo* historyInfo = (*itHistory).get(); if (historyInfo->GetId() == id) { ok = true; switch (action) { case DownloadQueue::eaHistoryDelete: case DownloadQueue::eaHistoryFinalDelete: HistoryDelete(downloadQueue, itHistory, historyInfo, action == DownloadQueue::eaHistoryFinalDelete); break; case DownloadQueue::eaHistoryReturn: HistoryReturn(downloadQueue, itHistory, historyInfo); break; case DownloadQueue::eaHistoryProcess: HistoryProcess(downloadQueue, itHistory, historyInfo); break; case DownloadQueue::eaHistoryRedownload: HistoryRedownload(downloadQueue, itHistory, historyInfo, false); break; case DownloadQueue::eaHistoryRetryFailed: HistoryRetry(downloadQueue, itHistory, historyInfo, true, false); break; case DownloadQueue::eaHistorySetParameter: ok = HistorySetParameter(historyInfo, args); break; case DownloadQueue::eaHistorySetCategory: ok = HistorySetCategory(historyInfo, args); break; case DownloadQueue::eaHistorySetName: ok = HistorySetName(historyInfo, args); break; case DownloadQueue::eaHistorySetDupeKey: case DownloadQueue::eaHistorySetDupeScore: case DownloadQueue::eaHistorySetDupeMode: case DownloadQueue::eaHistorySetDupeBackup: HistorySetDupeParam(historyInfo, action, args); break; case DownloadQueue::eaHistoryMarkBad: g_DupeCoordinator->HistoryMark(downloadQueue, historyInfo, NzbInfo::ksBad); break; case DownloadQueue::eaHistoryMarkGood: g_DupeCoordinator->HistoryMark(downloadQueue, historyInfo, NzbInfo::ksGood); break; case DownloadQueue::eaHistoryMarkSuccess: g_DupeCoordinator->HistoryMark(downloadQueue, historyInfo, NzbInfo::ksSuccess); break; default: // nothing, just to avoid compiler warning break; } break; } } } if (ok) { downloadQueue->HistoryChanged(); downloadQueue->Save(); } return ok; } void HistoryCoordinator::HistoryDelete(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo, bool final) { info("Deleting %s from history", historyInfo->GetName()); if (historyInfo->GetKind() == HistoryInfo::hkNzb) { DeleteDiskFiles(historyInfo->GetNzbInfo()); } if (historyInfo->GetKind() == HistoryInfo::hkNzb && (historyInfo->GetNzbInfo()->GetDeleteStatus() != NzbInfo::dsNone || historyInfo->GetNzbInfo()->GetParStatus() == NzbInfo::psFailure || historyInfo->GetNzbInfo()->GetUnpackStatus() == NzbInfo::usFailure || historyInfo->GetNzbInfo()->GetUnpackStatus() == NzbInfo::usPassword) && FileSystem::DirectoryExists(historyInfo->GetNzbInfo()->GetDestDir())) { info("Deleting %s", historyInfo->GetNzbInfo()->GetDestDir()); CString errmsg; if (!FileSystem::DeleteDirectoryWithContent(historyInfo->GetNzbInfo()->GetDestDir(), errmsg)) { error("Could not delete directory %s: %s", historyInfo->GetNzbInfo()->GetDestDir(), *errmsg); } } if (final || !g_Options->GetDupeCheck() || historyInfo->GetKind() == HistoryInfo::hkUrl) { downloadQueue->GetHistory()->erase(itHistory); } else { if (historyInfo->GetKind() == HistoryInfo::hkNzb) { // replace history element int rindex = downloadQueue->GetHistory()->size() - 1 - (itHistory - downloadQueue->GetHistory()->begin()); HistoryHide(downloadQueue, historyInfo, rindex); } } } void HistoryCoordinator::MoveToQueue(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo, bool reprocess) { debug("Returning %s from history back to download queue", historyInfo->GetName()); CString nicename = historyInfo->GetName(); NzbInfo* nzbInfo = historyInfo->GetNzbInfo(); // unpark files for (FileInfo* fileInfo : nzbInfo->GetFileList()) { nzbInfo->PrintMessage(Message::mkDetail, "Unparking file %s", fileInfo->GetFilename()); } downloadQueue->GetQueue()->Add(std::unique_ptr(nzbInfo), true); historyInfo->DiscardNzbInfo(); // reset postprocessing status variables if (!nzbInfo->GetUnpackCleanedUpDisk()) { nzbInfo->SetUnpackStatus(NzbInfo::usNone); nzbInfo->SetDirectUnpackStatus(NzbInfo::nsNone); nzbInfo->SetCleanupStatus(NzbInfo::csNone); nzbInfo->SetParRenameStatus(NzbInfo::rsNone); nzbInfo->SetRarRenameStatus(NzbInfo::rsNone); nzbInfo->SetPostTotalSec(nzbInfo->GetPostTotalSec() - nzbInfo->GetUnpackSec()); nzbInfo->SetUnpackSec(0); if (ParParser::FindMainPars(nzbInfo->GetDestDir(), nullptr)) { nzbInfo->SetParStatus(NzbInfo::psNone); nzbInfo->SetPostTotalSec(nzbInfo->GetPostTotalSec() - nzbInfo->GetParSec()); nzbInfo->SetParSec(0); nzbInfo->SetRepairSec(0); nzbInfo->SetParFull(false); } } nzbInfo->SetDeleteStatus(NzbInfo::dsNone); nzbInfo->SetDeletePaused(false); nzbInfo->SetMarkStatus(NzbInfo::ksNone); nzbInfo->GetScriptStatuses()->clear(); nzbInfo->SetParkedFileCount(0); if (nzbInfo->GetMoveStatus() == NzbInfo::msFailure) { nzbInfo->SetMoveStatus(NzbInfo::msNone); } nzbInfo->SetReprocess(reprocess); nzbInfo->SetFinalDir(""); downloadQueue->GetHistory()->erase(itHistory); // the object "pHistoryInfo" is released few lines later, after the call to "NZBDownloaded" nzbInfo->PrintMessage(Message::mkInfo, "%s returned from history back to download queue", *nicename); if (reprocess) { // start postprocessing debug("Restarting postprocessing for %s", *nicename); g_PrePostProcessor->NzbDownloaded(downloadQueue, nzbInfo); } } void HistoryCoordinator::HistoryRedownload(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo, bool restorePauseState) { if (historyInfo->GetKind() == HistoryInfo::hkUrl) { NzbInfo* nzbInfo = historyInfo->GetNzbInfo(); historyInfo->DiscardNzbInfo(); nzbInfo->SetUrlStatus(NzbInfo::lsNone); nzbInfo->SetDeleteStatus(NzbInfo::dsNone); downloadQueue->GetQueue()->Add(std::unique_ptr(nzbInfo), true); downloadQueue->GetHistory()->erase(itHistory); return; } if (historyInfo->GetKind() != HistoryInfo::hkNzb) { error("Could not download again %s: history item has wrong type", historyInfo->GetName()); return; } NzbInfo* nzbInfo = historyInfo->GetNzbInfo(); bool paused = restorePauseState && nzbInfo->GetDeletePaused(); if (!FileSystem::FileExists(nzbInfo->GetQueuedFilename())) { error("Could not download again %s: could not find source nzb-file %s", nzbInfo->GetName(), nzbInfo->GetQueuedFilename()); return; } NzbFile nzbFile(nzbInfo->GetQueuedFilename(), ""); if (!nzbFile.Parse()) { error("Could not download again %s: could not parse nzb-file", nzbInfo->GetName()); return; } info("Downloading again %s", nzbInfo->GetName()); std::unique_ptr newNzbInfo = nzbFile.DetachNzbInfo(); for (FileInfo* fileInfo : newNzbInfo->GetFileList()) { fileInfo->SetPaused(paused); } if (FileSystem::DirectoryExists(nzbInfo->GetDestDir())) { detail("Deleting %s", nzbInfo->GetDestDir()); CString errmsg; if (!FileSystem::DeleteDirectoryWithContent(nzbInfo->GetDestDir(), errmsg)) { error("Could not delete directory %s: %s", nzbInfo->GetDestDir(), *errmsg); } } nzbInfo->BuildDestDirName(); if (FileSystem::DirectoryExists(nzbInfo->GetDestDir())) { detail("Deleting %s", nzbInfo->GetDestDir()); CString errmsg; if (!FileSystem::DeleteDirectoryWithContent(nzbInfo->GetDestDir(), errmsg)) { error("Could not delete directory %s: %s", nzbInfo->GetDestDir(), *errmsg); } } g_DiskState->DiscardFiles(nzbInfo); // reset status fields (which are not reset by "MoveToQueue") nzbInfo->SetMoveStatus(NzbInfo::msNone); nzbInfo->SetUnpackCleanedUpDisk(false); nzbInfo->SetParStatus(NzbInfo::psNone); nzbInfo->SetParRenameStatus(NzbInfo::rsNone); nzbInfo->SetRarRenameStatus(NzbInfo::rsNone); nzbInfo->SetDirectRenameStatus(NzbInfo::tsNone); nzbInfo->SetDirectUnpackStatus(NzbInfo::nsNone); nzbInfo->SetDownloadedSize(0); nzbInfo->SetDownloadSec(0); nzbInfo->SetPostTotalSec(0); nzbInfo->SetParSec(0); nzbInfo->SetRepairSec(0); nzbInfo->SetUnpackSec(0); nzbInfo->SetExtraParBlocks(0); nzbInfo->SetAllFirst(false); nzbInfo->SetWaitingPar(false); nzbInfo->SetLoadingPar(false); nzbInfo->GetCompletedFiles()->clear(); nzbInfo->GetServerStats()->clear(); nzbInfo->GetCurrentServerStats()->clear(); nzbInfo->MoveFileList(newNzbInfo.get()); g_QueueCoordinator->CheckDupeFileInfos(nzbInfo); MoveToQueue(downloadQueue, itHistory, historyInfo, false); g_PrePostProcessor->NzbAdded(downloadQueue, nzbInfo); } void HistoryCoordinator::HistoryReturn(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo) { if (historyInfo->GetKind() == HistoryInfo::hkUrl) { HistoryRedownload(downloadQueue, itHistory, historyInfo, false); } else if (historyInfo->GetKind() == HistoryInfo::hkNzb && historyInfo->GetNzbInfo()->GetParkedFileCount() == 0) { warn("Could not download remaining files for %s: history item does not have any files left for download", historyInfo->GetName()); } else if (historyInfo->GetKind() == HistoryInfo::hkNzb) { HistoryRetry(downloadQueue, itHistory, historyInfo, false, false); } else { error("Could not download remaining files for %s: history item has wrong type", historyInfo->GetName()); } } void HistoryCoordinator::HistoryProcess(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo) { if (historyInfo->GetKind() != HistoryInfo::hkNzb) { error("Could not post-process again %s: history item has wrong type", historyInfo->GetName()); return; } HistoryRetry(downloadQueue, itHistory, historyInfo, false, true); } void HistoryCoordinator::HistoryRetry(DownloadQueue* downloadQueue, HistoryList::iterator itHistory, HistoryInfo* historyInfo, bool resetFailed, bool reprocess) { if (historyInfo->GetKind() != HistoryInfo::hkNzb) { error("Could not %s for %s: history item has wrong type", (resetFailed ? "retry failed articles" : "download remaining files"), historyInfo->GetName()); return; } NzbInfo* nzbInfo = historyInfo->GetNzbInfo(); if (!FileSystem::DirectoryExists(nzbInfo->GetDestDir())) { error("Could not %s %s: destination directory %s doesn't exist", (resetFailed ? "retry failed articles for" : reprocess ? "post-process again" : "download remaining files for"), historyInfo->GetName(), nzbInfo->GetDestDir()); return; } nzbInfo->PrintMessage(Message::mkInfo, "%s %s", (resetFailed ? "Retrying failed articles for" : reprocess ? "Post-processing again" : "Downloading remaining files for"), nzbInfo->GetName()); // move failed completed files to (parked) file list for (CompletedFileList::iterator it = nzbInfo->GetCompletedFiles()->begin(); it != nzbInfo->GetCompletedFiles()->end(); ) { CompletedFile& completedFile = *it; if (completedFile.GetStatus() != CompletedFile::cfSuccess && (completedFile.GetStatus() != CompletedFile::cfFailure || resetFailed) && completedFile.GetId() > 0) { std::unique_ptr fileInfo = std::make_unique(); fileInfo->SetId(completedFile.GetId()); if (g_DiskState->LoadFile(fileInfo.get(), true, true) && (completedFile.GetStatus() == CompletedFile::cfNone || (completedFile.GetStatus() == CompletedFile::cfFailure && resetFailed) || (completedFile.GetStatus() == CompletedFile::cfPartial && g_DiskState->LoadFileState(fileInfo.get(), g_ServerPool->GetServers(), true) && (resetFailed || fileInfo->GetRemainingSize() > 0)))) { fileInfo->SetFilename(completedFile.GetFilename()); fileInfo->SetNzbInfo(nzbInfo); BString<1024> outputFilename("%s%c%s", nzbInfo->GetDestDir(), PATH_SEPARATOR, fileInfo->GetFilename()); if (fileInfo->GetSuccessArticles() == 0 || FileSystem::FileSize(outputFilename) == 0) { FileSystem::DeleteFile(outputFilename); } if (fileInfo->GetSuccessArticles() > 0) { if (FileSystem::FileExists(outputFilename)) { fileInfo->SetPartialState(FileInfo::psCompleted); } else if (!reprocess) { nzbInfo->PrintMessage(Message::mkWarning, "File %s could not be found on disk, downloading again", fileInfo->GetFilename()); fileInfo->SetPartialState(FileInfo::psNone); } } ResetArticles(fileInfo.get(), completedFile.GetStatus() == CompletedFile::cfFailure, resetFailed); g_DiskState->DiscardFile(fileInfo->GetId(), false, true, fileInfo->GetPartialState() != FileInfo::psCompleted); if (fileInfo->GetPartialState() == FileInfo::psCompleted) { g_DiskState->SaveFileState(fileInfo.get(), true); } fileInfo->GetArticles()->clear(); nzbInfo->GetFileList()->Add(std::move(fileInfo), false); it = nzbInfo->GetCompletedFiles()->erase(it); continue; } } it++; } nzbInfo->UpdateCurrentStats(); MoveToQueue(downloadQueue, itHistory, historyInfo, reprocess); if (g_Options->GetParCheck() != Options::pcForce) { downloadQueue->EditEntry(nzbInfo->GetId(), DownloadQueue::eaGroupPauseExtraPars, nullptr); } } void HistoryCoordinator::ResetArticles(FileInfo* fileInfo, bool allFailed, bool resetFailed) { NzbInfo* nzbInfo = fileInfo->GetNzbInfo(); if (allFailed) { fileInfo->SetFailedSize(fileInfo->GetSize() - fileInfo->GetMissedSize()); fileInfo->SetFailedArticles(fileInfo->GetTotalArticles() - fileInfo->GetMissedArticles()); fileInfo->SetRemainingSize(0); fileInfo->SetCompletedArticles(fileInfo->GetFailedArticles()); } nzbInfo->GetServerStats()->ListOp(fileInfo->GetServerStats(), ServerStatList::soSubtract); nzbInfo->SetFailedSize(nzbInfo->GetFailedSize() - fileInfo->GetFailedSize()); nzbInfo->SetSuccessSize(nzbInfo->GetSuccessSize() - fileInfo->GetSuccessSize()); nzbInfo->SetFailedArticles(nzbInfo->GetFailedArticles() - fileInfo->GetFailedArticles()); nzbInfo->SetSuccessArticles(nzbInfo->GetSuccessArticles() - fileInfo->GetSuccessArticles()); if (fileInfo->GetParFile()) { nzbInfo->SetParFailedSize(nzbInfo->GetParFailedSize() - fileInfo->GetFailedSize()); nzbInfo->SetParSuccessSize(nzbInfo->GetParSuccessSize() - fileInfo->GetSuccessSize()); } for (ArticleInfo* pa : fileInfo->GetArticles()) { if ((pa->GetStatus() == ArticleInfo::aiFailed && (resetFailed || fileInfo->GetPartialState() == FileInfo::psNone)) || (pa->GetStatus() == ArticleInfo::aiUndefined && resetFailed && allFailed) || (pa->GetStatus() == ArticleInfo::aiFinished && fileInfo->GetPartialState() == FileInfo::psNone)) { fileInfo->SetCompletedArticles(fileInfo->GetCompletedArticles() - 1); fileInfo->SetRemainingSize(fileInfo->GetRemainingSize() + pa->GetSize()); if (pa->GetStatus() == ArticleInfo::aiFailed || pa->GetStatus() == ArticleInfo::aiUndefined) { fileInfo->SetFailedArticles(fileInfo->GetFailedArticles() - 1); fileInfo->SetFailedSize(fileInfo->GetFailedSize() - pa->GetSize()); } else if (pa->GetStatus() == ArticleInfo::aiFinished) { fileInfo->SetSuccessArticles(fileInfo->GetSuccessArticles() - 1); fileInfo->SetSuccessSize(fileInfo->GetSuccessSize() - pa->GetSize()); } pa->SetStatus(ArticleInfo::aiUndefined); pa->SetCrc(0); pa->SetSegmentOffset(0); pa->SetSegmentSize(0); } } } bool HistoryCoordinator::HistorySetParameter(HistoryInfo* historyInfo, const char* text) { debug("Setting post-process-parameter '%s' for '%s'", text, historyInfo->GetName()); if (!(historyInfo->GetKind() == HistoryInfo::hkNzb || historyInfo->GetKind() == HistoryInfo::hkUrl)) { error("Could not set post-process-parameter for %s: history item has wrong type", historyInfo->GetName()); return false; } CString str = text; char* value = strchr(str, '='); if (value) { *value = '\0'; value++; historyInfo->GetNzbInfo()->GetParameters()->SetParameter(str, value); } else { error("Could not set post-process-parameter for %s: invalid argument: %s", historyInfo->GetNzbInfo()->GetName(), text); } return true; } bool HistoryCoordinator::HistorySetCategory(HistoryInfo* historyInfo, const char* text) { debug("Setting category '%s' for '%s'", text, historyInfo->GetName()); if (!(historyInfo->GetKind() == HistoryInfo::hkNzb || historyInfo->GetKind() == HistoryInfo::hkUrl)) { error("Could not set category for %s: history item has wrong type", historyInfo->GetName()); return false; } historyInfo->GetNzbInfo()->SetCategory(text); return true; } bool HistoryCoordinator::HistorySetName(HistoryInfo* historyInfo, const char* text) { debug("Setting name '%s' for '%s'", text, historyInfo->GetName()); if (Util::EmptyStr(text)) { error("Could not rename %s. The new name cannot be empty", historyInfo->GetName()); return false; } if (historyInfo->GetKind() == HistoryInfo::hkNzb || historyInfo->GetKind() == HistoryInfo::hkUrl) { historyInfo->GetNzbInfo()->SetName(text); } else if (historyInfo->GetKind() == HistoryInfo::hkDup) { historyInfo->GetDupInfo()->SetName(text); } return true; } void HistoryCoordinator::HistorySetDupeParam(HistoryInfo* historyInfo, DownloadQueue::EEditAction action, const char* text) { debug("Setting dupe-parameter '%i'='%s' for '%s'", (int)action, text, historyInfo->GetName()); EDupeMode mode = dmScore; if (action == DownloadQueue::eaHistorySetDupeMode) { if (!strcasecmp(text, "SCORE")) { mode = dmScore; } else if (!strcasecmp(text, "ALL")) { mode = dmAll; } else if (!strcasecmp(text, "FORCE")) { mode = dmForce; } else { error("Could not set duplicate mode for %s: incorrect mode (%s)", historyInfo->GetName(), text); return; } } if (historyInfo->GetKind() == HistoryInfo::hkNzb || historyInfo->GetKind() == HistoryInfo::hkUrl) { switch (action) { case DownloadQueue::eaHistorySetDupeKey: historyInfo->GetNzbInfo()->SetDupeKey(text); break; case DownloadQueue::eaHistorySetDupeScore: historyInfo->GetNzbInfo()->SetDupeScore(atoi(text)); break; case DownloadQueue::eaHistorySetDupeMode: historyInfo->GetNzbInfo()->SetDupeMode(mode); break; case DownloadQueue::eaHistorySetDupeBackup: if (historyInfo->GetKind() == HistoryInfo::hkUrl) { error("Could not set duplicate parameter for %s: history item has wrong type", historyInfo->GetName()); return; } else if (historyInfo->GetNzbInfo()->GetDeleteStatus() != NzbInfo::dsDupe && historyInfo->GetNzbInfo()->GetDeleteStatus() != NzbInfo::dsManual) { error("Could not set duplicate parameter for %s: history item has wrong delete status", historyInfo->GetName()); return; } historyInfo->GetNzbInfo()->SetDeleteStatus(!strcasecmp(text, "YES") || !strcasecmp(text, "TRUE") || !strcasecmp(text, "1") ? NzbInfo::dsDupe : NzbInfo::dsManual); break; default: // suppress compiler warning break; } } else if (historyInfo->GetKind() == HistoryInfo::hkDup) { switch (action) { case DownloadQueue::eaHistorySetDupeKey: historyInfo->GetDupInfo()->SetDupeKey(text); break; case DownloadQueue::eaHistorySetDupeScore: historyInfo->GetDupInfo()->SetDupeScore(atoi(text)); break; case DownloadQueue::eaHistorySetDupeMode: historyInfo->GetDupInfo()->SetDupeMode(mode); break; case DownloadQueue::eaHistorySetDupeBackup: error("Could not set duplicate parameter for %s: history item has wrong type", historyInfo->GetName()); return; default: // suppress compiler warning break; } } } void HistoryCoordinator::Redownload(DownloadQueue* downloadQueue, HistoryInfo* historyInfo) { HistoryList::iterator it = downloadQueue->GetHistory()->Find(historyInfo); HistoryRedownload(downloadQueue, it, historyInfo, true); } nzbget-19.1/daemon/queue/DirectRenamer.h0000644000175000017500000000526413130203062020032 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2017 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, see . */ #ifndef DIRECTRENAMER_H #define DIRECTRENAMER_H #include "ArticleDownloader.h" class DirectRenamer { public: class FileHash { public: FileHash(const char* filename, const char* hash) : m_filename(filename), m_hash(hash) {} const char* GetFilename() { return m_filename; } const char* GetHash() { return m_hash; } private: CString m_filename; CString m_hash; }; typedef std::deque FileHashList; class ParFile { public: ParFile(int id, const char* filename, const char* setId, bool completed) : m_id(id), m_filename(filename), m_setId(setId), m_completed(completed) {} int GetId() { return m_id; } const char* GetFilename() { return m_filename; } const char* GetSetId() { return m_setId; } bool GetCompleted() { return m_completed; } private: int m_id; CString m_filename; CString m_setId; bool m_completed = false; }; typedef std::deque ParFileList; std::unique_ptr MakeArticleContentAnalyzer(); void ArticleDownloaded(DownloadQueue* downloadQueue, FileInfo* fileInfo, ArticleInfo* articleInfo, ArticleContentAnalyzer* articleContentAnalyzer); void FileDownloaded(DownloadQueue* downloadQueue, FileInfo* fileInfo); protected: virtual void RenameCompleted(DownloadQueue* downloadQueue, NzbInfo* nzbInfo) = 0; private: void CheckState(DownloadQueue* downloadQueue, NzbInfo* nzbInfo); void UnpausePars(NzbInfo* nzbInfo); void RenameFiles(DownloadQueue* downloadQueue, NzbInfo* nzbInfo, FileHashList* parHashes); bool RenameCompletedFile(NzbInfo* nzbInfo, const char* oldName, const char* newName); bool NeedRenamePars(NzbInfo* nzbInfo); void CollectPars(NzbInfo* nzbInfo, ParFileList* parFiles); CString BuildNewRegularName(const char* oldName, FileHashList* parHashes, const char* hash16k); CString BuildNewParName(const char* oldName, const char* destDir, const char* setId, int& vol); friend class DirectParLoader; }; #endif nzbget-19.1/daemon/queue/Scanner.cpp0000644000175000017500000003622613130203062017234 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "Scanner.h" #include "Options.h" #include "Log.h" #include "QueueCoordinator.h" #include "HistoryCoordinator.h" #include "ScanScript.h" #include "Util.h" #include "FileSystem.h" int Scanner::m_idGen = 0; Scanner::QueueData::QueueData(const char* filename, const char* nzbName, const char* category, int priority, const char* dupeKey, int dupeScore, EDupeMode dupeMode, NzbParameterList* parameters, bool addTop, bool addPaused, NzbInfo* urlInfo, EAddStatus* addStatus, int* nzbId) { m_filename = filename; m_nzbName = nzbName; m_category = category ? category : ""; m_priority = priority; m_dupeKey = dupeKey ? dupeKey : ""; m_dupeScore = dupeScore; m_dupeMode = dupeMode; m_addTop = addTop; m_addPaused = addPaused; m_urlInfo = urlInfo; m_addStatus = addStatus; m_nzbId = nzbId; if (parameters) { m_parameters.CopyFrom(parameters); } } void Scanner::QueueData::SetAddStatus(EAddStatus addStatus) { if (m_addStatus) { *m_addStatus = addStatus; } } void Scanner::QueueData::SetNzbId(int nzbId) { if (m_nzbId) { *m_nzbId = nzbId; } } void Scanner::InitOptions() { m_nzbDirInterval = g_Options->GetNzbDirInterval() * 1000; m_scanScript = ScanScriptController::HasScripts(); } void Scanner::ServiceWork() { if (!DownloadQueue::IsLoaded()) { return; } Guard guard(m_scanMutex); if (m_requestedNzbDirScan || (!g_Options->GetPauseScan() && g_Options->GetNzbDirInterval() > 0 && m_nzbDirInterval >= g_Options->GetNzbDirInterval() * 1000)) { // check nzbdir every g_pOptions->GetNzbDirInterval() seconds or if requested bool checkStat = !m_requestedNzbDirScan; m_requestedNzbDirScan = false; m_scanning = true; CheckIncomingNzbs(g_Options->GetNzbDir(), "", checkStat); if (!checkStat && m_scanScript) { // if immediate scan requested, we need second scan to process files extracted by scan-scripts CheckIncomingNzbs(g_Options->GetNzbDir(), "", checkStat); } m_scanning = false; m_nzbDirInterval = 0; // if NzbDirFileAge is less than NzbDirInterval (that can happen if NzbDirInterval // is set for rare scans like once per hour) we make 4 scans: // - one additional scan is neccessary to check sizes of detected files; // - another scan is required to check files which were extracted by scan-scripts; // - third scan is needed to check sizes of extracted files. if (g_Options->GetNzbDirInterval() > 0 && g_Options->GetNzbDirFileAge() < g_Options->GetNzbDirInterval()) { int maxPass = m_scanScript ? 3 : 1; if (m_pass < maxPass) { // scheduling another scan of incoming directory in NzbDirFileAge seconds. m_nzbDirInterval = (g_Options->GetNzbDirInterval() - g_Options->GetNzbDirFileAge()) * 1000; m_pass++; } else { m_pass = 0; } } DropOldFiles(); m_queueList.clear(); } m_nzbDirInterval += 200; } /** * Check if there are files in directory for incoming nzb-files * and add them to download queue */ void Scanner::CheckIncomingNzbs(const char* directory, const char* category, bool checkStat) { DirBrowser dir(directory); while (const char* filename = dir.Next()) { if (filename[0] == '.') { // skip hidden files continue; } BString<1024> fullfilename("%s%c%s", directory, PATH_SEPARATOR, filename); bool isDirectory = FileSystem::DirectoryExists(fullfilename); // check subfolders if (isDirectory) { const char* useCategory = filename; BString<1024> subCategory; if (strlen(category) > 0) { subCategory.Format("%s%c%s", category, PATH_SEPARATOR, filename); useCategory = subCategory; } CheckIncomingNzbs(fullfilename, useCategory, checkStat); } else if (!isDirectory && CanProcessFile(fullfilename, checkStat)) { ProcessIncomingFile(directory, filename, fullfilename, category); } } } /** * Only files which were not changed during last g_pOptions->GetNzbDirFileAge() seconds * can be processed. That prevents the processing of files, which are currently being * copied into nzb-directory (eg. being downloaded in web-browser). */ bool Scanner::CanProcessFile(const char* fullFilename, bool checkStat) { const char* extension = strrchr(fullFilename, '.'); if (!extension || !strcasecmp(extension, ".queued") || !strcasecmp(extension, ".error") || !strcasecmp(extension, ".processed")) { return false; } if (!checkStat) { return true; } int64 size = FileSystem::FileSize(fullFilename); time_t current = Util::CurrentTime(); bool canProcess = false; bool inList = false; for (FileList::iterator it = m_fileList.begin(); it != m_fileList.end(); it++) { FileData& fileData = *it; if (!strcmp(fileData.GetFilename(), fullFilename)) { inList = true; if (fileData.GetSize() == size && current - fileData.GetLastChange() >= g_Options->GetNzbDirFileAge()) { canProcess = true; m_fileList.erase(it); } else { fileData.SetSize(size); if (fileData.GetSize() != size) { fileData.SetLastChange(current); } } break; } } if (!inList) { m_fileList.emplace_back(fullFilename, size, current); } return canProcess; } /** * Remove old files from the list of monitored files. * Normally these files are deleted from the list when they are processed. * However if a file was detected by function "CanProcessFile" once but wasn't * processed later (for example if the user deleted it), it will stay in the list, * until we remove it here. */ void Scanner::DropOldFiles() { time_t current = Util::CurrentTime(); m_fileList.erase(std::remove_if(m_fileList.begin(), m_fileList.end(), [current](FileData& fileData) { if ((current - fileData.GetLastChange() >= (g_Options->GetNzbDirInterval() + g_Options->GetNzbDirFileAge()) * 2) || // can occur if the system clock was adjusted current < fileData.GetLastChange()) { debug("Removing file %s from scan file list", fileData.GetFilename()); return true; } return false; }), m_fileList.end()); } void Scanner::ProcessIncomingFile(const char* directory, const char* baseFilename, const char* fullFilename, const char* category) { const char* extension = strrchr(baseFilename, '.'); if (!extension) { return; } CString nzbName = ""; CString nzbCategory = category; NzbParameterList parameters; int priority = 0; bool addTop = false; bool addPaused = false; CString dupeKey = ""; int dupeScore = 0; EDupeMode dupeMode = dmScore; EAddStatus addStatus = asSkipped; QueueData* queueData = nullptr; NzbInfo* urlInfo = nullptr; int nzbId = 0; for (QueueData& queueData1 : m_queueList) { if (FileSystem::SameFilename(queueData1.GetFilename(), fullFilename)) { queueData = &queueData1; nzbName = queueData->GetNzbName(); nzbCategory = queueData->GetCategory(); priority = queueData->GetPriority(); dupeKey = queueData->GetDupeKey(); dupeScore = queueData->GetDupeScore(); dupeMode = queueData->GetDupeMode(); addTop = queueData->GetAddTop(); addPaused = queueData->GetAddPaused(); parameters.CopyFrom(queueData->GetParameters()); urlInfo = queueData->GetUrlInfo(); } } InitPPParameters(nzbCategory, ¶meters, false); bool exists = true; if (m_scanScript && strcasecmp(extension, ".nzb_processed")) { ScanScriptController::ExecuteScripts(fullFilename, urlInfo ? urlInfo->GetUrl() : "", directory, &nzbName, &nzbCategory, &priority, ¶meters, &addTop, &addPaused, &dupeKey, &dupeScore, &dupeMode); exists = FileSystem::FileExists(fullFilename); if (exists && strcasecmp(extension, ".nzb")) { CString bakname2; bool renameOK = FileSystem::RenameBak(fullFilename, "processed", false, bakname2); if (!renameOK) { error("Could not rename file %s to %s: %s", fullFilename, *bakname2, *FileSystem::GetLastErrorMessage()); } } } if (!strcasecmp(extension, ".nzb_processed")) { CString renamedName; bool renameOK = FileSystem::RenameBak(fullFilename, "nzb", true, renamedName); if (renameOK) { bool added = AddFileToQueue(renamedName, nzbName, nzbCategory, priority, dupeKey, dupeScore, dupeMode, ¶meters, addTop, addPaused, urlInfo, &nzbId); addStatus = added ? asSuccess : asFailed; } else { error("Could not rename file %s to %s: %s", fullFilename, *renamedName, *FileSystem::GetLastErrorMessage()); addStatus = asFailed; } } else if (exists && !strcasecmp(extension, ".nzb")) { bool added = AddFileToQueue(fullFilename, nzbName, nzbCategory, priority, dupeKey, dupeScore, dupeMode, ¶meters, addTop, addPaused, urlInfo, &nzbId); addStatus = added ? asSuccess : asFailed; } if (queueData) { queueData->SetAddStatus(addStatus); queueData->SetNzbId(nzbId); } } void Scanner::InitPPParameters(const char* category, NzbParameterList* parameters, bool reset) { bool unpack = g_Options->GetUnpack(); const char* extensions = g_Options->GetExtensions(); if (!Util::EmptyStr(category)) { Options::Category* categoryObj = g_Options->FindCategory(category, false); if (categoryObj) { unpack = categoryObj->GetUnpack(); if (!Util::EmptyStr(categoryObj->GetExtensions())) { extensions = categoryObj->GetExtensions(); } } } if (reset) { for (ScriptConfig::Script& script : g_ScriptConfig->GetScripts()) { parameters->SetParameter(BString<1024>("%s:", script.GetName()), nullptr); } } parameters->SetParameter("*Unpack:", unpack ? "yes" : "no"); if (!Util::EmptyStr(extensions)) { // create pp-parameter for each post-processing or queue- script Tokenizer tok(extensions, ",;"); while (const char* scriptName = tok.Next()) { for (ScriptConfig::Script& script : g_ScriptConfig->GetScripts()) { if ((script.GetPostScript() || script.GetQueueScript()) && FileSystem::SameFilename(scriptName, script.GetName())) { parameters->SetParameter(BString<1024>("%s:", scriptName), "yes"); } } } } } bool Scanner::AddFileToQueue(const char* filename, const char* nzbName, const char* category, int priority, const char* dupeKey, int dupeScore, EDupeMode dupeMode, NzbParameterList* parameters, bool addTop, bool addPaused, NzbInfo* urlInfo, int* nzbId) { const char* basename = FileSystem::BaseFileName(filename); info("Adding collection %s to queue", basename); NzbFile nzbFile(filename, category); bool ok = nzbFile.Parse(); if (!ok) { error("Could not add collection %s to queue", basename); } CString bakname2; if (!FileSystem::RenameBak(filename, ok ? "queued" : "error", false, bakname2)) { ok = false; error("Could not rename file %s to %s: %s", filename, *bakname2, *FileSystem::GetLastErrorMessage()); } std::unique_ptr nzbInfo = nzbFile.DetachNzbInfo(); nzbInfo->SetQueuedFilename(bakname2); if (nzbName && strlen(nzbName) > 0) { nzbInfo->SetName(nullptr); nzbInfo->SetFilename(nzbName); nzbInfo->BuildDestDirName(); } nzbInfo->SetDupeKey(dupeKey); nzbInfo->SetDupeScore(dupeScore); nzbInfo->SetDupeMode(dupeMode); nzbInfo->SetPriority(priority); if (urlInfo) { nzbInfo->SetUrl(urlInfo->GetUrl()); nzbInfo->SetUrlStatus(urlInfo->GetUrlStatus()); nzbInfo->SetFeedId(urlInfo->GetFeedId()); } if (nzbFile.GetPassword()) { nzbInfo->GetParameters()->SetParameter("*Unpack:Password", nzbFile.GetPassword()); } nzbInfo->GetParameters()->CopyFrom(parameters); for (FileInfo* fileInfo : nzbInfo->GetFileList()) { fileInfo->SetPaused(addPaused); } NzbInfo* addedNzb = nullptr; if (ok) { addedNzb = g_QueueCoordinator->AddNzbFileToQueue(std::move(nzbInfo), std::move(urlInfo), addTop); } else if (!urlInfo) { nzbInfo->SetDeleteStatus(NzbInfo::dsScan); addedNzb = g_QueueCoordinator->AddNzbFileToQueue(std::move(nzbInfo), std::move(urlInfo), addTop); } if (nzbId) { *nzbId = addedNzb ? addedNzb->GetId() : 0; } return ok; } void Scanner::ScanNzbDir(bool syncMode) { { Guard guard(m_scanMutex); m_scanning = true; m_requestedNzbDirScan = true; } while (syncMode && (m_scanning || m_requestedNzbDirScan)) { usleep(100 * 1000); } } Scanner::EAddStatus Scanner::AddExternalFile(const char* nzbName, const char* category, int priority, const char* dupeKey, int dupeScore, EDupeMode dupeMode, NzbParameterList* parameters, bool addTop, bool addPaused, NzbInfo* urlInfo, const char* fileName, const char* buffer, int bufSize, int* nzbId) { bool nzb = false; BString<1024> tempFileName; if (fileName) { tempFileName = fileName; } else { int num = ++m_idGen; while (tempFileName.Empty() || FileSystem::FileExists(tempFileName)) { tempFileName.Format("%s%cnzb-%i.tmp", g_Options->GetTempDir(), PATH_SEPARATOR, num); num++; } if (!FileSystem::SaveBufferIntoFile(tempFileName, buffer, bufSize)) { error("Could not create file %s", *tempFileName); return asFailed; } // "buffer" doesn't end with nullptr, therefore we can't search in it with "strstr" BString<1024> buf; buf.Set(buffer, bufSize); nzb = !strncmp(buf, " scanFileName("%s%c%s", g_Options->GetNzbDir(), PATH_SEPARATOR, *validNzbName); char *ext = strrchr(validNzbName, '.'); if (ext) { *ext = '\0'; ext++; } int num = 2; while (FileSystem::FileExists(scanFileName)) { if (ext) { scanFileName.Format("%s%c%s_%i.%s", g_Options->GetNzbDir(), PATH_SEPARATOR, *validNzbName, num, ext); } else { scanFileName.Format("%s%c%s_%i", g_Options->GetNzbDir(), PATH_SEPARATOR, *validNzbName, num); } num++; } EAddStatus addStatus; { Guard guard(m_scanMutex); if (!FileSystem::MoveFile(tempFileName, scanFileName)) { error("Could not move file %s to %s: %s", *tempFileName, *scanFileName, *FileSystem::GetLastErrorMessage()); FileSystem::DeleteFile(tempFileName); return asFailed; } CString useCategory = category ? category : ""; Options::Category* categoryObj = g_Options->FindCategory(useCategory, true); if (categoryObj && strcmp(useCategory, categoryObj->GetName())) { useCategory = categoryObj->GetName(); detail("Category %s matched to %s for %s", category, *useCategory, nzbName); } addStatus = asSkipped; m_queueList.emplace_back(scanFileName, nzbName, useCategory, priority, dupeKey, dupeScore, dupeMode, parameters, addTop, addPaused, urlInfo, &addStatus, nzbId); } ScanNzbDir(true); return addStatus; } nzbget-19.1/daemon/nserv/0000755000175000017500000000000013130203062015137 5ustar andreasandreasnzbget-19.1/daemon/nserv/NServMain.h0000644000175000017500000000157113130203062017156 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #ifndef NSERVMAIN_H #define NSERVMAIN_H int NServMain(int argc, char * argv[]); #endif nzbget-19.1/daemon/nserv/NServFrontend.cpp0000644000175000017500000000633213130203062020404 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #include "nzbget.h" #include "NServFrontend.h" #include "Util.h" NServFrontend::NServFrontend() { #ifdef WIN32 m_console = GetStdHandle(STD_OUTPUT_HANDLE); #endif } void NServFrontend::Run() { while (!IsStopped()) { Update(); usleep(100 * 1000); } // Printing the last messages Update(); } void NServFrontend::Update() { BeforePrint(); { GuardedMessageList messages = g_Log->GuardMessages(); if (!messages->empty()) { Message& firstMessage = messages->front(); int start = m_neededLogFirstId - firstMessage.GetId() + 1; if (start < 0) { PrintSkip(); start = 0; } for (uint32 i = (uint32)start; i < messages->size(); i++) { PrintMessage(messages->at(i)); m_neededLogFirstId = messages->at(i).GetId(); } } } fflush(stdout); } void NServFrontend::BeforePrint() { if (m_needGoBack) { // go back one line #ifdef WIN32 CONSOLE_SCREEN_BUFFER_INFO BufInfo; GetConsoleScreenBufferInfo(m_console, &BufInfo); BufInfo.dwCursorPosition.Y--; SetConsoleCursorPosition(m_console, BufInfo.dwCursorPosition); #else printf("\r\033[1A"); #endif m_needGoBack = false; } } void NServFrontend::PrintMessage(Message& message) { #ifdef WIN32 switch (message.GetKind()) { case Message::mkDebug: SetConsoleTextAttribute(m_console, 8); printf("[DEBUG] "); break; case Message::mkError: SetConsoleTextAttribute(m_console, 4); printf("[ERROR] "); break; case Message::mkWarning: SetConsoleTextAttribute(m_console, 5); printf("[WARNING]"); break; case Message::mkInfo: SetConsoleTextAttribute(m_console, 2); printf("[INFO] "); break; case Message::mkDetail: SetConsoleTextAttribute(m_console, 2); printf("[DETAIL]"); break; } SetConsoleTextAttribute(m_console, 7); CString msg = message.GetText(); CharToOem(msg, msg); printf(" %s\n", *msg); #else const char* msg = message.GetText(); switch (message.GetKind()) { case Message::mkDebug: printf("[DEBUG] %s\033[K\n", msg); break; case Message::mkError: printf("\033[31m[ERROR]\033[39m %s\033[K\n", msg); break; case Message::mkWarning: printf("\033[35m[WARNING]\033[39m %s\033[K\n", msg); break; case Message::mkInfo: printf("\033[32m[INFO]\033[39m %s\033[K\n", msg); break; case Message::mkDetail: printf("\033[32m[DETAIL]\033[39m %s\033[K\n", msg); break; } #endif } void NServFrontend::PrintSkip() { #ifdef WIN32 printf(".....\n"); #else printf(".....\033[K\n"); #endif } nzbget-19.1/daemon/nserv/NntpServer.h0000644000175000017500000000264313130203062017423 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #ifndef NNTPSERVER_H #define NNTPSERVER_H #include "Thread.h" #include "Connection.h" class NntpServer : public Thread { public: NntpServer(int id, const char* host, int port, const char* secureCert, const char* secureKey, const char* dataDir, const char* cacheDir) : m_id(id), m_host(host), m_port(port), m_secureCert(secureCert), m_secureKey(secureKey), m_dataDir(dataDir), m_cacheDir(cacheDir) {} virtual void Run(); virtual void Stop(); private: int m_id; CString m_host; int m_port; CString m_dataDir; CString m_cacheDir; CString m_secureCert; CString m_secureKey; std::unique_ptr m_connection; }; #endif nzbget-19.1/daemon/nserv/YEncoder.cpp0000644000175000017500000000643413130203062017362 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #include "nzbget.h" #include "YEncoder.h" #include "Util.h" #include "FileSystem.h" #include "Log.h" bool YEncoder::OpenFile(CString& errmsg) { if (m_size < 0) { errmsg = "Invalid segment size"; return false; } if (!m_diskfile.Open(m_filename, DiskFile::omRead) || !m_diskfile.Seek(0, DiskFile::soEnd)) { errmsg = "File not found"; return false; } m_fileSize = m_diskfile.Position(); if (m_size == 0) { m_size = (int)(m_fileSize - m_offset + 1); } if (m_fileSize < m_offset + m_size) { errmsg = "Invalid segment size"; return false; } if (!m_diskfile.Seek(m_offset)) { errmsg = "Invalid segment offset"; return false; } return true; } void YEncoder::WriteSegment() { StringBuilder outbuf; outbuf.Reserve(std::max(2048, std::min((int)(m_size * 1.1), 16 * 1024 * 1024))); outbuf.Append(CString::FormatStr("=ybegin part=%i line=128 size=%lli name=%s\r\n", m_part, (long long)m_fileSize, FileSystem::BaseFileName(m_filename))); outbuf.Append(CString::FormatStr("=ypart begin=%lli end=%lli\r\n", (long long)(m_offset + 1), (long long)(m_offset + m_size))); uint32 crc = 0xFFFFFFFF; CharBuffer inbuf(std::min(m_size, 16 * 1024 * 1024)); int lnsz = 0; char* out = (char*)outbuf + outbuf.Length(); while (m_diskfile.Position() < m_offset + m_size) { int64 needBytes = std::min((int64)inbuf.Size(), m_offset + m_size - m_diskfile.Position()); int64 readBytes = m_diskfile.Read(inbuf, needBytes); bool lastblock = m_diskfile.Position() == m_offset + m_size; if (readBytes == 0) { return; // error; } crc = Util::Crc32m(crc, (uchar*)(const char*)inbuf, (int)readBytes); char* in = inbuf; while (readBytes > 0) { char ch = *in++; readBytes--; ch = (char)(((uchar)(ch) + 42) % 256); if (ch == '\0' || ch == '\n' || ch == '\r' || ch == '=' || ch == ' ' || ch == '\t') { *out++ = '='; lnsz++; ch = (char)(((uchar)ch + 64) % 256); } if (ch == '.' && lnsz == 0) { *out++ = '.'; lnsz++; } *out++ = ch; lnsz++; if (lnsz >= 128 || (readBytes == 0 && lastblock)) { *out++ = '\r'; *out++ = '\n'; lnsz += 2; outbuf.SetLength(outbuf.Length() + lnsz); if (outbuf.Length() > outbuf.Capacity() - 200) { m_writeFunc(outbuf, outbuf.Length()); outbuf.SetLength(0); out = (char*)outbuf; } lnsz = 0; } } } crc ^= 0xFFFFFFFF; m_diskfile.Close(); outbuf.Append(CString::FormatStr("=yend size=%i part=0 pcrc32=%08x\r\n", m_size, (unsigned int)crc)); m_writeFunc(outbuf, outbuf.Length()); } nzbget-19.1/daemon/nserv/NServMain.cpp0000644000175000017500000001331013130203062017503 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #include "nzbget.h" #include "Thread.h" #include "Connection.h" #include "Log.h" #include "Util.h" #include "FileSystem.h" #include "NServFrontend.h" #include "NntpServer.h" #include "NzbGenerator.h" #include "Options.h" struct NServOpts { CString dataDir; CString cacheDir; CString bindAddress; int firstPort; int instances; CString logFile; CString secureCert; CString secureKey; BString<1024> logOpt; bool generateNzb; int segmentSize; bool quit; NServOpts(int argc, char* argv[], Options::CmdOptList& cmdOpts); }; void NServPrintUsage(const char* com); int NServMain(int argc, char* argv[]) { Log log; info("NServ %s (Test NNTP server)", Util::VersionRevision()); Options::CmdOptList cmdOpts; NServOpts opts(argc, argv, cmdOpts); if (opts.dataDir.Empty()) { NServPrintUsage(argv[0]); return 1; } if (!FileSystem::DirectoryExists(opts.dataDir)) { // dataDir does not exist. Let's find out a bit more, and report: if (FileSystem::FileExists(opts.dataDir)) { error("Specified data-dir %s is not a directory, but a file", *opts.dataDir ); } else { error("Specified data-dir %s does not exist", *opts.dataDir ); } } Options options(&cmdOpts, nullptr); log.InitOptions(); Thread::Init(); Connection::Init(); #ifndef DISABLE_TLS TlsSocket::Init(); #endif NServFrontend frontend; frontend.Start(); if (opts.generateNzb) { NzbGenerator gen(opts.dataDir, opts.segmentSize); gen.Execute(); if (opts.quit) { return 0; } } CString errmsg; if (opts.cacheDir && !FileSystem::ForceDirectories(opts.cacheDir, errmsg)) { error("Could not create directory %s: %s", *opts.cacheDir, *errmsg); } std::vector> instances; for (int i = 0; i < opts.instances; i++) { instances.emplace_back(std::make_unique(i + 1, opts.bindAddress, opts.firstPort + i, opts.secureCert, opts.secureKey, opts.dataDir, opts.cacheDir)); instances.back()->Start(); } info("Press Ctrl+C to quit"); while (getchar()) usleep(1000*200); for (std::unique_ptr& serv: instances) { serv->Stop(); } frontend.Stop(); bool hasRunning = false; do { hasRunning = frontend.IsRunning(); for (std::unique_ptr& serv : instances) { hasRunning |= serv->IsRunning(); } usleep(50 * 1000); } while (hasRunning); return 0; } void NServPrintUsage(const char* com) { printf("Usage:\n" " %s --nserv -d [optional switches] \n" " -d - directory whose files will be served\n" " Optional switches:\n" " -c - directory to store encoded articles\n" " -l - write into log-file (disabled by default)\n" " -i - number of server instances (default is 1)\n" " -b
- ip address to bind to (default is 0.0.0.0)\n" " -p - port number for the first instance (default is 6791)\n" " -s - paths to SSL certificate and key files\n" " -v - verbosity level 0..3 (default is 2)\n" " -z - generate nzbs for all files in data-dir (size in bytes)\n" " -q - quit after generating nzbs (in combination with -z)\n" , FileSystem::BaseFileName(com)); } NServOpts::NServOpts(int argc, char* argv[], Options::CmdOptList& cmdOpts) { instances = 1; bindAddress = "0.0.0.0"; firstPort = 6791; generateNzb = false; segmentSize = 500000; quit = false; int verbosity = 2; char short_options[] = "b:c:d:l:p:i:s:v:z:q"; optind = 2; while (true) { int c = getopt(argc, argv, short_options); if (c == -1) break; switch (c) { case 'd': dataDir = optind > argc ? nullptr : argv[optind - 1]; break; case 'c': cacheDir = optind > argc ? nullptr : argv[optind - 1]; break; case 'l': logFile = optind > argc ? nullptr : argv[optind - 1]; break; case 'b': bindAddress= optind > argc ? "0.0.0.0" : argv[optind - 1]; break; case 'p': firstPort = atoi(optind > argc ? "6791" : argv[optind - 1]); break; case 's': secureCert = optind > argc ? nullptr : argv[optind - 1]; optind++; secureKey = optind > argc ? nullptr : argv[optind - 1]; break; case 'i': instances = atoi(optind > argc ? "1" : argv[optind - 1]); break; case 'v': verbosity = atoi(optind > argc ? "1" : argv[optind - 1]); break; case 'z': generateNzb = true; segmentSize = atoi(optind > argc ? "500000" : argv[optind - 1]); break; case 'q': quit = true; break; } } if (logFile.Empty()) { cmdOpts.push_back("WriteLog=none"); } else { cmdOpts.push_back("WriteLog=append"); logOpt.Format("LogFile=%s", *logFile); cmdOpts.push_back(logOpt); } if (verbosity < 1) { cmdOpts.push_back("InfoTarget=none"); cmdOpts.push_back("WarningTarget=none"); cmdOpts.push_back("ErrorTarget=none"); } if (verbosity < 2) { cmdOpts.push_back("DetailTarget=none"); } if (verbosity > 2) { cmdOpts.push_back("DebugTarget=both"); } } nzbget-19.1/daemon/nserv/YEncoder.h0000644000175000017500000000253113130203062017021 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #ifndef YENCODER_H #define YENCODER_H #include "NString.h" #include "FileSystem.h" class YEncoder { public: typedef std::function WriteFunc; YEncoder(const char* filename, int part, int64 offset, int size, WriteFunc writeFunc) : m_filename(filename), m_part(part), m_offset(offset), m_size(size), m_writeFunc(writeFunc) {}; bool OpenFile(CString& errmsg); void WriteSegment(); private: DiskFile m_diskfile; CString m_filename; int m_part; int64 m_offset; int m_size; int64 m_fileSize; WriteFunc m_writeFunc; }; #endif nzbget-19.1/daemon/nserv/NzbGenerator.cpp0000644000175000017500000000724313130203062020251 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #include "nzbget.h" #include "NzbGenerator.h" #include "Util.h" #include "FileSystem.h" #include "Log.h" void NzbGenerator::Execute() { info("Generating nzbs for %s", *m_dataDir); DirBrowser dir(m_dataDir); while (const char* filename = dir.Next()) { BString<1024> fullFilename("%s%c%s", *m_dataDir, PATH_SEPARATOR, filename); int len = strlen(filename); if (len > 4 && !strcasecmp(filename + len - 4, ".nzb")) { // skip nzb-files continue; } GenerateNzb(fullFilename); } info("Nzb generation finished"); } void NzbGenerator::GenerateNzb(const char* path) { BString<1024> nzbFilename("%s%c%s.nzb", *m_dataDir, PATH_SEPARATOR, FileSystem::BaseFileName(path)); if (FileSystem::FileExists(nzbFilename)) { return; } info("Generating nzb for %s", FileSystem::BaseFileName(path)); DiskFile outfile; if (!outfile.Open(nzbFilename, DiskFile::omWrite)) { error("Could not create file %s", *nzbFilename); return; } outfile.Print("\n"); outfile.Print("\n"); outfile.Print("\n"); bool isDir = FileSystem::DirectoryExists(path); if (isDir) { AppendDir(outfile, path); } else { AppendFile(outfile, path, nullptr); } outfile.Print("\n"); outfile.Close(); } void NzbGenerator::AppendDir(DiskFile& outfile, const char* path) { DirBrowser dir(path); while (const char* filename = dir.Next()) { BString<1024> fullFilename("%s%c%s", path, PATH_SEPARATOR, filename); bool isDir = FileSystem::DirectoryExists(fullFilename); if (!isDir) { AppendFile(outfile, fullFilename, FileSystem::BaseFileName(path)); } } } void NzbGenerator::AppendFile(DiskFile& outfile, const char* filename, const char* relativePath) { detail("Processing %s", FileSystem::BaseFileName(filename)); int64 fileSize = FileSystem::FileSize(filename); time_t timestamp = Util::CurrentTime(); int segmentCount = (int)((fileSize + m_segmentSize - 1) / m_segmentSize); outfile.Print("\n", (int)timestamp, FileSystem::BaseFileName(filename), segmentCount); outfile.Print("\n"); outfile.Print("alt.binaries.test\n"); outfile.Print("\n"); outfile.Print("\n"); int64 segOffset = 0; for (int segno = 1; segno <= segmentCount; segno++) { int segSize = (int)(segOffset + m_segmentSize < fileSize ? m_segmentSize : fileSize - segOffset); outfile.Print("%s%s%s?%i=%lli:%i\n", m_segmentSize, segno, relativePath ? relativePath : "", relativePath ? "/" : "", FileSystem::BaseFileName(filename), segno, (long long)segOffset, (int)segSize); segOffset += segSize; } outfile.Print("\n"); outfile.Print("\n"); } nzbget-19.1/daemon/nserv/NntpServer.cpp0000644000175000017500000002017413130203062017755 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #include "nzbget.h" #include "NntpServer.h" #include "Log.h" #include "Util.h" #include "YEncoder.h" class NntpProcessor : public Thread { public: NntpProcessor(int id, int serverId, const char* dataDir, const char* cacheDir, const char* secureCert, const char* secureKey) : m_id(id), m_serverId(serverId), m_dataDir(dataDir), m_cacheDir(cacheDir), m_secureCert(secureCert), m_secureKey(secureKey) {} ~NntpProcessor() { m_connection->Disconnect(); } virtual void Run(); void SetConnection(std::unique_ptr&& connection) { m_connection = std::move(connection); } private: int m_id; int m_serverId; std::unique_ptr m_connection; const char* m_dataDir; const char* m_cacheDir; const char* m_secureCert; const char* m_secureKey; const char* m_messageid; CString m_filename; int m_part; int64 m_offset; int m_size; bool m_sendHeaders; void ServArticle(); void SendSegment(); bool ServerInList(const char* servList); }; void NntpServer::Run() { debug("Entering NntpServer-loop"); info("Listening on port %i", m_port); int num = 1; while (!IsStopped()) { bool bind = true; if (!m_connection) { m_connection = std::make_unique(m_host, m_port, m_secureCert); m_connection->SetTimeout(10); m_connection->SetSuppressErrors(false); bind = m_connection->Bind(); } // Accept connections and store the new Connection std::unique_ptr acceptedConnection; if (bind) { acceptedConnection = m_connection->Accept(); } if (!bind || !acceptedConnection) { // Server could not bind or accept connection, waiting 1/2 sec and try again if (IsStopped()) { break; } m_connection.reset(); usleep(500 * 1000); continue; } NntpProcessor* commandThread = new NntpProcessor(num++, m_id, m_dataDir, m_cacheDir, m_secureCert, m_secureKey); commandThread->SetAutoDestroy(true); commandThread->SetConnection(std::move(acceptedConnection)); commandThread->Start(); } if (m_connection) { m_connection->Disconnect(); } debug("Exiting NntpServer-loop"); } void NntpServer::Stop() { Thread::Stop(); if (m_connection) { m_connection->SetSuppressErrors(true); m_connection->Cancel(); #ifdef WIN32 m_connection->Disconnect(); #endif } } void NntpProcessor::Run() { m_connection->SetSuppressErrors(false); #ifndef DISABLE_TLS if (m_secureCert && !m_connection->StartTls(false, m_secureCert, m_secureKey)) { error("Could not establish secure connection to nntp-client: Start TLS failed"); return; } #endif m_connection->WriteLine("200 Welcome (NServ)\r\n"); CharBuffer buf(1024); int bytesRead = 0; while (CString line = m_connection->ReadLine(buf, 1024, &bytesRead)) { line.TrimRight(); detail("[%i] Received: %s", m_id, *line); if (!strncasecmp(line, "ARTICLE ", 8)) { m_messageid = line + 8; m_sendHeaders = true; ServArticle(); } else if (!strncasecmp(line, "BODY ", 5)) { m_messageid = line + 5; m_sendHeaders = false; ServArticle(); } else if (!strncasecmp(line, "GROUP ", 6)) { m_connection->WriteLine(CString::FormatStr("211 0 0 0 %s\r\n", line + 7)); } else if (!strncasecmp(line, "AUTHINFO ", 9)) { m_connection->WriteLine("281 Authentication accepted\r\n"); } else if (!strcasecmp(line, "QUIT")) { detail("[%i] Closing connection", m_id); m_connection->WriteLine("205 Connection closing\r\n"); break; } else { warn("[%i] Unknown command: %s", m_id, *line); m_connection->WriteLine("500 Unknown command\r\n"); } } m_connection->SetGracefull(true); m_connection->Disconnect(); } /* Message-id format: where: xxx - part number (integer) xxx - offset from which to read the files (integer) yyy - size of file block to return (integer) 1,2,3 - list of server ids, which have the article (optional), if the list is given and current server is not in the list the "article not found"-error is returned. Examples: - return first 50000 bytes starting from beginning - return 50000 bytes starting from offset 50000 - article is missing on server 1 */ void NntpProcessor::ServArticle() { detail("[%i] Serving: %s", m_id, m_messageid); bool ok = false; const char* from = strchr(m_messageid, '?'); const char* off = strchr(m_messageid, '='); const char* to = strchr(m_messageid, ':'); const char* end = strchr(m_messageid, '>'); const char* serv = strchr(m_messageid, '!'); if (from && off && to && end) { m_filename.Set(m_messageid + 1, from - m_messageid - 1); m_part = atoi(from + 1); m_offset = atoll(off + 1); m_size = atoi(to + 1); ok = !serv || ServerInList(serv + 1); if (ok) { SendSegment(); return; } if (!ok) { m_connection->WriteLine("430 No Such Article Found\r\n"); } } else { m_connection->WriteLine("430 No Such Article Found (invalid message id format)\r\n"); } } bool NntpProcessor::ServerInList(const char* servList) { Tokenizer tok(servList, ","); while (const char* servid = tok.Next()) { if (atoi(servid) == m_serverId) { return true; } } return false; } void NntpProcessor::SendSegment() { detail("[%i] Sending segment %s (%i=%lli:%i)", m_id, *m_filename, m_part, (long long)m_offset, m_size); BString<1024> fullFilename("%s/%s", m_dataDir, *m_filename); BString<1024> cacheFileDir("%s/%s", m_cacheDir, *m_filename); BString<1024> cacheFileName("%i=%lli-%i", m_part, (long long)m_offset, m_size); BString<1024> cacheFullFilename("%s/%s", *cacheFileDir, *cacheFileName); DiskFile cacheFile; bool readCache = m_cacheDir && cacheFile.Open(cacheFullFilename, DiskFile::omRead); bool writeCache = m_cacheDir && !readCache; CString errmsg; if (writeCache && !FileSystem::ForceDirectories(cacheFileDir, errmsg)) { error("Could not create directory %s: %s", *cacheFileDir, *errmsg); } if (writeCache && !cacheFile.Open(cacheFullFilename, DiskFile::omWrite)) { error("Could not create file %s: %s", *cacheFullFilename, *FileSystem::GetLastErrorMessage()); } if (!readCache && !FileSystem::FileExists(fullFilename)) { m_connection->WriteLine(CString::FormatStr("430 Article not found\r\n")); return; } YEncoder encoder(fullFilename, m_part, m_offset, m_size, [con = m_connection.get(), writeCache, &cacheFile](const char* buf, int size) { if (writeCache) { cacheFile.Write(buf, size); } con->Send(buf, size); }); if (!readCache && !encoder.OpenFile(errmsg)) { m_connection->WriteLine(CString::FormatStr("403 %s\r\n", *errmsg)); return; } m_connection->WriteLine(CString::FormatStr("%i, 0 %s\r\n", m_sendHeaders ? 222 : 220, m_messageid)); if (m_sendHeaders) { m_connection->WriteLine(CString::FormatStr("Message-ID: %s\r\n", m_messageid)); m_connection->WriteLine(CString::FormatStr("Subject: \"%s\"\r\n", FileSystem::BaseFileName(m_filename))); m_connection->WriteLine("\r\n"); } if (readCache) { cacheFile.Seek(0, DiskFile::soEnd); int size = (int)cacheFile.Position(); CharBuffer buf(size); cacheFile.Seek(0); if (cacheFile.Read((char*)buf, size) != size) { error("Could not read file %s: %s", *cacheFullFilename, *FileSystem::GetLastErrorMessage()); } m_connection->Send(buf, size); } else { encoder.WriteSegment(); } m_connection->WriteLine(".\r\n"); } nzbget-19.1/daemon/nserv/NServFrontend.h0000644000175000017500000000227413130203062020052 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #ifndef NSERVFRONTEND_H #define NSERVFRONTEND_H #include "Thread.h" #include "Log.h" class NServFrontend : public Thread { public: NServFrontend(); private: uint32 m_neededLogEntries = 0; uint32 m_neededLogFirstId = 0; bool m_needGoBack = false; #ifdef WIN32 HANDLE m_console; #endif void Run(); void Update(); void BeforePrint(); void PrintMessage(Message& message); void PrintSkip(); }; #endif nzbget-19.1/daemon/nserv/NzbGenerator.h0000644000175000017500000000240413130203062017710 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #ifndef NZBGENERATOR_H #define NZBGENERATOR_H #include "NString.h" #include "FileSystem.h" class NzbGenerator { public: NzbGenerator(const char* dataDir, int segmentSize) : m_dataDir(dataDir), m_segmentSize(segmentSize) {}; void Execute(); private: CString m_dataDir; int m_segmentSize; void GenerateNzb(const char* path); void AppendFile(DiskFile& outfile, const char* filename, const char* relativePath); void AppendDir(DiskFile& outfile, const char* path); }; #endif nzbget-19.1/daemon/util/0000755000175000017500000000000013130203062014757 5ustar andreasandreasnzbget-19.1/daemon/util/Log.h0000644000175000017500000000657513130203062015666 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef LOG_H #define LOG_H #include "NString.h" #include "Thread.h" void error(const char* msg, ...) PRINTF_SYNTAX(1); void warn(const char* msg, ...) PRINTF_SYNTAX(1); void info(const char* msg, ...) PRINTF_SYNTAX(1); void detail(const char* msg, ...) PRINTF_SYNTAX(1); #ifdef DEBUG #ifdef HAVE_VARIADIC_MACROS void debug(const char* filename, const char* funcname, int lineNr, const char* msg, ...) PRINTF_SYNTAX(4); #else void debug(const char* msg, ...) PRINTF_SYNTAX(1); #endif #endif class Message { public: enum EKind { mkInfo, mkWarning, mkError, mkDebug, mkDetail }; Message(uint32 id, EKind kind, time_t time, const char* text) : m_id(id), m_kind(kind), m_time(time), m_text(text) {} uint32 GetId() { return m_id; } EKind GetKind() { return m_kind; } time_t GetTime() { return m_time; } const char* GetText() { return m_text; } private: uint32 m_id; EKind m_kind; time_t m_time; CString m_text; friend class Log; }; typedef std::deque MessageList; typedef GuardedPtr GuardedMessageList; class Debuggable; class Log { public: Log(); ~Log(); GuardedMessageList GuardMessages() { return GuardedMessageList(&m_messages, &m_logMutex); } void Clear(); void ResetLog(); void InitOptions(); void RegisterDebuggable(Debuggable* debuggable); void UnregisterDebuggable(Debuggable* debuggable); void LogDebugInfo(); private: typedef std::list Debuggables; Mutex m_logMutex; MessageList m_messages; Debuggables m_debuggables; Mutex m_debugMutex; CString m_logFilename; uint32 m_idGen = 0; time_t m_lastWritten = 0; bool m_optInit = false; #ifdef DEBUG bool m_extraDebug; #endif void Filelog(const char* msg, ...) PRINTF_SYNTAX(2); void AddMessage(Message::EKind kind, const char* text); void RotateLog(); friend void error(const char* msg, ...); friend void warn(const char* msg, ...); friend void info(const char* msg, ...); friend void detail(const char* msg, ...); #ifdef DEBUG #ifdef HAVE_VARIADIC_MACROS friend void debug(const char* filename, const char* funcname, int lineNr, const char* msg, ...); #else friend void debug(const char* msg, ...); #endif #endif }; #ifdef DEBUG #ifdef HAVE_VARIADIC_MACROS #define debug(...) debug(__FILE__, FUNCTION_MACRO_NAME, __LINE__, __VA_ARGS__) #endif #else #define debug(...) do { } while(0) #endif extern Log* g_Log; class Debuggable { public: Debuggable() { g_Log->RegisterDebuggable(this); } virtual ~Debuggable() { g_Log->UnregisterDebuggable(this); } protected: virtual void LogDebugInfo() = 0; friend class Log; }; #endif nzbget-19.1/daemon/util/Observer.h0000644000175000017500000000230113130203062016713 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef OBSERVER_H #define OBSERVER_H class Observer; class Subject { public: void Attach(Observer* observer); void Detach(Observer* observer); void Notify(void* aspect); private: std::vector m_observers; }; class Observer { protected: virtual void Update(Subject* caller, void* aspect) = 0; friend class Subject; }; #endif nzbget-19.1/daemon/util/Script.cpp0000644000175000017500000004743413130203062016743 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #include "nzbget.h" #include "Script.h" #include "Log.h" #include "Util.h" #include "FileSystem.h" #include "Options.h" // System global variable holding environments variables extern char** environ; extern char* (*g_EnvironmentVariables)[]; ScriptController::RunningScripts ScriptController::m_runningScripts; Mutex ScriptController::m_runningMutex; const int FORK_ERROR_EXIT_CODE = 254; #ifdef CHILD_WATCHDOG /** * Sometimes the forked child process doesn't start properly and hangs * just during the starting. I didn't find any explanation about what * could cause that problem except of a general advice, that * "a forking in a multithread application is not recommended". * * Workaround: * 1) child process prints a line into stdout directly after the start; * 2) parent process waits for a line for 60 seconds. If it didn't receive it * the child process is assumed to be hanging and will be killed. Another attempt * will be made. */ class ChildWatchDog : public Thread { public: void SetProcessId(pid_t processId) { m_processId = processId; } void SetInfoName(const char* infoName) { m_infoName = infoName; } protected: virtual void Run(); private: pid_t m_processId; CString m_infoName; }; void ChildWatchDog::Run() { static const int WAIT_SECONDS = 60; time_t start = Util::CurrentTime(); while (!IsStopped() && (Util::CurrentTime() - start) < WAIT_SECONDS) { usleep(10 * 1000); } if (!IsStopped()) { info("Restarting hanging child process for %s", *m_infoName); kill(m_processId, SIGKILL); } } #endif void EnvironmentStrings::Clear() { m_strings.clear(); } void EnvironmentStrings::InitFromCurrentProcess() { for (int i = 0; (*g_EnvironmentVariables)[i]; i++) { char* var = (*g_EnvironmentVariables)[i]; // Ignore all env vars set by NZBGet. // This is to avoid the passing of env vars after program update (when NZBGet is // started from a script which was started by a previous instance of NZBGet). // Format: NZBXX_YYYY (XX are any two characters, YYYY are any number of any characters). if (!(!strncmp(var, "NZB", 3) && strlen(var) > 5 && var[5] == '_')) { Append(var); } } } void EnvironmentStrings::Append(const char* envstr) { m_strings.emplace_back(envstr); } void EnvironmentStrings::Append(CString&& envstr) { m_strings.push_back(std::move(envstr)); } #ifdef WIN32 /* * Returns environment block in format suitable for using with CreateProcess. */ std::unique_ptr EnvironmentStrings::GetStrings() { int size = 1; for (CString& var : m_strings) { size += var.Length() + 1; } std::unique_ptr strings = std::make_unique(size * 2); wchar_t* ptr = strings.get(); for (CString& var : m_strings) { WString wstr(var); wcscpy(ptr, wstr); ptr += wstr.Length() + 1; } *ptr = '\0'; return strings; } #else /* * Returns environment block in format suitable for using with execve. */ std::vector EnvironmentStrings::GetStrings() { std::vector strings; strings.reserve(m_strings.size() + 1); std::copy(m_strings.begin(), m_strings.end(), std::back_inserter(strings)); strings.push_back(nullptr); return strings; } #endif ScriptController::ScriptController() { ResetEnv(); Guard guard(m_runningMutex); m_runningScripts.push_back(this); } ScriptController::~ScriptController() { UnregisterRunningScript(); } void ScriptController::UnregisterRunningScript() { Guard guard(m_runningMutex); m_runningScripts.erase(std::remove(m_runningScripts.begin(), m_runningScripts.end(), this), m_runningScripts.end()); } void ScriptController::ResetEnv() { m_environmentStrings.Clear(); m_environmentStrings.InitFromCurrentProcess(); } void ScriptController::SetEnvVar(const char* name, const char* value) { m_environmentStrings.Append(CString::FormatStr("%s=%s", name, value)); } void ScriptController::SetIntEnvVar(const char* name, int value) { BString<1024> strValue("%i", value); SetEnvVar(name, strValue); } /** * If szStripPrefix is not nullptr, only options, whose names start with the prefix * are processed. The prefix is then stripped from the names. * If szStripPrefix is nullptr, all options are processed; without stripping. */ void ScriptController::PrepareEnvOptions(const char* stripPrefix) { int prefixLen = stripPrefix ? strlen(stripPrefix) : 0; for (Options::OptEntry& optEntry : g_Options->GuardOptEntries()) { const char* value = GetOptValue(optEntry.GetName(), optEntry.GetValue()); if (stripPrefix && !strncmp(optEntry.GetName(), stripPrefix, prefixLen) && (int)strlen(optEntry.GetName()) > prefixLen) { SetEnvVarSpecial("NZBPO", optEntry.GetName() + prefixLen, value); } else if (!stripPrefix) { SetEnvVarSpecial("NZBOP", optEntry.GetName(), value); } } } void ScriptController::SetEnvVarSpecial(const char* prefix, const char* name, const char* value) { BString<1024> varname("%s_%s", prefix, name); // Original name SetEnvVar(varname, value); BString<1024> normVarname = *varname; // Replace special characters with "_" and convert to upper case for (char* ptr = normVarname; *ptr; ptr++) { if (strchr(".:*!\"$%&/()=`+~#'{}[]@- ", *ptr)) *ptr = '_'; *ptr = toupper(*ptr); } // Another env var with normalized name (replaced special chars and converted to upper case) if (strcmp(varname, normVarname)) { SetEnvVar(normVarname, value); } } void ScriptController::PrepareArgs() { if (m_args.size() == 1 && !Util::EmptyStr(g_Options->GetShellOverride())) { const char* extension = strrchr(m_args[0], '.'); Tokenizer tok(g_Options->GetShellOverride(), ",;"); while (CString shellover = tok.Next()) { char* shellcmd = strchr(shellover, '='); if (shellcmd) { *shellcmd = '\0'; shellcmd++; if (!strcasecmp(extension, shellover)) { debug("Using shell override for %s: %s", extension, shellcmd); m_args.emplace(m_args.begin(), shellcmd); break; } } } } #ifdef WIN32 *m_cmdLine = '\0'; if (m_args.size() == 1) { // Special support for script languages: // automatically find the app registered for this extension and run it const char* extension = strrchr(m_args[0], '.'); if (extension && strcasecmp(extension, ".exe") && strcasecmp(extension, ".bat") && strcasecmp(extension, ".cmd")) { debug("Looking for associated program for %s", extension); char command[512]; int bufLen = 512 - 1; if (Util::RegReadStr(HKEY_CLASSES_ROOT, extension, nullptr, command, &bufLen)) { command[bufLen] = '\0'; debug("Extension: %s", command); bufLen = 512 - 1; if (Util::RegReadStr(HKEY_CLASSES_ROOT, BString<1024>("%s\\shell\\open\\command", command), nullptr, command, &bufLen)) { command[bufLen] = '\0'; debug("Command: %s", command); DWORD_PTR args[] = {(DWORD_PTR)*m_args[0], (DWORD_PTR)0}; if (FormatMessage(FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ARGUMENT_ARRAY, command, 0, 0, m_cmdLine, sizeof(m_cmdLine), (va_list*)args)) { Util::TrimRight(Util::ReduceStr(m_cmdLine, "*", "")); debug("CmdLine: %s", m_cmdLine); return; } } } warn("Could not find associated program for %s. Trying to execute %s directly", extension, FileSystem::BaseFileName(m_args[0])); } } #endif } int ScriptController::Execute() { PrepareEnvOptions(nullptr); PrepareArgs(); m_completed = false; int exitCode = 0; #ifdef CHILD_WATCHDOG bool childConfirmed = false; while (!childConfirmed && !m_terminated) { #endif int pipein = -1, pipeout = -1; StartProcess(&pipein, &pipeout); if (pipein == -1) { m_completed = true; return -1; } // open the read end m_readpipe = fdopen(pipein, "r"); if (!m_readpipe) { PrintMessage(Message::mkError, "Could not open read pipe to %s", *m_infoName); close(pipein); close(pipeout); m_completed = true; return -1; } m_writepipe = 0; if (m_needWrite) { // open the write end m_writepipe = fdopen(pipeout, "w"); if (!m_writepipe) { PrintMessage(Message::mkError, "Could not open write pipe to %s", *m_infoName); close(pipein); close(pipeout); m_completed = true; return -1; } } #ifdef CHILD_WATCHDOG debug("Creating child watchdog"); ChildWatchDog watchDog; watchDog.SetAutoDestroy(false); watchDog.SetProcessId(m_processId); watchDog.SetInfoName(m_infoName); watchDog.Start(); #endif CharBuffer buf(1024 * 10); debug("Entering pipe-loop"); bool firstLine = true; bool startError = false; while (!m_terminated && !m_detached && !feof(m_readpipe)) { if (ReadLine(buf, buf.Size(), m_readpipe) && m_readpipe) { #ifdef CHILD_WATCHDOG if (!childConfirmed) { childConfirmed = true; watchDog.Stop(); debug("Child confirmed"); continue; } #endif if (firstLine && !strncmp(buf, "[ERROR] Could not start ", 24)) { startError = true; } ProcessOutput(buf); firstLine = false; } } debug("Exited pipe-loop"); #ifdef CHILD_WATCHDOG debug("Destroying WatchDog"); if (!childConfirmed) { watchDog.Stop(); } while (watchDog.IsRunning()) { usleep(5 * 1000); } #endif if (m_readpipe) { fclose(m_readpipe); } if (m_writepipe) { fclose(m_writepipe); } if (m_terminated && m_infoName) { warn("Interrupted %s", *m_infoName); } exitCode = 0; if (!m_detached) { exitCode = WaitProcess(); #ifndef WIN32 if (exitCode == FORK_ERROR_EXIT_CODE && startError) { exitCode = -1; } #endif } #ifdef CHILD_WATCHDOG } // while (!bChildConfirmed && !m_bTerminated) #endif debug("Exit code %i", exitCode); m_completed = true; return exitCode; } #ifdef WIN32 void ScriptController::BuildCommandLine(char* cmdLineBuf, int bufSize) { int usedLen = 0; for (const char* arg : m_args) { int len = strlen(arg); bool endsWithBackslash = arg[len - 1] == '\\'; bool isDirectPath = !strncmp(arg, "\\\\?", 3); snprintf(cmdLineBuf + usedLen, bufSize - usedLen, endsWithBackslash && ! isDirectPath ? "\"%s\\\" " : "\"%s\" ", arg); usedLen += len + 3 + (endsWithBackslash ? 1 : 0); } cmdLineBuf[usedLen < bufSize ? usedLen - 1 : bufSize - 1] = '\0'; } #endif /* * Returns file descriptor of the read-pipe or -1 on error. */ void ScriptController::StartProcess(int* pipein, int* pipeout) { CString workingDir = m_workingDir; if (workingDir.Empty()) { workingDir = FileSystem::GetCurrentDirectory(); } const char* script = m_args[0]; #ifdef WIN32 char* cmdLine = m_cmdLine; char cmdLineBuf[2048]; if (!*m_cmdLine) { BuildCommandLine(cmdLineBuf, sizeof(cmdLineBuf)); cmdLine = cmdLineBuf; } WString wideWorkingDir = FileSystem::UtfPathToWidePath(workingDir); if (strlen(workingDir) > 260 - 14) { GetShortPathNameW(wideWorkingDir, wideWorkingDir, wideWorkingDir.Length() + 1); } // create pipes to write and read data HANDLE readPipe, readProcPipe; HANDLE writePipe = 0, writeProcPipe = 0; SECURITY_ATTRIBUTES securityAttributes = { 0 }; securityAttributes.nLength = sizeof(securityAttributes); securityAttributes.bInheritHandle = TRUE; CreatePipe(&readPipe, &readProcPipe, &securityAttributes, 0); SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0); if (m_needWrite) { CreatePipe(&writeProcPipe, &writePipe, &securityAttributes, 0); SetHandleInformation(writePipe, HANDLE_FLAG_INHERIT, 0); } STARTUPINFOW startupInfo = { 0 }; startupInfo.cb = sizeof(startupInfo); startupInfo.dwFlags = STARTF_USESTDHANDLES; startupInfo.hStdInput = writeProcPipe; startupInfo.hStdOutput = readProcPipe; startupInfo.hStdError = readProcPipe; PROCESS_INFORMATION processInfo = { 0 }; std::unique_ptr environmentStrings = m_environmentStrings.GetStrings(); BOOL ok = CreateProcessW(nullptr, WString(cmdLine), nullptr, nullptr, TRUE, NORMAL_PRIORITY_CLASS | CREATE_NEW_PROCESS_GROUP | CREATE_UNICODE_ENVIRONMENT, environmentStrings.get(), wideWorkingDir, &startupInfo, &processInfo); if (!ok) { DWORD errCode = GetLastError(); char errMsg[255]; errMsg[255 - 1] = '\0'; if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, errCode, 0, errMsg, 255, nullptr)) { PrintMessage(Message::mkError, "Could not start %s: %s", *m_infoName, errMsg); } else { PrintMessage(Message::mkError, "Could not start %s: error %i", *m_infoName, errCode); } if (!FileSystem::FileExists(script)) { PrintMessage(Message::mkError, "Could not find file %s", script); } if (wcslen(wideWorkingDir) > 260) { PrintMessage(Message::mkError, "Could not build short path for %s", workingDir); } CloseHandle(readPipe); CloseHandle(readProcPipe); CloseHandle(writePipe); CloseHandle(writeProcPipe); return; } debug("Child Process-ID: %i", (int)processInfo.dwProcessId); m_processId = processInfo.hProcess; m_dwProcessId = processInfo.dwProcessId; // close unused pipe ends CloseHandle(readProcPipe); CloseHandle(writeProcPipe); *pipein = _open_osfhandle((intptr_t)readPipe, _O_RDONLY); if (m_needWrite) { *pipeout = _open_osfhandle((intptr_t)writePipe, _O_WRONLY); } #else int pin[] = {0, 0}; int pout[] = {0, 0}; // create the pipes if (pipe(pin)) { PrintMessage(Message::mkError, "Could not open read pipe: errno %i", errno); return; } if (m_needWrite && pipe(pout)) { PrintMessage(Message::mkError, "Could not open write pipe: errno %i", errno); close(pin[0]); close(pin[1]); return; } *pipein = pin[0]; *pipeout = pout[1]; std::vector environmentStrings = m_environmentStrings.GetStrings(); char** envdata = environmentStrings.data(); ArgList args; std::copy(m_args.begin(), m_args.end(), std::back_inserter(args)); args.emplace_back(nullptr); char* const* argdata = (char* const*)args.data(); debug("forking"); pid_t pid = fork(); if (pid == -1) { PrintMessage(Message::mkError, "Could not start %s: errno %i", *m_infoName, errno); close(pin[0]); close(pin[1]); if (m_needWrite) { close(pout[0]); close(pout[1]); } return; } else if (pid == 0) { // here goes the second instance // only certain functions may be used here or the program may hang. // for a list of functions see chapter "async-signal-safe functions" in // http://man7.org/linux/man-pages/man7/signal.7.html // create new process group (see Terminate() where it is used) setsid(); // make the pipeout to be the same as stdout and stderr dup2(pin[1], 1); dup2(pin[1], 2); close(pin[0]); close(pin[1]); if (m_needWrite) { // make the pipein to be the same as stdin dup2(pout[0], 0); close(pout[0]); close(pout[1]); } #ifdef CHILD_WATCHDOG write(1, "\n", 1); fsync(1); #endif chdir(workingDir); environ = envdata; execvp(script, argdata); if (errno == EACCES) { write(1, "[WARNING] Fixing permissions for", 32); write(1, script, strlen(script)); write(1, "\n", 1); fsync(1); FileSystem::FixExecPermission(script); execvp(script, argdata); } // NOTE: the text "[ERROR] Could not start " is checked later, // by changing adjust the dependent code below. write(1, "[ERROR] Could not start ", 24); write(1, script, strlen(script)); write(1, ": ", 2); char* errtext = strerror(errno); write(1, errtext, strlen(errtext)); write(1, "\n", 1); fsync(1); _exit(FORK_ERROR_EXIT_CODE); } // continue the first instance debug("forked"); debug("Child Process-ID: %i", (int)pid); m_processId = pid; // close unused pipe ends close(pin[1]); if (m_needWrite) { close(pout[0]); } #endif } int ScriptController::WaitProcess() { #ifdef WIN32 // wait max 60 seconds for terminated processes WaitForSingleObject(m_processId, m_terminated ? 60 * 1000 : INFINITE); DWORD exitCode = 0; GetExitCodeProcess(m_processId, &exitCode); return exitCode; #else int status = 0; waitpid(m_processId, &status, 0); if (WIFEXITED(status)) { int exitCode = WEXITSTATUS(status); return exitCode; } return 0; #endif } void ScriptController::Terminate() { debug("Stopping %s", *m_infoName); m_terminated = true; #ifdef WIN32 BOOL ok = TerminateProcess(m_processId, -1) || m_completed; #else pid_t killId = m_processId; if (getpgid(killId) == killId) { // if the child process has its own group (setsid() was successful), kill the whole group killId = -killId; } bool ok = (killId && kill(killId, SIGKILL) == 0) || m_completed; #endif if (ok) { debug("Terminated %s", *m_infoName); } else { error("Could not terminate %s", *m_infoName); } debug("Stopped %s", *m_infoName); } void ScriptController::TerminateAll() { Guard guard(m_runningMutex); for (ScriptController* script : m_runningScripts) { if (script->m_processId && !script->m_detached) { // send break signal and wait up to 5 seconds for graceful termination if (script->Break()) { time_t curtime = Util::CurrentTime(); while (!script->m_completed && std::abs(curtime - Util::CurrentTime()) <= 10) { usleep(100 * 1000); } } script->Terminate(); } } } bool ScriptController::Break() { debug("Sending break signal to %s", *m_infoName); #ifdef WIN32 BOOL ok = GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, m_dwProcessId); #else bool ok = kill(m_processId, SIGINT) == 0; #endif if (ok) { debug("Sent break signal to %s", *m_infoName); } else { warn("Could not send break signal to %s", *m_infoName); } return ok; } void ScriptController::Detach() { debug("Detaching %s", *m_infoName); m_detached = true; FILE* readpipe = m_readpipe; m_readpipe = nullptr; fclose(readpipe); } void ScriptController::Resume() { m_terminated = false; m_detached = false; m_processId = 0; } bool ScriptController::ReadLine(char* buf, int bufSize, FILE* stream) { return fgets(buf, bufSize, stream); } void ScriptController::ProcessOutput(char* text) { debug("Processing output received from script"); for (char* pend = text + strlen(text) - 1; pend >= text && (*pend == '\n' || *pend == '\r' || *pend == ' '); pend--) *pend = '\0'; if (text[0] == '\0') { // skip empty lines return; } if (!strncmp(text, "[INFO] ", 7)) { PrintMessage(Message::mkInfo, "%s", text + 7); } else if (!strncmp(text, "[WARNING] ", 10)) { PrintMessage(Message::mkWarning, "%s", text + 10); } else if (!strncmp(text, "[ERROR] ", 8)) { PrintMessage(Message::mkError, "%s", text + 8); } else if (!strncmp(text, "[DETAIL] ", 9)) { PrintMessage(Message::mkDetail, "%s", text + 9); } else if (!strncmp(text, "[DEBUG] ", 8)) { PrintMessage(Message::mkDebug, "%s", text + 8); } else { PrintMessage(Message::mkInfo, "%s", text); } debug("Processing output received from script - completed"); } void ScriptController::AddMessage(Message::EKind kind, const char* text) { switch (kind) { case Message::mkDetail: detail("%s", text); break; case Message::mkInfo: info("%s", text); break; case Message::mkWarning: warn("%s", text); break; case Message::mkError: error("%s", text); break; case Message::mkDebug: debug("%s", text); break; } } void ScriptController::PrintMessage(Message::EKind kind, const char* format, ...) { BString<1024> tmp2; va_list ap; va_start(ap, format); tmp2.FormatV(format, ap); va_end(ap); if (m_logPrefix) { AddMessage(kind, BString<1024>("%s: %s", m_logPrefix, *tmp2)); } else { AddMessage(kind, tmp2); } } void ScriptController::Write(const char* str) { fwrite(str, 1, strlen(str), m_writepipe); fflush(m_writepipe); } nzbget-19.1/daemon/util/NString.cpp0000644000175000017500000001730213130203062017052 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2015-2016 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, see . */ #include "nzbget.h" #include "NString.h" template BString::BString(const char* format, ...) { va_list ap; va_start(ap, format); FormatV(format, ap); va_end(ap); } template void BString::Set(const char* str, int len) { m_data[0] = '\0'; int addLen = len > 0 ? std::min(size - 1, len) : size - 1; strncpy(m_data, str, addLen); m_data[addLen] = '\0'; } template void BString::Append(const char* str, int len) { if (len == 0) { len = strlen(str); } int curLen = strlen(m_data); int avail = size - curLen - 1; int addLen = std::min(avail, len); if (addLen > 0) { strncpy(m_data + curLen, str, addLen); m_data[curLen + addLen] = '\0'; } } template void BString::AppendFmt(const char* format, ...) { va_list ap; va_start(ap, format); AppendFmtV(format, ap); va_end(ap); } template void BString::AppendFmtV(const char* format, va_list ap) { int curLen = strlen(m_data); int avail = size - curLen; if (avail > 0) { vsnprintf(m_data + curLen, avail, format, ap); } } template void BString::Format(const char* format, ...) { va_list ap; va_start(ap, format); FormatV(format, ap); va_end(ap); } template void BString::FormatV(const char* format, va_list ap) { vsnprintf(m_data, size, format, ap); } bool CString::operator==(const CString& other) { return (!m_data && !other.m_data) || (m_data && other.m_data && !strcmp(m_data, other.m_data)); } bool CString::operator==(const char* other) { return (!m_data && !other) || (m_data && other && !strcmp(m_data, other)); } void CString::Set(const char* str, int len) { if (str) { if (len == 0) { len = strlen(str); } m_data = (char*)realloc(m_data, len + 1); strncpy(m_data, str, len); m_data[len] = '\0'; } else { free(m_data); m_data = nullptr; } } void CString::Append(const char* str, int len) { if (len == 0) { len = strlen(str); } int curLen = Length(); int newLen = curLen + len; m_data = (char*)realloc(m_data, newLen + 1); strncpy(m_data + curLen, str, len); m_data[curLen + len] = '\0'; } void CString::AppendFmt(const char* format, ...) { va_list ap; va_start(ap, format); AppendFmtV(format, ap); va_end(ap); } void CString::AppendFmtV(const char* format, va_list ap) { va_list ap2; va_copy(ap2, ap); int addLen = vsnprintf(nullptr, 0, format, ap); if (addLen < 0) return; // error int curLen = Length(); int newLen = curLen + addLen; m_data = (char*)realloc(m_data, newLen + 1); vsnprintf(m_data + curLen, newLen + 1, format, ap2); va_end(ap2); } void CString::Format(const char* format, ...) { va_list ap; va_start(ap, format); FormatV(format, ap); va_end(ap); } void CString::FormatV(const char* format, va_list ap) { va_list ap2; va_copy(ap2, ap); int newLen = vsnprintf(nullptr, 0, format, ap); if (newLen < 0) return; // bad argument m_data = (char*)realloc(m_data, newLen + 1); vsnprintf(m_data, newLen + 1, format, ap2); va_end(ap2); } int CString::Find(const char* str, int pos) { if (pos != 0 && pos >= Length()) { return -1; } char* res = strstr(m_data + pos, str); return res ? (int)(res - m_data) : -1; } void CString::Replace(int pos, int len, const char* str, int strLen) { int addLen = strlen(str); if (strLen > 0) { addLen = std::min(addLen, strLen); } int curLen = Length(); int delLen = pos + len <= curLen ? len : curLen - pos; int newLen = curLen - delLen + addLen; if (pos > curLen) return; // bad argument char* newvalue = (char*)malloc(newLen + 1); strncpy(newvalue, m_data, pos); strncpy(newvalue + pos, str, addLen); strcpy(newvalue + pos + addLen, m_data + pos + len); free(m_data); m_data = newvalue; } void CString::Replace(const char* from, const char* to) { int fromLen = strlen(from); int toLen = strlen(to); int pos = 0; while ((pos = Find(from, pos)) != -1) { Replace(pos, fromLen, to); pos += toLen; } } void CString::Bind(char* str) { free(m_data); m_data = str; } char* CString::Unbind() { char* olddata = m_data; m_data = nullptr; return olddata; } void CString::Reserve(int capacity) { int curLen = Length(); if (capacity > curLen || curLen == 0) { m_data = (char*)realloc(m_data, capacity + 1); m_data[curLen] = '\0'; } } CString CString::FormatStr(const char* format, ...) { CString result; va_list ap; va_start(ap, format); result.FormatV(format, ap); va_end(ap); return result; } void CString::TrimRight() { int len = Length(); if (len == 0) { return; } char* end = m_data + len - 1; while (end >= m_data && (*end == '\n' || *end == '\r' || *end == ' ' || *end == '\t')) { *end = '\0'; end--; } } WString::WString(const char* utfstr) { m_data = (wchar_t*)malloc((strlen(utfstr) * 2 + 1) * sizeof(wchar_t)); wchar_t* out = m_data; unsigned int codepoint; while (*utfstr != 0) { unsigned char ch = (unsigned char)*utfstr; if (ch <= 0x7f) codepoint = ch; else if (ch <= 0xbf) codepoint = (codepoint << 6) | (ch & 0x3f); else if (ch <= 0xdf) codepoint = ch & 0x1f; else if (ch <= 0xef) codepoint = ch & 0x0f; else codepoint = ch & 0x07; ++utfstr; if (((*utfstr & 0xc0) != 0x80) && (codepoint <= 0x10ffff)) { if (codepoint > 0xffff) { *out++ = (wchar_t)(0xd800 + (codepoint >> 10)); *out++ = (wchar_t)(0xdc00 + (codepoint & 0x03ff)); } else if (codepoint < 0xd800 || codepoint >= 0xe000) *out++ = (wchar_t)(codepoint); } } *out = '\0'; } void StringBuilder::Clear() { free(m_data); m_data = nullptr; m_length = 0; m_capacity = 0; } char* StringBuilder::Unbind() { char* olddata = m_data; m_data = nullptr; m_length = 0; m_capacity = 0; return olddata; } void StringBuilder::Reserve(int capacity, bool exact) { int oldCapacity = Capacity(); if (capacity > oldCapacity || oldCapacity == 0) { char* oldData = m_data; // we may grow more than requested int smartCapacity = exact ? 0 : (int)(oldCapacity * 1.5); m_capacity = smartCapacity > capacity ? smartCapacity : capacity; m_data = (char*)realloc(m_data, m_capacity + 1); if (!oldData) { m_data[0] = '\0'; } } } void StringBuilder::Append(const char* str, int len) { if (len == 0) { len = strlen(str); } int curLen = Length(); int newLen = curLen + len; Reserve(newLen, false); strncpy(m_data + curLen, str, len); m_data[curLen + len] = '\0'; m_length = newLen; } void StringBuilder::AppendFmt(const char* format, ...) { va_list ap; va_start(ap, format); AppendFmtV(format, ap); va_end(ap); } void StringBuilder::AppendFmtV(const char* format, va_list ap) { va_list ap2; va_copy(ap2, ap); int addLen = vsnprintf(nullptr, 0, format, ap); if (addLen < 0) return; // error int curLen = Length(); int newLen = curLen + addLen; Reserve(newLen, false); vsnprintf(m_data + curLen, newLen + 1, format, ap2); m_length = newLen; va_end(ap2); } // Instantiate all classes used in our project template class BString<1024>; template class BString<100>; template class BString<20>; nzbget-19.1/daemon/util/Script.h0000644000175000017500000000650313130203062016400 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #ifndef SCRIPT_H #define SCRIPT_H #include "NString.h" #include "Container.h" #include "Thread.h" #include "Log.h" class EnvironmentStrings { public: void Clear(); void InitFromCurrentProcess(); void Append(const char* envstr); void Append(CString&& envstr); #ifdef WIN32 std::unique_ptr GetStrings(); #else std::vector GetStrings(); #endif private: typedef std::vector Strings; Strings m_strings; }; class ScriptController { public: typedef std::vector ArgList; ScriptController(); virtual ~ScriptController(); int Execute(); void Terminate(); bool Break(); void Resume(); void Detach(); static void TerminateAll(); const char* GetScript() { return !m_args.empty() ? *m_args[0] : nullptr; } void SetWorkingDir(const char* workingDir) { m_workingDir = workingDir; } void SetArgs(ArgList&& args) { m_args = std::move(args); } void SetInfoName(const char* infoName) { m_infoName = infoName; } const char* GetInfoName() { return m_infoName; } void SetLogPrefix(const char* logPrefix) { m_logPrefix = logPrefix; } void SetEnvVar(const char* name, const char* value); void SetEnvVarSpecial(const char* prefix, const char* name, const char* value); void SetIntEnvVar(const char* name, int value); protected: void ProcessOutput(char* text); virtual bool ReadLine(char* buf, int bufSize, FILE* stream); void PrintMessage(Message::EKind kind, const char* format, ...) PRINTF_SYNTAX(3); virtual void AddMessage(Message::EKind kind, const char* text); bool GetTerminated() { return m_terminated; } void ResetEnv(); void PrepareEnvOptions(const char* stripPrefix); void PrepareArgs(); virtual const char* GetOptValue(const char* name, const char* value) { return value; } void StartProcess(int* pipein, int* pipeout); int WaitProcess(); void SetNeedWrite(bool needWrite) { m_needWrite = needWrite; } void Write(const char* str); #ifdef WIN32 void BuildCommandLine(char* cmdLineBuf, int bufSize); #endif void UnregisterRunningScript(); private: ArgList m_args; const char* m_workingDir = nullptr; CString m_infoName; const char* m_logPrefix = nullptr; EnvironmentStrings m_environmentStrings; bool m_terminated = false; bool m_completed = false; bool m_detached = false; bool m_needWrite = false; FILE* m_readpipe = 0; FILE* m_writepipe = 0; #ifdef WIN32 HANDLE m_processId = 0; DWORD m_dwProcessId = 0; char m_cmdLine[2048]; #else pid_t m_processId = 0; #endif typedef std::vector RunningScripts; static RunningScripts m_runningScripts; static Mutex m_runningMutex; }; #endif nzbget-19.1/daemon/util/FileSystem.cpp0000644000175000017500000006201613130203062017554 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #include "nzbget.h" #include "FileSystem.h" #include "Util.h" const char* RESERVED_DEVICE_NAMES[] = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", NULL }; CString FileSystem::GetLastErrorMessage() { BString<1024> msg; strerror_r(errno, msg, msg.Capacity()); #ifdef WIN32 if (!errno) { FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), msg, 1024, nullptr); } #endif return *msg; } void FileSystem::NormalizePathSeparators(char* path) { for (char* p = path; *p; p++) { if (*p == ALT_PATH_SEPARATOR) { *p = PATH_SEPARATOR; } } } #ifdef WIN32 bool FileSystem::ForceDirectories(const char* path, CString& errmsg) { errmsg.Clear(); BString<1024> normPath = path; NormalizePathSeparators(normPath); int len = strlen(normPath); if (len > 3 && normPath[len - 1] == PATH_SEPARATOR) { normPath[len - 1] = '\0'; } if (DirectoryExists(normPath)) { return true; } if (FileExists(normPath)) { errmsg.Format("path %s is not a directory", *normPath); return false; } if (strlen(normPath) > 2) { BString<1024> parentPath = *normPath; char* p = (char*)strrchr(parentPath, PATH_SEPARATOR); if (p) { if (p - parentPath == 2 && parentPath[1] == ':' && strlen(parentPath) > 2) { parentPath[3] = '\0'; } else { *p = '\0'; } if (strlen(parentPath) != strlen(path) && !ForceDirectories(parentPath, errmsg)) { return false; } } if (_wmkdir(UtfPathToWidePath(normPath)) != 0 && errno != EEXIST) { errmsg.Format("could not create directory %s: %s", *normPath, *GetLastErrorMessage()); return false; } if (DirectoryExists(normPath)) { return true; } if (FileExists(normPath)) { errmsg.Format("path %s is not a directory", *normPath); return false; } } errmsg.Format("path %s does not exist and could not be created", *normPath); return false; } #else bool FileSystem::ForceDirectories(const char* path, CString& errmsg) { errmsg.Clear(); BString<1024> normPath = path; NormalizePathSeparators(normPath); int len = strlen(normPath); if ((len > 0) && normPath[len - 1] == PATH_SEPARATOR) { normPath[len - 1] = '\0'; } struct stat buffer; bool ok = !stat(normPath, &buffer); if (!ok && errno != ENOENT) { errmsg.Format("could not read information for directory %s: errno %i, %s", *normPath, errno, *GetLastErrorMessage()); return false; } if (ok && !S_ISDIR(buffer.st_mode)) { errmsg.Format("path %s is not a directory", *normPath); return false; } if (!ok) { BString<1024> parentPath = *normPath; char* p = (char*)strrchr(parentPath, PATH_SEPARATOR); if (p) { *p = '\0'; if (strlen(parentPath) != strlen(path) && !ForceDirectories(parentPath, errmsg)) { return false; } } if (mkdir(normPath, S_DIRMODE) != 0 && errno != EEXIST) { errmsg.Format("could not create directory %s: %s", *normPath, *GetLastErrorMessage()); return false; } if (stat(normPath, &buffer) != 0) { errmsg.Format("could not read information for directory %s: %s", *normPath, *GetLastErrorMessage()); return false; } if (!S_ISDIR(buffer.st_mode)) { errmsg.Format("path %s is not a directory", *normPath); return false; } } return true; } #endif CString FileSystem::GetCurrentDirectory() { #ifdef WIN32 wchar_t unistr[1024]; ::GetCurrentDirectoryW(1024, unistr); return WidePathToUtfPath(unistr); #else char str[1024]; getcwd(str, 1024); return str; #endif } bool FileSystem::SetCurrentDirectory(const char* dirFilename) { #ifdef WIN32 return ::SetCurrentDirectoryW(UtfPathToWidePath(dirFilename)); #else return chdir(dirFilename) == 0; #endif } bool FileSystem::DirEmpty(const char* dirFilename) { DirBrowser dir(dirFilename); return dir.Next() == nullptr; } bool FileSystem::LoadFileIntoBuffer(const char* filename, CharBuffer& buffer, bool addTrailingNull) { DiskFile file; if (!file.Open(filename, DiskFile::omRead)) { return false; } // obtain file size. file.Seek(0, DiskFile::soEnd); int size = (int)file.Position(); file.Seek(0); // allocate memory to contain the whole file. buffer.Reserve(size + (addTrailingNull ? 1 : 0)); // copy the file into the buffer. file.Read(buffer, size); file.Close(); if (addTrailingNull) { buffer[size] = 0; } return true; } bool FileSystem::SaveBufferIntoFile(const char* filename, const char* buffer, int bufLen) { DiskFile file; if (!file.Open(filename, DiskFile::omWrite)) { return false; } int writtenBytes = (int)file.Write(buffer, bufLen); file.Close(); return writtenBytes == bufLen; } bool FileSystem::AllocateFile(const char* filename, int64 size, bool sparse, CString& errmsg) { errmsg.Clear(); bool ok = false; #ifdef WIN32 HANDLE hFile = CreateFileW(UtfPathToWidePath(filename), GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_NEW, 0, nullptr); if (hFile == INVALID_HANDLE_VALUE) { errno = 0; // wanting error message from WinAPI instead of C-lib errmsg = GetLastErrorMessage(); return false; } if (sparse) { // try to create sparse file (supported only on NTFS partitions); it may fail but that's OK. DWORD dwBytesReturned; DeviceIoControl(hFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &dwBytesReturned, nullptr); } LARGE_INTEGER size64; size64.QuadPart = size; SetFilePointerEx(hFile, size64, nullptr, FILE_END); SetEndOfFile(hFile); CloseHandle(hFile); ok = true; #else // create file FILE* file = fopen(filename, FOPEN_AB); if (!file) { errmsg = GetLastErrorMessage(); return false; } fclose(file); // there are no reliable function to expand file on POSIX, so we must try different approaches, // starting with the fastest one and hoping it will work // 1) set file size using function "truncate" (this is fast, if it works) truncate(filename, size); // check if it worked ok = FileSize(filename) == size; if (!ok) { // 2) truncate did not work, expanding the file by writing to it (that's slow) truncate(filename, 0); file = fopen(filename, FOPEN_AB); if (!file) { errmsg = GetLastErrorMessage(); return false; } char c = '0'; fwrite(&c, 1, size, file); fclose(file); ok = FileSize(filename) == size; } #endif return ok; } bool FileSystem::TruncateFile(const char* filename, int size) { #ifdef WIN32 FILE* file = _wfopen(UtfPathToWidePath(filename), WString(FOPEN_RBP)); fseek(file, size, SEEK_SET); bool ok = SetEndOfFile((HANDLE)_get_osfhandle(_fileno(file))) != 0; fclose(file); return ok; #else return truncate(filename, size) == 0; #endif } char* FileSystem::BaseFileName(const char* filename) { char* p = (char*)strrchr(filename, PATH_SEPARATOR); char* p1 = (char*)strrchr(filename, ALT_PATH_SEPARATOR); if (p1) { if ((p && p < p1) || !p) { p = p1; } } if (p) { return p + 1; } else { return (char*)filename; } } bool FileSystem::ReservedChar(char ch) { if ((unsigned char)ch < 32) { return true; } else { switch (ch) { case '"': case '*': case '/': case ':': case '<': case '>': case '?': case '\\': case '|': return true; } } return false; } //replace bad chars in filename CString FileSystem::MakeValidFilename(const char* filename, bool allowSlashes) { CString result = filename; for (char* p = result; *p; p++) { if (ReservedChar(*p)) { if (allowSlashes && (*p == PATH_SEPARATOR || *p == ALT_PATH_SEPARATOR)) { *p = PATH_SEPARATOR; continue; } *p = '_'; } } // remove trailing dots and spaces. they are not allowed in directory names on windows, // but we remove them on posix too, in a case the directory is accessed from windows via samba. for (int len = strlen(result); len > 0 && (result[len - 1] == '.' || result[len - 1] == ' '); len--) { result[len - 1] = '\0'; } // check if the filename starts with a reserved device name. // although these names are reserved only on Windows we adjust them on posix too, // in a case the directory is accessed from windows via samba. for (const char** ptr = RESERVED_DEVICE_NAMES; const char* reserved = *ptr; ptr++) { int len = strlen(reserved); if (!strncasecmp(result, reserved, len) && (result[len] == '.' || result[len] == '\0')) { result = CString::FormatStr("_%s", *result); break; } } return result; } CString FileSystem::MakeUniqueFilename(const char* destDir, const char* basename) { CString result; result.Format("%s%c%s", destDir, PATH_SEPARATOR, basename); int dupeNumber = 0; while (FileExists(result)) { dupeNumber++; const char* extension = strrchr(basename, '.'); if (extension && extension != basename) { BString<1024> filenameWithoutExt = basename; int end = extension - basename; filenameWithoutExt[end < 1024 ? end : 1024-1] = '\0'; if (!strcasecmp(extension, ".par2")) { char* volExtension = strrchr(filenameWithoutExt, '.'); if (volExtension && volExtension != filenameWithoutExt && !strncasecmp(volExtension, ".vol", 4)) { *volExtension = '\0'; extension = basename + (volExtension - filenameWithoutExt); } } result.Format("%s%c%s.duplicate%d%s", destDir, PATH_SEPARATOR, *filenameWithoutExt, dupeNumber, extension); } else { result.Format("%s%c%s.duplicate%d", destDir, PATH_SEPARATOR, basename, dupeNumber); } } return result; } bool FileSystem::MoveFile(const char* srcFilename, const char* dstFilename) { #ifdef WIN32 return _wrename(UtfPathToWidePath(srcFilename), UtfPathToWidePath(dstFilename)) == 0; #else bool ok = rename(srcFilename, dstFilename) == 0; if (!ok && errno == EXDEV) { ok = CopyFile(srcFilename, dstFilename) && DeleteFile(srcFilename); } return ok; #endif } bool FileSystem::CopyFile(const char* srcFilename, const char* dstFilename) { DiskFile infile; if (!infile.Open(srcFilename, DiskFile::omRead)) { return false; } DiskFile outfile; if (!outfile.Open(dstFilename, DiskFile::omWrite)) { return false; } CharBuffer buffer(1024 * 50); int cnt = buffer.Size(); while (cnt == buffer.Size()) { cnt = (int)infile.Read(buffer, buffer.Size()); outfile.Write(buffer, cnt); } infile.Close(); outfile.Close(); return true; } bool FileSystem::DeleteFile(const char* filename) { #ifdef WIN32 return _wremove(UtfPathToWidePath(filename)) == 0; #else return remove(filename) == 0; #endif } bool FileSystem::FileExists(const char* filename) { #ifdef WIN32 // we use a native windows call because c-lib function "stat" fails on windows if file date is invalid WIN32_FIND_DATAW findData; HANDLE handle = FindFirstFileW(UtfPathToWidePath(filename), &findData); if (handle != INVALID_HANDLE_VALUE) { bool exists = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0; FindClose(handle); return exists; } return false; #else struct stat buffer; bool exists = !stat(filename, &buffer) && S_ISREG(buffer.st_mode); return exists; #endif } bool FileSystem::DirectoryExists(const char* dirFilename) { #ifdef WIN32 WIN32_FIND_DATAW findData; HANDLE handle = FindFirstFileW(UtfPathToWidePath( BString<1024>(dirFilename && dirFilename[strlen(dirFilename) - 1] == PATH_SEPARATOR ? "%s*" : "%s\\*", dirFilename)), &findData); if (handle != INVALID_HANDLE_VALUE) { bool exists = ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) || (dirFilename[0] != '\0' && dirFilename[1] == ':' && (dirFilename[2] == '\0' || dirFilename[3] == '\0')); FindClose(handle); return exists; } if (GetLastError() == ERROR_FILE_NOT_FOUND) { // path exists but doesn't have any file/directory entries - possible only for root paths (e. g. "C:\") return true; } return false; #else struct stat buffer; bool exists = !stat(dirFilename, &buffer) && S_ISDIR(buffer.st_mode); return exists; #endif } bool FileSystem::CreateDirectory(const char* dirFilename) { #ifdef WIN32 _wmkdir(UtfPathToWidePath(dirFilename)); #else mkdir(dirFilename, S_DIRMODE); #endif return DirectoryExists(dirFilename); } bool FileSystem::RemoveDirectory(const char* dirFilename) { #ifdef WIN32 return _wrmdir(UtfPathToWidePath(dirFilename)) == 0; #else return rmdir(dirFilename) == 0; #endif } /* Delete directory which is empty or contains only hidden files or directories (whose names start with dot) */ bool FileSystem::DeleteDirectory(const char* dirFilename) { if (RemoveDirectory(dirFilename)) { return true; } // check if directory contains only hidden files (whose names start with dot) { DirBrowser dir(dirFilename); while (const char* filename = dir.Next()) { if (*filename != '.') { // calling RemoveDirectory to set correct errno return RemoveDirectory(dirFilename); } } } // make sure "DirBrowser dir" is destroyed (and has closed its handle) before we trying to delete the directory CString errmsg; if (!DeleteDirectoryWithContent(dirFilename, errmsg)) { // calling RemoveDirectory to set correct errno return RemoveDirectory(dirFilename); } return true; } bool FileSystem::DeleteDirectoryWithContent(const char* dirFilename, CString& errmsg) { errmsg.Clear(); bool del = false; bool ok = true; { DirBrowser dir(dirFilename); while (const char* filename = dir.Next()) { BString<1024> fullFilename("%s%c%s", dirFilename, PATH_SEPARATOR, filename); if (FileSystem::DirectoryExists(fullFilename)) { del = DeleteDirectoryWithContent(fullFilename, errmsg); } else { del = DeleteFile(fullFilename); } ok &= del; if (!del && errmsg.Empty()) { errmsg.Format("could not delete %s: %s", *fullFilename, *GetLastErrorMessage()); } } } // make sure "DirBrowser dir" is destroyed (and has closed its handle) before we trying to delete the directory del = RemoveDirectory(dirFilename); ok &= del; if (!del && errmsg.Empty()) { errmsg = GetLastErrorMessage(); } return ok; } int64 FileSystem::FileSize(const char* filename) { #ifdef WIN32 // we use a native windows call because c-lib function "stat" fails on windows if file date is invalid WIN32_FIND_DATAW findData; HANDLE handle = FindFirstFileW(UtfPathToWidePath(filename), &findData); if (handle != INVALID_HANDLE_VALUE) { int64 size = ((int64)(findData.nFileSizeHigh) << 32) + findData.nFileSizeLow; FindClose(handle); return size; } return -1; #else struct stat buffer; buffer.st_size = -1; stat(filename, &buffer); return buffer.st_size; #endif } int64 FileSystem::FreeDiskSize(const char* path) { #ifdef WIN32 ULARGE_INTEGER free, dummy; if (GetDiskFreeSpaceEx(path, &free, &dummy, &dummy)) { return free.QuadPart; } #else struct statvfs diskdata; if (!statvfs(path, &diskdata)) { return (int64)diskdata.f_frsize * (int64)diskdata.f_bavail; } #endif return -1; } bool FileSystem::RenameBak(const char* filename, const char* bakPart, bool removeOldExtension, CString& newName) { BString<1024> changedFilename; if (removeOldExtension) { changedFilename = filename; char* extension = strrchr(changedFilename, '.'); if (extension) { *extension = '\0'; } } newName.Format("%s.%s", removeOldExtension ? *changedFilename : filename, bakPart); int i = 2; while (FileExists(newName) || DirectoryExists(newName)) { newName.Format("%s.%i.%s", removeOldExtension ? *changedFilename : filename, i++, bakPart); } return MoveFile(filename, newName); } #ifndef WIN32 CString FileSystem::ExpandHomePath(const char* filename) { CString result; if (filename && (filename[0] == '~') && (filename[1] == '/')) { // expand home-dir char* home = getenv("HOME"); if (!home) { struct passwd *pw = getpwuid(getuid()); if (pw) { home = pw->pw_dir; } } if (!home) { return filename; } if (home[strlen(home)-1] == '/') { result.Format("%s%s", home, filename + 2); } else { result.Format("%s/%s", home, filename + 2); } } else { result.Append(filename ? filename : ""); } return result; } #endif CString FileSystem::ExpandFileName(const char* filename) { #ifdef WIN32 wchar_t unistr[1024]; _wfullpath(unistr, UtfPathToWidePath(filename), 1024); return WidePathToUtfPath(unistr); #else CString result; result.Reserve(1024 - 1); if (filename[0] != '\0' && filename[0] != '/') { char curDir[MAX_PATH + 1]; getcwd(curDir, sizeof(curDir) - 1); // 1 char reserved for adding backslash int offset = 0; if (filename[0] == '.' && filename[1] == '/') { offset += 2; } result.Format("%s/%s", curDir, filename + offset); } else { result = filename; } return result; #endif } CString FileSystem::GetExeFileName(const char* argv0) { CString exename; exename.Reserve(1024 - 1); exename[1024 - 1] = '\0'; #ifdef WIN32 GetModuleFileName(nullptr, exename, 1024); #else // Linux int r = readlink("/proc/self/exe", exename, 1024 - 1); if (r > 0) { exename[r] = '\0'; return exename; } // FreeBSD r = readlink("/proc/curproc/file", exename, 1024 - 1); if (r > 0) { exename[r] = '\0'; return exename; } exename = ExpandFileName(argv0); #endif return exename; } bool FileSystem::SameFilename(const char* filename1, const char* filename2) { #ifdef WIN32 return strcasecmp(filename1, filename2) == 0; #else return strcmp(filename1, filename2) == 0; #endif } #ifdef WIN32 CString FileSystem::MakeCanonicalPath(const char* path) { int len = strlen(path); if (!strncmp("\\\\?\\", path, 4) || len == 0) { return path; } std::vector components = Util::SplitStr(path, "\\/"); for (uint32 i = 1; i < components.size(); i++) { if (!strcmp(components[i], "..")) { components.erase(components.begin() + i - 1, components.begin() + i + 1); i -= 2; } else if (!strcmp(components[i], ".")) { components.erase(components.begin() + i); i--; } } StringBuilder result; result.Reserve(strlen(path)); if (!strncmp("\\\\", path, 2)) { result.Append("\\\\"); } bool first = true; for (CString& comp : components) { if (comp.Length() > 0) { if (!first) { result.Append("\\"); } result.Append(comp); first = false; } } if ((path[len - 1] == '\\' || path[len - 1] == '/' || (len > 3 && !strcmp(path + len - 3, "\\..")) || (len > 2 && !strcmp(path + len - 2, "\\."))) && result[result.Length() - 1] != '\\') { result.Append("\\"); } return *result; } #endif bool FileSystem::FlushFileBuffers(int fileDescriptor, CString& errmsg) { #ifdef WIN32 BOOL ok = ::FlushFileBuffers((HANDLE)_get_osfhandle(fileDescriptor)); if (!ok) { errno = 0; // wanting error message from WinAPI instead of C-lib errmsg = GetLastErrorMessage(); } return ok; #else #ifdef HAVE_FULLFSYNC int ret = fcntl(fileDescriptor, F_FULLFSYNC) == -1 ? 1 : 0; #elif HAVE_FDATASYNC int ret = fdatasync(fileDescriptor); #else int ret = fsync(fileDescriptor); #endif if (ret != 0) { errmsg = GetLastErrorMessage(); } return ret == 0; #endif } bool FileSystem::FlushDirBuffers(const char* filename, CString& errmsg) { #ifdef WIN32 FILE* file = _wfopen(UtfPathToWidePath(filename), WString(FOPEN_RBP)); #else BString<1024> parentPath = filename; char* p = (char*)strrchr(parentPath, PATH_SEPARATOR); if (p) { *p = '\0'; } FILE* file = fopen(parentPath, FOPEN_RB); #endif if (!file) { errmsg = GetLastErrorMessage(); return false; } bool ok = FlushFileBuffers(fileno(file), errmsg); fclose(file); return ok; } #ifndef WIN32 void FileSystem::FixExecPermission(const char* filename) { struct stat buffer; bool ok = !stat(filename, &buffer); if (ok) { buffer.st_mode = buffer.st_mode | S_IXUSR | S_IXGRP | S_IXOTH; chmod(filename, buffer.st_mode); } } #endif #ifdef WIN32 bool FileSystem::NeedLongPath(const char* path) { bool alreadyLongPath = !strncmp(path, "\\\\?\\", 4); if (alreadyLongPath) { return false; } if (strlen(path) > 260 - 14) { return true; } Tokenizer tok(path, "\\/"); for (int partNo = 0; const char* part = tok.Next(); partNo++) { // check if the file part starts with a reserved device name for (const char** ptr = RESERVED_DEVICE_NAMES; const char* reserved = *ptr; ptr++) { int len = strlen(reserved); if (!strncasecmp(part, reserved, len) && (part[len] == '.' || part[len] == '\0')) { return true; } } // check if the file part contains reserved characters for (const char* p = part; *p; p++) { if (ReservedChar(*p) && !(partNo == 0 && p == part + 1 && *p == ':')) { return true; } } } return false; } #endif CString FileSystem::MakeExtendedPath(const char* path, bool force) { #ifdef WIN32 if (force || NeedLongPath(path)) { CString canonicalPath = MakeCanonicalPath(path); BString<1024> longpath; if (canonicalPath[0] == '\\' && canonicalPath[1] == '\\') { // UNC path longpath.Format("\\\\?\\UNC\\%s", canonicalPath + 2); } else { // local path longpath.Format("\\\\?\\%s", canonicalPath); } return *longpath; } else #endif { return path; } } #ifdef WIN32 WString FileSystem::UtfPathToWidePath(const char* utfpath) { return *FileSystem::MakeExtendedPath(utfpath, false); } CString FileSystem::WidePathToUtfPath(const wchar_t* wpath) { char utfstr[1024]; int copied = WideCharToMultiByte(CP_UTF8, 0, wpath, -1, utfstr, 1024, nullptr, nullptr); return utfstr; } #endif #ifdef WIN32 DirBrowser::DirBrowser(const char* path) { BString<1024> mask("%s%c*.*", path, PATH_SEPARATOR); m_file = FindFirstFileW(FileSystem::UtfPathToWidePath(mask), &m_findData); m_first = true; } DirBrowser::~DirBrowser() { if (m_file != INVALID_HANDLE_VALUE) { FindClose(m_file); } } const char* DirBrowser::InternNext() { bool ok = false; if (m_first) { ok = m_file != INVALID_HANDLE_VALUE; m_first = false; } else { ok = FindNextFileW(m_file, &m_findData) != 0; } if (ok) { m_filename = FileSystem::WidePathToUtfPath(m_findData.cFileName); return m_filename; } return nullptr; } #else #ifdef DIRBROWSER_SNAPSHOT DirBrowser::DirBrowser(const char* path, bool snapshot) : m_snapshot(snapshot) #else DirBrowser::DirBrowser(const char* path) #endif { #ifdef DIRBROWSER_SNAPSHOT if (m_snapshot) { DirBrowser dir(path, false); while (const char* filename = dir.Next()) { m_snapshotFiles.emplace_back(filename); } m_snapshotIter = m_snapshotFiles.begin(); } else #endif { m_dir = opendir(path); } } DirBrowser::~DirBrowser() { #ifdef DIRBROWSER_SNAPSHOT if (!m_snapshot) #endif { if (m_dir) { closedir(m_dir); } } } const char* DirBrowser::InternNext() { #ifdef DIRBROWSER_SNAPSHOT if (m_snapshot) { return m_snapshotIter == m_snapshotFiles.end() ? nullptr : **m_snapshotIter++; } else #endif { if (m_dir) { m_findData = readdir(m_dir); if (m_findData) { return m_findData->d_name; } } return nullptr; } } #endif const char* DirBrowser::Next() { const char* filename = nullptr; for (filename = InternNext(); filename && (!strcmp(filename, ".") || !strcmp(filename, "..")); ) { filename = InternNext(); } return filename; } DiskFile::~DiskFile() { if (m_file) { Close(); } } bool DiskFile::Open(const char* filename, EOpenMode mode) { const char* strmode = mode == omRead ? FOPEN_RB : mode == omReadWrite ? FOPEN_RBP : mode == omWrite ? FOPEN_WB : FOPEN_AB; #ifdef WIN32 m_file = _wfopen(FileSystem::UtfPathToWidePath(filename), WString(strmode)); #else m_file = fopen(filename, strmode); #endif return m_file; } bool DiskFile::Close() { if (m_file) { int ret = fclose(m_file); m_file = nullptr; return ret; } else { return false; } } int64 DiskFile::Read(void* buffer, int64 size) { return fread(buffer, 1, (size_t)size, m_file); } int64 DiskFile::Write(const void* buffer, int64 size) { return fwrite(buffer, 1, (size_t)size, m_file); } int64 DiskFile::Print(const char* format, ...) { va_list ap; va_start(ap, format); int ret = vfprintf(m_file, format, ap); va_end(ap); return ret; } char* DiskFile::ReadLine(char* buffer, int64 size) { return fgets(buffer, (int)size, m_file); } int64 DiskFile::Position() { return ftell(m_file); } bool DiskFile::Seek(int64 position, ESeekOrigin origin) { return fseek(m_file, position, origin == soCur ? SEEK_CUR : origin == soEnd ? SEEK_END : SEEK_SET) == 0; } bool DiskFile::Eof() { return feof(m_file) != 0; } bool DiskFile::Error() { return ferror(m_file) != 0; } bool DiskFile::SetWriteBuffer(int size) { return setvbuf(m_file, nullptr, _IOFBF, size) == 0; } bool DiskFile::Flush() { return fflush(m_file) == 0; } bool DiskFile::Sync(CString& errmsg) { return FileSystem::FlushFileBuffers(fileno(m_file), errmsg); } nzbget-19.1/daemon/util/Thread.h0000644000175000017500000000633113130203062016342 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef THREAD_H #define THREAD_H class Mutex { public: Mutex(); Mutex(const Mutex&) = delete; ~Mutex(); void Lock(); void Unlock(); private: #ifdef WIN32 CRITICAL_SECTION m_mutexObj; #else pthread_mutex_t m_mutexObj; #endif }; class Guard { public: Guard() : m_mutex(nullptr) {} Guard(Mutex& mutex) : m_mutex(&mutex) { if (m_mutex) m_mutex->Lock(); } Guard(Mutex* mutex) : m_mutex(mutex) { if (m_mutex) m_mutex->Lock(); } Guard(std::unique_ptr& mutex) : m_mutex(mutex.get()) { if (m_mutex) m_mutex->Lock(); } Guard(Guard&& other) : m_mutex(other.m_mutex) { other.m_mutex = nullptr; } Guard(const Guard&) = delete; ~Guard() { Unlock(); } Guard& operator=(Guard&& other) { m_mutex = other.m_mutex; other.m_mutex = nullptr; return *this; } operator bool() { return m_mutex; } private: Mutex* m_mutex; void Unlock() { if (m_mutex) { m_mutex->Unlock(); m_mutex = nullptr; } } }; template class GuardedPtr { public: GuardedPtr(T* ptr, Mutex* mutex) : m_ptr(ptr), m_mutex(mutex) { if (m_mutex) m_mutex->Lock(); } GuardedPtr(GuardedPtr&& other) : m_ptr(other.m_ptr), m_mutex(other.m_mutex) { other.m_mutex = nullptr; } GuardedPtr(const GuardedPtr& other) = delete; ~GuardedPtr() { Unlock(); } T* operator->() { return m_ptr; } operator T*() { return m_ptr; } // for-range loops on GuardedPtr auto begin() { return m_ptr->begin(); } auto end() { return m_ptr->end(); } private: T* m_ptr; Mutex* m_mutex; void Unlock() { if (m_mutex) { m_mutex->Unlock(); m_mutex = nullptr; } } }; class Thread { public: Thread(); Thread(const Thread&) = delete; virtual ~Thread(); static void Init(); virtual void Start(); virtual void Stop(); virtual void Resume(); bool Kill(); bool IsStopped() { return m_stopped; }; bool IsRunning() const { return m_running; } bool GetAutoDestroy() { return m_autoDestroy; } void SetAutoDestroy(bool autoDestroy) { m_autoDestroy = autoDestroy; } static int GetThreadCount(); protected: virtual void Run() {}; // Virtual function - override in derivatives private: static std::unique_ptr m_threadMutex; static int m_threadCount; #ifdef WIN32 HANDLE m_threadObj = 0; #else pthread_t m_threadObj = 0; #endif bool m_running = false; bool m_stopped = false; bool m_autoDestroy = false; #ifdef WIN32 static void __cdecl thread_handler(void* object); #else static void *thread_handler(void* object); #endif }; #endif nzbget-19.1/daemon/util/FileSystem.h0000644000175000017500000001146013130203062017216 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #ifndef FILESYSTEM_H #define FILESYSTEM_H #include "NString.h" class FileSystem { public: static CString GetLastErrorMessage(); static char* BaseFileName(const char* filename); static bool SameFilename(const char* filename1, const char* filename2); static void NormalizePathSeparators(char* path); static bool LoadFileIntoBuffer(const char* filename, CharBuffer& buffer, bool addTrailingNull); static bool SaveBufferIntoFile(const char* filename, const char* buffer, int bufLen); static bool AllocateFile(const char* filename, int64 size, bool sparse, CString& errmsg); static bool TruncateFile(const char* filename, int size); static CString MakeValidFilename(const char* filename, bool allowSlashes = false); static bool ReservedChar(char ch); static CString MakeUniqueFilename(const char* destDir, const char* basename); static bool MoveFile(const char* srcFilename, const char* dstFilename); static bool CopyFile(const char* srcFilename, const char* dstFilename); static bool DeleteFile(const char* filename); static bool FileExists(const char* filename); static bool DirectoryExists(const char* dirFilename); static bool CreateDirectory(const char* dirFilename); /* Delete empty directory */ static bool RemoveDirectory(const char* dirFilename); /* Delete directory which is empty or contains only hidden files or directories */ static bool DeleteDirectory(const char* dirFilename); static bool DeleteDirectoryWithContent(const char* dirFilename, CString& errmsg); static bool ForceDirectories(const char* path, CString& errmsg); static CString GetCurrentDirectory(); static bool SetCurrentDirectory(const char* dirFilename); static int64 FileSize(const char* filename); static int64 FreeDiskSize(const char* path); static bool DirEmpty(const char* dirFilename); static bool RenameBak(const char* filename, const char* bakPart, bool removeOldExtension, CString& newName); #ifndef WIN32 static CString ExpandHomePath(const char* filename); static void FixExecPermission(const char* filename); #endif static CString ExpandFileName(const char* filename); static CString GetExeFileName(const char* argv0); /* Flush disk buffers for file with given descriptor */ static bool FlushFileBuffers(int fileDescriptor, CString& errmsg); /* Flush disk buffers for file metadata (after file renaming) */ static bool FlushDirBuffers(const char* filename, CString& errmsg); static CString MakeExtendedPath(const char* path, bool force); #ifdef WIN32 static WString UtfPathToWidePath(const char* utfpath); static CString WidePathToUtfPath(const wchar_t* wpath); static CString MakeCanonicalPath(const char* filename); static bool NeedLongPath(const char* path); #endif }; class DirBrowser { public: #ifdef DIRBROWSER_SNAPSHOT DirBrowser(const char* path, bool snapshot = true); #else DirBrowser(const char* path); #endif ~DirBrowser(); const char* Next(); private: #ifdef WIN32 WIN32_FIND_DATAW m_findData; HANDLE m_file; bool m_first; CString m_filename; #else DIR* m_dir = nullptr; struct dirent* m_findData; #endif #ifdef DIRBROWSER_SNAPSHOT bool m_snapshot; typedef std::deque FileList; FileList m_snapshotFiles; FileList::iterator m_snapshotIter; #endif const char* InternNext(); }; class DiskFile { public: enum EOpenMode { omRead, // file must exist omReadWrite, // file must exist omWrite, // create new or overwrite existing omAppend // create new or append to existing }; enum ESeekOrigin { soSet, soCur, soEnd }; DiskFile() = default; DiskFile(const DiskFile&) = delete; ~DiskFile(); bool Open(const char* filename, EOpenMode mode); bool Close(); bool Active() { return m_file != nullptr; } int64 Read(void* buffer, int64 size); int64 Write(const void* buffer, int64 size); int64 Position(); bool Seek(int64 position, ESeekOrigin origin = soSet); bool Eof(); bool Error(); int64 Print(const char* format, ...) PRINTF_SYNTAX(2); char* ReadLine(char* buffer, int64 size); bool SetWriteBuffer(int size); bool Flush(); bool Sync(CString& errmsg); private: FILE* m_file = nullptr; }; #endif nzbget-19.1/daemon/util/Service.h0000644000175000017500000000255013130203062016532 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2015-2016 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, see . */ #ifndef SERVICE_H #define SERVICE_H #include "Thread.h" class Service { public: Service(); protected: virtual int ServiceInterval() = 0; virtual void ServiceWork() = 0; private: int m_lastTick = 0; friend class ServiceCoordinator; }; class ServiceCoordinator : public Thread { public: typedef std::vector ServiceList; ServiceCoordinator(); virtual ~ServiceCoordinator(); virtual void Run(); private: ServiceList m_services; void RegisterService(Service* service); friend class Service; }; extern ServiceCoordinator* g_ServiceCoordinator; #endif nzbget-19.1/daemon/util/Container.h0000644000175000017500000001025013130203062017050 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2016 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, see . */ #ifndef CONTAINER_H #define CONTAINER_H // for-range loops on pointers to std-containers (avoiding dereferencing) template typename std::deque::iterator begin(std::deque* c) { return c->begin(); } template typename std::deque::iterator end(std::deque* c) { return c->end(); } template typename std::vector::iterator begin(std::vector* c) { return c->begin(); } template typename std::vector::iterator end(std::vector* c) { return c->end(); } template typename std::list::iterator begin(std::list* c) { return c->begin(); } template typename std::list::iterator end(std::list* c) { return c->end(); } // for-range loops on pointers to std-containers of unique_ptr: // iterating through raw pointers instead of through unique_ptr template struct RawDequeIterator { RawDequeIterator(typename std::deque>::iterator baseIterator) : m_baseIterator(baseIterator) {} typename std::deque>::iterator m_baseIterator; }; template bool operator!=(RawDequeIterator& it1, RawDequeIterator& it2) { return it1.m_baseIterator != it2.m_baseIterator; } template T* operator*(RawDequeIterator& it) { return (*it.m_baseIterator).get(); } template RawDequeIterator operator++(RawDequeIterator& it) { return RawDequeIterator(it.m_baseIterator++); } template RawDequeIterator begin(std::deque>* c) { return RawDequeIterator(c->begin()); } template RawDequeIterator end(std::deque>* c) { return RawDequeIterator(c->end()); } template struct RawVectorIterator { RawVectorIterator(typename std::vector>::iterator baseIterator) : m_baseIterator(baseIterator) {} typename std::vector>::iterator m_baseIterator; }; template bool operator!=(RawVectorIterator& it1, RawVectorIterator& it2) { return it1.m_baseIterator != it2.m_baseIterator; } template T* operator*(RawVectorIterator& it) { return (*it.m_baseIterator).get(); } template RawVectorIterator operator++(RawVectorIterator& it) { return RawVectorIterator(it.m_baseIterator++); } template RawVectorIterator begin(std::vector>* c) { return RawVectorIterator(c->begin()); } template RawVectorIterator end(std::vector>* c) { return RawVectorIterator(c->end()); } /* Template class for deque of unique_ptr with useful utility functions */ template class UniqueDeque : public std::deque> { public: void Add(std::unique_ptr uptr, bool addTop = false) { if (addTop) { this->push_front(std::move(uptr)); } else { this->push_back(std::move(uptr)); } } std::unique_ptr Remove(T* p) { std::unique_ptr uptr; typename UniqueDeque::iterator it = Find(p); if (it != this->end()) { uptr = std::move(*it); this->erase(it); } return uptr; } typename UniqueDeque::iterator Find(T* p) { return std::find_if(this->begin(), this->end(), [p](std::unique_ptr& uptr) { return uptr.get() == p; }); } T* Find(int id) { for (std::unique_ptr& uptr : *this) { if (uptr->GetId() == id) { return uptr.get(); } } return nullptr; } }; #endif nzbget-19.1/daemon/util/Observer.cpp0000644000175000017500000000241013130203062017247 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "Observer.h" #include "Log.h" void Subject::Attach(Observer* observer) { m_observers.push_back(observer); } void Subject::Detach(Observer* observer) { m_observers.erase(std::find(m_observers.begin(), m_observers.end(), observer)); } void Subject::Notify(void* aspect) { debug("Notifying observers"); for (Observer* observer : m_observers) { observer->Update(this, aspect); } } nzbget-19.1/daemon/util/Service.cpp0000644000175000017500000000346013130203062017066 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2015-2016 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, see . */ #include "nzbget.h" #include "Service.h" #include "DownloadInfo.h" #include "Options.h" #include "Log.h" #include "Util.h" Service::Service() { g_ServiceCoordinator->RegisterService(this); } ServiceCoordinator::ServiceCoordinator() { debug("Creating ServiceCoordinator"); } ServiceCoordinator::~ServiceCoordinator() { debug("Destroying ServiceCoordinator"); } void ServiceCoordinator::Run() { debug("Entering ServiceCoordinator-loop"); const int stepMSec = 100; int curTick = 0; while (!IsStopped()) { for (Service* service : m_services) { if (curTick >= service->m_lastTick + service->ServiceInterval() || // interval expired curTick == 0 || // first start curTick + 10000 < service->m_lastTick) // int overflow { service->ServiceWork(); service->m_lastTick = curTick; } } curTick += stepMSec; usleep(stepMSec * 1000); } debug("Exiting ServiceCoordinator-loop"); } void ServiceCoordinator::RegisterService(Service* service) { m_services.push_back(service); } nzbget-19.1/daemon/util/Thread.cpp0000644000175000017500000001003013130203062016664 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "Log.h" #include "Thread.h" int Thread::m_threadCount = 1; // take the main program thread into account std::unique_ptr Thread::m_threadMutex; Mutex::Mutex() { #ifdef WIN32 InitializeCriticalSection(&m_mutexObj); #else pthread_mutex_init(&m_mutexObj, nullptr); #endif } Mutex::~Mutex() { #ifdef WIN32 DeleteCriticalSection(&m_mutexObj); #else pthread_mutex_destroy(&m_mutexObj); #endif } void Mutex::Lock() { #ifdef WIN32 EnterCriticalSection(&m_mutexObj); #ifdef DEBUG // CriticalSections on Windows can be locked many times from the same thread, // but we do not want this and must treat such situations as errors and detect them. if (m_mutexObj.RecursionCount > 1) { error("Internal program error: inconsistent thread-lock detected"); } #endif #else pthread_mutex_lock(&m_mutexObj); #endif } void Mutex::Unlock() { #ifdef WIN32 LeaveCriticalSection(&m_mutexObj); #else pthread_mutex_unlock(&m_mutexObj); #endif } void Thread::Init() { debug("Initializing global thread data"); m_threadMutex = std::make_unique(); } Thread::Thread() { debug("Creating Thread"); } Thread::~Thread() { debug("Destroying Thread"); } void Thread::Start() { debug("Starting Thread"); m_running = true; // NOTE: we must guarantee, that in a time we set m_running // to value returned from pthread_create, the thread-object still exists. // This is not obvious! // pthread_create could wait long enough before returning result // back to allow the started thread to complete its job and terminate. // We lock mutex m_threadMutex on calling pthread_create; the started thread // then also try to lock the mutex (see thread_handler) and therefore // must wait until we unlock it Guard guard(m_threadMutex); #ifdef WIN32 m_threadObj = (HANDLE)_beginthread(Thread::thread_handler, 0, (void*)this); m_running = m_threadObj != 0; #else pthread_attr_t m_attr; pthread_attr_init(&m_attr); pthread_attr_setdetachstate(&m_attr, PTHREAD_CREATE_DETACHED); pthread_attr_setinheritsched(&m_attr, PTHREAD_INHERIT_SCHED); m_running = !pthread_create(&m_threadObj, &m_attr, Thread::thread_handler, (void *) this); pthread_attr_destroy(&m_attr); #endif } void Thread::Stop() { debug("Stopping Thread"); m_stopped = true; } void Thread::Resume() { debug("Resuming Thread"); m_stopped = false; } bool Thread::Kill() { debug("Killing Thread"); Guard guard(m_threadMutex); #ifdef WIN32 bool terminated = TerminateThread(m_threadObj, 0) != 0; #else bool terminated = pthread_cancel(m_threadObj) == 0; #endif if (terminated) { m_threadCount--; } return terminated; } #ifdef WIN32 void __cdecl Thread::thread_handler(void* object) #else void* Thread::thread_handler(void* object) #endif { { Guard guard(m_threadMutex); m_threadCount++; } debug("Entering Thread-func"); Thread* thread = (Thread*)object; thread->Run(); debug("Thread-func exited"); if (thread->m_autoDestroy) { debug("Autodestroying Thread-object"); delete thread; } else { thread->m_running = false; } { Guard guard(m_threadMutex); m_threadCount--; } #ifndef WIN32 return nullptr; #endif } int Thread::GetThreadCount() { Guard guard(m_threadMutex); return m_threadCount; } nzbget-19.1/daemon/util/Util.h0000644000175000017500000002114713130203062016052 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #ifndef UTIL_H #define UTIL_H #include "NString.h" #ifdef WIN32 extern int optind, opterr; extern char *optarg; int getopt(int argc, char *argv[], char *optstring); #endif class Util { public: static bool MatchFileExt(const char* filename, const char* extensionList, const char* listSeparator); static int64 GetCurrentTicks(); /* * Split command line into arguments. * Uses spaces and single quotation marks as separators. * May return empty list if bad escaping was detected. */ static std::vector SplitCommandLine(const char* commandLine); static int64 JoinInt64(uint32 Hi, uint32 Lo); static void SplitInt64(int64 Int64, uint32* Hi, uint32* Lo); static void TrimRight(char* str); static char* Trim(char* str); static bool EmptyStr(const char* str) { return !str || !*str; } static std::vector SplitStr(const char* str, const char* separators); static bool EndsWith(const char* str, const char* suffix, bool caseSensitive); static bool AlphaNum(const char* str); /* replace all occurences of szFrom to szTo in string szStr with a limitation that szTo must be shorter than szFrom */ static char* ReduceStr(char* str, const char* from, const char* to); /* Calculate Hash using Bob Jenkins (1996) algorithm */ static uint32 HashBJ96(const char* buffer, int bufSize, uint32 initValue); #ifdef WIN32 static bool RegReadStr(HKEY keyRoot, const char* keyName, const char* valueName, char* buffer, int* bufLen); #endif static void SetStandByMode(bool standBy); static time_t CurrentTime(); /* cross platform version of GNU timegm, which is similar to mktime but takes an UTC time as parameter */ static time_t Timegm(tm const *t); static void FormatTime(time_t timeSec, char* buffer, int bufsize); static CString FormatTime(time_t timeSec); static CString FormatSpeed(int bytesPerSecond); static CString FormatSize(int64 fileSize); static CString FormatBuffer(const char* buf, int len); /* * Returns program version and revision number as string formatted like "0.7.0-r295". * If revision number is not available only version is returned ("0.7.0"). */ static const char* VersionRevision() { return VersionRevisionBuf; }; static char VersionRevisionBuf[100]; static void Init(); static uint32 Crc32(uchar *block, uint32 length); static uint32 Crc32m(uint32 startCrc, uchar *block, uint32 length); static uint32 Crc32Combine(uint32 crc1, uint32 crc2, uint32 len2); /* * Returns number of available CPU cores or -1 if it could not be determined */ static int NumberOfCpuCores(); }; class WebUtil { public: static uint32 DecodeBase64(char* inputBuffer, int inputBufferLength, char* outputBuffer); /* * Encodes string to be used as content of xml-tag. */ static CString XmlEncode(const char* raw); /* * Decodes string from xml. * The string is decoded on the place overwriting the content of raw-data. */ static void XmlDecode(char* raw); /* * Returns pointer to tag-content and length of content in iValueLength * The returned pointer points to the part of source-string, no additional strings are allocated. */ static const char* XmlFindTag(const char* xml, const char* tag, int* valueLength); /* * Parses tag-content into szValueBuf. */ static bool XmlParseTagValue(const char* xml, const char* tag, char* valueBuf, int valueBufSize, const char** tagEnd); /* * Replaces all tags with spaces effectively providing the text content only. * The string is transformed in-place overwriting the previous content. */ static void XmlStripTags(char* xml); /* * Replaces all entities with spaces. * The string is transformed in-place overwriting the previous content. */ static void XmlRemoveEntities(char* raw); /* * Creates JSON-string by replace the certain characters with escape-sequences. */ static CString JsonEncode(const char* raw); /* * Decodes JSON-string. * The string is decoded on the place overwriting the content of raw-data. */ static void JsonDecode(char* raw); /* * Returns pointer to field-content and length of content in iValueLength * The returned pointer points to the part of source-string, no additional strings are allocated. */ static const char* JsonFindField(const char* jsonText, const char* fieldName, int* valueLength); /* * Returns pointer to field-content and length of content in iValueLength * The returned pointer points to the part of source-string, no additional strings are allocated. */ static const char* JsonNextValue(const char* jsonText, int* valueLength); /* * Unquote http quoted string. * The string is decoded on the place overwriting the content of raw-data. */ static void HttpUnquote(char* raw); /* * Decodes URL-string. * The string is decoded on the place overwriting the content of raw-data. */ static void UrlDecode(char* raw); /* * Makes valid URL by replacing of spaces with "%20". */ static CString UrlEncode(const char* raw); /* * Converts ISO-8859-1 (aka Latin-1) into UTF-8. */ static CString Latin1ToUtf8(const char* str); static time_t ParseRfc822DateTime(const char* dateTimeStr); }; class URL { public: URL(const char* address); bool IsValid() { return m_valid; } const char* GetAddress() { return m_address; } const char* GetProtocol() { return m_protocol; } const char* GetUser() { return m_user; } const char* GetPassword() { return m_password; } const char* GetHost() { return m_host; } const char* GetResource() { return m_resource; } int GetPort() { return m_port; } bool GetTls() { return m_tls; } private: CString m_address; CString m_protocol; CString m_user; CString m_password; CString m_host; CString m_resource; int m_port = 0; bool m_tls = false; bool m_valid = false; void ParseUrl(); }; class RegEx { public: RegEx(const char *pattern, int matchBufSize = 100); ~RegEx(); bool IsValid() { return m_valid; } bool Match(const char* str); int GetMatchCount(); int GetMatchStart(int index); int GetMatchLen(int index); private: #ifdef HAVE_REGEX_H regex_t m_context; std::unique_ptr m_matches; #endif bool m_valid; int m_matchBufSize; }; class WildMask { public: WildMask(const char* pattern, bool wantsPositions = false): m_pattern(pattern), m_wantsPositions(wantsPositions) {} bool Match(const char* text); int GetMatchCount() { return m_wildCount; } int GetMatchStart(int index) { return m_wildStart[index]; } int GetMatchLen(int index) { return m_wildLen[index]; } private: typedef std::vector IntList; CString m_pattern; bool m_wantsPositions; int m_wildCount; IntList m_wildStart; IntList m_wildLen; void ExpandArray(); }; #ifndef DISABLE_GZIP class ZLib { public: /* * calculates the size required for output buffer */ static uint32 GZipLen(int inputBufferLength); /* * compresses inputBuffer and returns the size of bytes written to * outputBuffer or 0 if the buffer is too small or an error occured. */ static uint32 GZip(const void* inputBuffer, int inputBufferLength, void* outputBuffer, int outputBufferLength); }; class GUnzipStream { public: enum EStatus { zlError, zlFinished, zlOK }; GUnzipStream(int BufferSize); ~GUnzipStream(); /* * set next memory block for uncompression */ void Write(const void *inputBuffer, int inputBufferLength); /* * get next uncompressed memory block. * iOutputBufferLength - the size of uncompressed block. if it is "0" the next compressed block must be provided via "Write". */ EStatus Read(const void **outputBuffer, int *outputBufferLength); private: z_stream m_zStream = {0}; std::unique_ptr m_outputBuffer; int m_bufferSize; bool m_active = false; }; #endif class Tokenizer { public: Tokenizer(const char* dataString, const char* separators); Tokenizer(char* dataString, const char* separators, bool inplaceBuf); char* Next(); private: BString<1024> m_shortString; CString m_longString; char* m_dataString; const char* m_separators; char* m_savePtr = nullptr; bool m_working = false; }; #endif nzbget-19.1/daemon/util/NString.h0000644000175000017500000001352313130203062016520 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2015-2016 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, see . */ #ifndef NSTRING_H #define NSTRING_H /* BString is a replacement for char-arrays allocated on stack. It has no memory overhead, provides memory management and formatting functions. */ template class BString { public: BString() { m_data[0] = '\0'; } explicit BString(const char* format, ...) PRINTF_SYNTAX(2); BString(BString& other) = delete; BString(const char* str) { Set(str); } // for initialization via assignment BString(BString&& other) noexcept { Set(other.m_data); } // never used but declaration is needed for initialization via assignment BString& operator=(const char* str) { Set(str); return *this; } int Length() const { return (int)strlen(m_data); } int Capacity() const { return size - 1; } bool Empty() const { return !*m_data; } void Clear() { m_data[0] = '\0'; } const char* Str() const { return m_data; } operator char*() const { return const_cast(m_data); } char* operator*() const { return const_cast(m_data); } void Set(const char* str, int len = 0); void Append(const char* str, int len = 0); void AppendFmt(const char* format, ...) PRINTF_SYNTAX(2); void AppendFmtV(const char* format, va_list ap); void Format(const char* format, ...) PRINTF_SYNTAX(2); void FormatV(const char* format, va_list ap); protected: char m_data[size]; }; /* CString is a replacement for C-Style null-terminated strings. It has no memory overhead, provides memory management and string handling functions. */ class CString { public: CString() {} ~CString() { free(m_data); } CString(const char* str, int len = 0) { Set(str, len); } CString(CString&& other) noexcept { m_data = other.m_data; other.m_data = nullptr; } CString(CString& other) = delete; CString& operator=(CString&& other) { free(m_data); m_data = other.m_data; other.m_data = nullptr; return *this; } CString& operator=(const char* str) { Set(str); return *this; } bool operator==(const CString& other); bool operator==(const char* other); static CString FormatStr(const char* format, ...); operator char*() const { return m_data; } char* operator*() const { return m_data; } const char* Str() const { return m_data ? m_data : ""; } int Length() const { return m_data ? (int)strlen(m_data) : 0; } bool Empty() const { return !m_data || !*m_data; } void Clear() { free(m_data); m_data = nullptr; } void Reserve(int capacity); void Bind(char* str); char* Unbind(); void Set(const char* str, int len = 0); void Append(const char* str, int len = 0); void AppendFmt(const char* format, ...) PRINTF_SYNTAX(2); void AppendFmtV(const char* format, va_list ap); void Format(const char* format, ...) PRINTF_SYNTAX(2); void FormatV(const char* format, va_list ap); int Find(const char* str, int pos = 0); void Replace(int pos, int len, const char* str, int strLen = 0); void Replace(const char* from, const char* to); void TrimRight(); protected: char* m_data = nullptr; }; /* Wide-character string. */ class WString { public: WString(wchar_t* wstr) : m_data(wcsdup(wstr)) {} WString(const char* utfstr); ~WString() { free(m_data); } WString(WString&& other) noexcept { m_data = other.m_data; other.m_data = nullptr; } WString(WString& other) = delete; operator wchar_t*() const { return m_data; } wchar_t* operator*() const { return m_data; } int Length() { return wcslen(m_data); } protected: wchar_t* m_data = nullptr; }; /* StringBuilder preallocates storage space and is best suitable for often "Append"s. */ class StringBuilder { public: ~StringBuilder() { free(m_data); } operator const char*() const { return m_data ? m_data : ""; } explicit operator char*() { return m_data; } const char* operator*() const { return m_data; } int Length() const { return m_length; } void SetLength(int length) { m_length = length; } int Capacity() const { return m_capacity; } void Reserve(int capacity, bool exact = false); bool Empty() const { return m_length == 0; } void Clear(); void Append(const char* str, int len = 0); void AppendFmt(const char* format, ...) PRINTF_SYNTAX(2); void AppendFmtV(const char* format, va_list ap); char* Unbind(); protected: char* m_data = nullptr; int m_length = 0; int m_capacity = 0; }; /* Plain char-buffer for I/O operations. */ class CharBuffer { public: CharBuffer() {} CharBuffer(int size) : m_size(size) { m_data = (char*)malloc(size); } CharBuffer(CharBuffer& other) : m_data(other.m_data), m_size(other.m_size) { other.m_data = nullptr; other.m_size = 0; } ~CharBuffer() { free(m_data); } int Size() { return m_size; } void Reserve(int size) { m_data = (char*)realloc(m_data, size); m_size = size; } void Clear() { free(m_data); m_data = nullptr; m_size = 0; } operator char*() const { return m_data; } char* operator*() const { return m_data; } protected: char* m_data = nullptr; int m_size = 0; }; #ifdef DEBUG // helper declarations to identify incorrect calls to "free" at compile time #ifdef WIN32 void _free_dbg(CString str, int ignore); void _free_dbg(StringBuilder str, int ignore); void _free_dbg(CharBuffer str, int ignore); #else void free(CString str); void free(StringBuilder str); void free(CharBuffer str); #endif #endif #endif nzbget-19.1/daemon/util/Util.cpp0000644000175000017500000012326713130203062016413 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "Util.h" #ifndef WIN32 // function "code_revision" is automatically generated in file "code_revision.cpp" on each build const char* code_revision(void); #endif #ifdef WIN32 // getopt for WIN32: // from http://www.codeproject.com/cpp/xgetopt.asp // Original Author: Hans Dietrich (hdietrich2@hotmail.com) // Released to public domain from author (thanks) // Slightly modified by Andrey Prygunkov char *optarg; // global argument pointer int optind = 0; // global argv index int getopt(int argc, char *argv[], char *optstring) { static char *next = nullptr; if (optind == 0) next = nullptr; optarg = nullptr; if (next == nullptr || *next == '\0') { if (optind == 0) optind++; if (optind >= argc || argv[optind][0] != '-' || argv[optind][1] == '\0') { optarg = nullptr; if (optind < argc) optarg = argv[optind]; return -1; } if (strcmp(argv[optind], "--") == 0) { optind++; optarg = nullptr; if (optind < argc) optarg = argv[optind]; return -1; } next = argv[optind]; next++; // skip past - optind++; } char c = *next++; char *cp = strchr(optstring, c); if (cp == nullptr || c == ':') { fprintf(stderr, "Invalid option %c", c); return '?'; } cp++; if (*cp == ':') { if (*next != '\0') { optarg = next; next = nullptr; } else if (optind < argc) { optarg = argv[optind]; optind++; } else { fprintf(stderr, "Option %c needs an argument", c); return '?'; } } return c; } #endif char Util::VersionRevisionBuf[100]; void Util::Init() { #ifndef WIN32 if ((strlen(code_revision()) > 0) && strstr(VERSION, "testing")) { snprintf(VersionRevisionBuf, sizeof(VersionRevisionBuf), "%s-r%s", VERSION, code_revision()); } else #endif { snprintf(VersionRevisionBuf, sizeof(VersionRevisionBuf), "%s", VERSION); } // init static vars there GetCurrentTicks(); } int64 Util::JoinInt64(uint32 Hi, uint32 Lo) { return (((int64)Hi) << 32) + Lo; } void Util::SplitInt64(int64 Int64, uint32* Hi, uint32* Lo) { *Hi = (uint32)(Int64 >> 32); *Lo = (uint32)(Int64 & 0xFFFFFFFF); } /* Base64 decryption is taken from * Article "BASE 64 Decoding and Encoding Class 2003" by Jan Raddatz * http://www.codeguru.com/cpp/cpp/algorithms/article.php/c5099/ */ const static char BASE64_DEALPHABET [128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 9 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10 - 19 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20 - 29 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 30 - 39 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, // 40 - 49 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, // 50 - 59 0, 61, 0, 0, 0, 0, 1, 2, 3, 4, // 60 - 69 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 70 - 79 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 80 - 89 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, // 90 - 99 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // 100 - 109 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, // 110 - 119 49, 50, 51, 0, 0, 0, 0, 0 // 120 - 127 }; uint32 DecodeByteQuartet(char* inputBuffer, char* outputBuffer) { uint32 buffer = 0; if (inputBuffer[3] == '=') { if (inputBuffer[2] == '=') { buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[0]]) << 6; buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[1]]) << 6; buffer = buffer << 14; outputBuffer [0] = (char)(buffer >> 24); return 1; } else { buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[0]]) << 6; buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[1]]) << 6; buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[2]]) << 6; buffer = buffer << 8; outputBuffer [0] = (char)(buffer >> 24); outputBuffer [1] = (char)(buffer >> 16); return 2; } } else { buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[0]]) << 6; buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[1]]) << 6; buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[2]]) << 6; buffer = (buffer | BASE64_DEALPHABET [(int)inputBuffer[3]]) << 6; buffer = buffer << 2; outputBuffer [0] = (char)(buffer >> 24); outputBuffer [1] = (char)(buffer >> 16); outputBuffer [2] = (char)(buffer >> 8); return 3; } return 0; } CString Util::FormatSize(int64 fileSize) { CString result; if (fileSize > 1024 * 1024 * 1000) { result.Format("%.2f GB", (float)((float)fileSize / 1024 / 1024 / 1024)); } else if (fileSize > 1024 * 1000) { result.Format("%.2f MB", (float)((float)fileSize / 1024 / 1024)); } else if (fileSize > 1000) { result.Format("%.2f KB", (float)((float)fileSize / 1024)); } else if (fileSize == 0) { result = "0 MB"; } else { result.Format("%i B", (int)fileSize); } return result; } CString Util::FormatSpeed(int bytesPerSecond) { CString result; if (bytesPerSecond >= 100 * 1024 * 1024) { result.Format("%i MB/s", bytesPerSecond / 1024 / 1024); } else if (bytesPerSecond >= 10 * 1024 * 1024) { result.Format("%0.1f MB/s", (float)bytesPerSecond / 1024.0 / 1024.0); } else if (bytesPerSecond >= 1024 * 1000) { result.Format("%0.2f MB/s", (float)bytesPerSecond / 1024.0 / 1024.0); } else { result.Format("%i KB/s", bytesPerSecond / 1024); } return result; } void Util::FormatTime(time_t timeSec, char* buffer, int bufsize) { #ifdef HAVE_CTIME_R_3 ctime_r(&timeSec, buffer, bufsize); #else ctime_r(&timeSec, buffer); #endif buffer[bufsize-1] = '\0'; // trim LF buffer[strlen(buffer) - 1] = '\0'; } CString Util::FormatTime(time_t timeSec) { CString result; result.Reserve(50); FormatTime(timeSec, result, 50); return result; } CString Util::FormatBuffer(const char* buf, int len) { CString result; result.Reserve(len * 3 + 1); while (len--) { result.AppendFmt("%02x ", (int)(uchar)*buf++); } return result; } bool Util::MatchFileExt(const char* filename, const char* extensionList, const char* listSeparator) { int filenameLen = strlen(filename); Tokenizer tok(extensionList, listSeparator); while (const char* ext = tok.Next()) { int extLen = strlen(ext); if (filenameLen >= extLen && !strcasecmp(ext, filename + filenameLen - extLen)) { return true; } if (strchr(ext, '*') || strchr(ext, '?')) { WildMask mask(ext); if (mask.Match(filename)) { return true; } } } return false; } std::vector Util::SplitCommandLine(const char* commandLine) { std::vector result; char buf[1024]; uint32 len = 0; bool escaping = false; bool space = true; for (const char* p = commandLine; ; p++) { if (*p) { const char c = *p; if (escaping) { if (c == '\'') { if (p[1] == '\'' && len < sizeof(buf) - 1) { buf[len++] = c; p++; } else { escaping = false; space = true; } } else if (len < sizeof(buf) - 1) { buf[len++] = c; } } else { if (c == ' ') { space = true; } else if (c == '\'' && space) { escaping = true; space = false; } else if (len < sizeof(buf) - 1) { buf[len++] = c; space = false; } } } if ((space || !*p) && len > 0) { //add token buf[len] = '\0'; result.emplace_back(buf); len = 0; } if (!*p) { break; } } return result; } void Util::TrimRight(char* str) { char* end = str + strlen(str) - 1; while (end >= str && (*end == '\n' || *end == '\r' || *end == ' ' || *end == '\t')) { *end = '\0'; end--; } } char* Util::Trim(char* str) { TrimRight(str); while (*str == '\n' || *str == '\r' || *str == ' ' || *str == '\t') { str++; } return str; } char* Util::ReduceStr(char* str, const char* from, const char* to) { int lenFrom = strlen(from); int lenTo = strlen(to); // assert(iLenTo < iLenFrom); while (char* p = strstr(str, from)) { const char* src = to; while ((*p++ = *src++)) ; src = --p - lenTo + lenFrom; while ((*p++ = *src++)) ; } return str; } std::vector Util::SplitStr(const char* str, const char* separators) { std::vector result; Tokenizer tok(str, separators); while (const char* substr = tok.Next()) { result.emplace_back(substr); } return result; } bool Util::EndsWith(const char* str, const char* suffix, bool caseSensitive) { if (!str) { return false; } if (EmptyStr(suffix)) { return true; } int lenStr = strlen(str); int lenSuf = strlen(suffix); if (lenSuf > lenStr) { return false; } if (caseSensitive) { return !strcmp(str + lenStr - lenSuf, suffix); } else { return !strcasecmp(str + lenStr - lenSuf, suffix); } } bool Util::AlphaNum(const char* str) { for (const char* p = str; *p; p++) { char ch = *p; if (!((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'))) { return false; } } return true; } /* Calculate Hash using Bob Jenkins (1996) algorithm * http://burtleburtle.net/bob/c/lookup2.c */ #define hashsize(n) ((uint32)1<<(n)) #define hashmask(n) (hashsize(n)-1) #define mix(a,b,c) \ { \ a -= b; a -= c; a ^= (c>>13); \ b -= c; b -= a; b ^= (a<<8); \ c -= a; c -= b; c ^= (b>>13); \ a -= b; a -= c; a ^= (c>>12); \ b -= c; b -= a; b ^= (a<<16); \ c -= a; c -= b; c ^= (b>>5); \ a -= b; a -= c; a ^= (c>>3); \ b -= c; b -= a; b ^= (a<<10); \ c -= a; c -= b; c ^= (b>>15); \ } uint32 hash(register uint8 *k, register uint32 length, register uint32 initval) // register uint8 *k; /* the key */ // register uint32 length; /* the length of the key */ // register uint32 initval; /* the previous hash, or an arbitrary value */ { uint32 a,b,c,len; /* Set up the internal state */ len = length; a = b = 0x9e3779b9; /* the golden ratio; an arbitrary value */ c = initval; /* the previous hash value */ /*---------------------------------------- handle most of the key */ while (len >= 12) { a += (k[0] +((uint32)k[1]<<8) +((uint32)k[2]<<16) +((uint32)k[3]<<24)); b += (k[4] +((uint32)k[5]<<8) +((uint32)k[6]<<16) +((uint32)k[7]<<24)); c += (k[8] +((uint32)k[9]<<8) +((uint32)k[10]<<16)+((uint32)k[11]<<24)); mix(a,b,c); k += 12; len -= 12; } /*------------------------------------- handle the last 11 bytes */ c += length; switch(len) /* all the case statements fall through */ { case 11: c+=((uint32)k[10]<<24); case 10: c+=((uint32)k[9]<<16); case 9 : c+=((uint32)k[8]<<8); /* the first byte of c is reserved for the length */ case 8 : b+=((uint32)k[7]<<24); case 7 : b+=((uint32)k[6]<<16); case 6 : b+=((uint32)k[5]<<8); case 5 : b+=k[4]; case 4 : a+=((uint32)k[3]<<24); case 3 : a+=((uint32)k[2]<<16); case 2 : a+=((uint32)k[1]<<8); case 1 : a+=k[0]; /* case 0: nothing left to add */ } mix(a,b,c); /*-------------------------------------------- report the result */ return c; } uint32 Util::HashBJ96(const char* buffer, int bufSize, uint32 initValue) { return (uint32)hash((uint8*)buffer, (uint32)bufSize, (uint32)initValue); } #ifdef WIN32 bool Util::RegReadStr(HKEY keyRoot, const char* keyName, const char* valueName, char* buffer, int* bufLen) { HKEY subKey; if (!RegOpenKeyEx(keyRoot, keyName, 0, KEY_READ, &subKey)) { DWORD retBytes = *bufLen; LONG ret = RegQueryValueEx(subKey, valueName, nullptr, nullptr, (LPBYTE)buffer, &retBytes); *bufLen = retBytes; RegCloseKey(subKey); return ret == 0; } return false; } #endif time_t Util::CurrentTime() { #ifdef WIN32 // C-library function "time()" works on Windows too but is very CPU intensive // since it uses high performance timer which we don't need anyway. // A combination of GetSystemTime() + Timegm() works much faster. SYSTEMTIME systm; GetSystemTime(&systm); struct tm tm; tm.tm_year = systm.wYear - 1900; tm.tm_mon = systm.wMonth - 1; tm.tm_mday = systm.wDay; tm.tm_hour = systm.wHour; tm.tm_min = systm.wMinute; tm.tm_sec = systm.wSecond; return Timegm(&tm); #else return ::time(nullptr); #endif } /* From boost */ inline int is_leap(int year) { if(year % 400 == 0) return 1; if(year % 100 == 0) return 0; if(year % 4 == 0) return 1; return 0; } inline int days_from_0(int year) { year--; return 365 * year + (year / 400) - (year/100) + (year / 4); } inline int days_from_1970(int year) { static const int days_from_0_to_1970 = 719162; // days_from_0(1970); return days_from_0(year) - days_from_0_to_1970; } inline int days_from_1jan(int year,int month,int day) { static const int days[2][12] = { { 0,31,59,90,120,151,181,212,243,273,304,334}, { 0,31,60,91,121,152,182,213,244,274,305,335} }; return days[is_leap(year)][month-1] + day - 1; } inline time_t internal_timegm(tm const *t) { int year = t->tm_year + 1900; int month = t->tm_mon; if(month > 11) { year += month/12; month %= 12; } else if(month < 0) { int years_diff = (-month + 11)/12; year -= years_diff; month+=12 * years_diff; } month++; int day = t->tm_mday; int day_of_year = days_from_1jan(year,month,day); int days_since_epoch = days_from_1970(year) + day_of_year; time_t seconds_in_day = 3600 * 24; time_t result = seconds_in_day * days_since_epoch + 3600 * t->tm_hour + 60 * t->tm_min + t->tm_sec; return result; } time_t Util::Timegm(tm const *t) { return internal_timegm(t); } // prevent PC from going to sleep void Util::SetStandByMode(bool standBy) { #ifdef WIN32 SetThreadExecutionState((standBy ? 0 : ES_SYSTEM_REQUIRED) | ES_CONTINUOUS); #endif } static uint32 crc32_tab[] = { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d }; /* This is a modified version of chksum_crc() from * crc32.c (http://www.koders.com/c/fid699AFE0A656F0022C9D6B9D1743E697B69CE5815.aspx) * (c) 1999,2000 Krzysztof Dabrowski * (c) 1999,2000 ElysiuM deeZine * * chksum_crc() -- to a given block, this one calculates the * crc32-checksum until the length is * reached. the crc32-checksum will be * the result. */ uint32 Util::Crc32m(uint32 startCrc, uchar *block, uint32 length) { uint32 crc = startCrc; for (uint32 i = 0; i < length; i++) { crc = ((crc >> 8) & 0x00FFFFFF) ^ crc32_tab[(crc ^ *block++) & 0xFF]; } return crc; } uint32 Util::Crc32(uchar *block, uint32 length) { return Util::Crc32m(0xFFFFFFFF, block, length) ^ 0xFFFFFFFF; } /* From zlib/crc32.c (http://www.zlib.net/) * Copyright (C) 1995-2006, 2010, 2011, 2012 Mark Adler */ #define GF2_DIM 32 /* dimension of GF(2) vectors (length of CRC) */ uint32 gf2_matrix_times(uint32 *mat, uint32 vec) { uint32 sum; sum = 0; while (vec) { if (vec & 1) sum ^= *mat; vec >>= 1; mat++; } return sum; } void gf2_matrix_square(uint32 *square, uint32 *mat) { int n; for (n = 0; n < GF2_DIM; n++) square[n] = gf2_matrix_times(mat, mat[n]); } uint32 Util::Crc32Combine(uint32 crc1, uint32 crc2, uint32 len2) { int n; uint32 row; uint32 even[GF2_DIM]; /* even-power-of-two zeros operator */ uint32 odd[GF2_DIM]; /* odd-power-of-two zeros operator */ /* degenerate case (also disallow negative lengths) */ if (len2 <= 0) return crc1; /* put operator for one zero bit in odd */ odd[0] = 0xedb88320UL; /* CRC-32 polynomial */ row = 1; for (n = 1; n < GF2_DIM; n++) { odd[n] = row; row <<= 1; } /* put operator for two zero bits in even */ gf2_matrix_square(even, odd); /* put operator for four zero bits in odd */ gf2_matrix_square(odd, even); /* apply len2 zeros to crc1 (first square will put the operator for one zero byte, eight zero bits, in even) */ do { /* apply zeros operator for this bit of len2 */ gf2_matrix_square(even, odd); if (len2 & 1) crc1 = gf2_matrix_times(even, crc1); len2 >>= 1; /* if no more bits set, then done */ if (len2 == 0) break; /* another iteration of the loop with odd and even swapped */ gf2_matrix_square(odd, even); if (len2 & 1) crc1 = gf2_matrix_times(odd, crc1); len2 >>= 1; /* if no more bits set, then done */ } while (len2 != 0); /* return combined crc */ crc1 ^= crc2; return crc1; } int Util::NumberOfCpuCores() { #ifdef WIN32 SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); return sysinfo.dwNumberOfProcessors; #elif HAVE_SC_NPROCESSORS_ONLN return sysconf(_SC_NPROCESSORS_ONLN); #endif return -1; } int64 Util::GetCurrentTicks() { #ifdef WIN32 static int64 hz=0, hzo=0; if (!hz) { QueryPerformanceFrequency((LARGE_INTEGER*)&hz); QueryPerformanceCounter((LARGE_INTEGER*)&hzo); } int64 t; QueryPerformanceCounter((LARGE_INTEGER*)&t); return ((t-hzo)*1000000)/hz; #else timeval t; gettimeofday(&t, nullptr); return (int64)(t.tv_sec) * 1000000ll + (int64)(t.tv_usec); #endif } uint32 WebUtil::DecodeBase64(char* inputBuffer, int inputBufferLength, char* outputBuffer) { uint32 InputBufferIndex = 0; uint32 OutputBufferIndex = 0; uint32 InputBufferLength = inputBufferLength > 0 ? inputBufferLength : strlen(inputBuffer); char ByteQuartet [4]; int i = 0; while (InputBufferIndex < InputBufferLength) { // Ignore all characters except the ones in BASE64_ALPHABET if ((inputBuffer [InputBufferIndex] >= 48 && inputBuffer [InputBufferIndex] <= 57) || (inputBuffer [InputBufferIndex] >= 65 && inputBuffer [InputBufferIndex] <= 90) || (inputBuffer [InputBufferIndex] >= 97 && inputBuffer [InputBufferIndex] <= 122) || inputBuffer [InputBufferIndex] == '+' || inputBuffer [InputBufferIndex] == '/' || inputBuffer [InputBufferIndex] == '=') { ByteQuartet [i] = inputBuffer [InputBufferIndex]; i++; } InputBufferIndex++; if (i == 4) { OutputBufferIndex += DecodeByteQuartet(ByteQuartet, outputBuffer + OutputBufferIndex); i = 0; } } // OutputBufferIndex gives us the next position of the next decoded character // inside our output buffer and thus represents the number of decoded characters // in our buffer. return OutputBufferIndex; } /* END - Base64 */ CString WebUtil::XmlEncode(const char* raw) { // calculate the required outputstring-size based on number of xml-entities and their sizes int reqSize = strlen(raw); for (const char* p = raw; *p; p++) { uchar ch = *p; switch (ch) { case '>': case '<': reqSize += 4; break; case '&': reqSize += 5; break; case '\'': case '\"': reqSize += 6; break; default: if (ch < 0x20 || ch >= 0x80) { reqSize += 10; break; } } } CString result; result.Reserve(reqSize); // copy string char* output = result; for (const char* p = raw; ; p++) { uchar ch = *p; switch (ch) { case '\0': goto BreakLoop; case '<': strcpy(output, "<"); output += 4; break; case '>': strcpy(output, ">"); output += 4; break; case '&': strcpy(output, "&"); output += 5; break; case '\'': strcpy(output, "'"); output += 6; break; case '\"': strcpy(output, """); output += 6; break; default: if (ch < 0x20 || ch > 0x80) { uint32 cp = ch; // decode utf8 if ((cp >> 5) == 0x6 && (p[1] & 0xc0) == 0x80) { // 2 bytes if (!(ch = *++p)) goto BreakLoop; // read next char cp = ((cp << 6) & 0x7ff) + (ch & 0x3f); } else if ((cp >> 4) == 0xe && (p[1] & 0xc0) == 0x80) { // 3 bytes if (!(ch = *++p)) goto BreakLoop; // read next char cp = ((cp << 12) & 0xffff) + ((ch << 6) & 0xfff); if (!(ch = *++p)) goto BreakLoop; // read next char cp += ch & 0x3f; } else if ((cp >> 3) == 0x1e && (p[1] & 0xc0) == 0x80) { // 4 bytes if (!(ch = *++p)) goto BreakLoop; // read next char cp = ((cp << 18) & 0x1fffff) + ((ch << 12) & 0x3ffff); if (!(ch = *++p)) goto BreakLoop; // read next char cp += (ch << 6) & 0xfff; if (!(ch = *++p)) goto BreakLoop; // read next char cp += ch & 0x3f; } // accept only valid XML 1.0 characters if (cp == 0x9 || cp == 0xA || cp == 0xD || (0x20 <= cp && cp <= 0xD7FF) || (0xE000 <= cp && cp <= 0xFFFD) || (0x10000 <= cp && cp <= 0x10FFFF)) { sprintf(output, "&#x%06x;", cp); output += 10; } else { // replace invalid characters with dots *output++ = '.'; } } else { *output++ = ch; } break; } } BreakLoop: *output = '\0'; return result; } void WebUtil::XmlDecode(char* raw) { char* output = raw; for (char* p = raw;;) { switch (*p) { case '\0': goto BreakLoop; case '&': { p++; if (!strncmp(p, "lt;", 3)) { *output++ = '<'; p += 3; } else if (!strncmp(p, "gt;", 3)) { *output++ = '>'; p += 3; } else if (!strncmp(p, "amp;", 4)) { *output++ = '&'; p += 4; } else if (!strncmp(p, "apos;", 5)) { *output++ = '\''; p += 5; } else if (!strncmp(p, "quot;", 5)) { *output++ = '\"'; p += 5; } else if (*p == '#') { int code = atoi((p++)+1); while (strchr("0123456789;", *p)) p++; *output++ = (char)code; } else { // unknown entity, keep as is *output++ = *(p-1); *output++ = *p++; } break; } default: *output++ = *p++; break; } } BreakLoop: *output = '\0'; } const char* WebUtil::XmlFindTag(const char* xml, const char* tag, int* valueLength) { BString<100> openTag("<%s>", tag); BString<100> closeTag("", tag); BString<100> openCloseTag("<%s/>", tag); const char* pstart = strstr(xml, openTag); const char* pstartend = strstr(xml, openCloseTag); if (!pstart && !pstartend) return nullptr; if (pstartend && (!pstart || pstartend < pstart)) { *valueLength = 0; return pstartend; } const char* pend = strstr(pstart, closeTag); if (!pend) return nullptr; int tagLen = strlen(openTag); *valueLength = (int)(pend - pstart - tagLen); return pstart + tagLen; } bool WebUtil::XmlParseTagValue(const char* xml, const char* tag, char* valueBuf, int valueBufSize, const char** tagEnd) { int valueLen = 0; const char* value = XmlFindTag(xml, tag, &valueLen); if (!value) { return false; } int len = valueLen < valueBufSize ? valueLen : valueBufSize - 1; strncpy(valueBuf, value, len); valueBuf[len] = '\0'; if (tagEnd) { *tagEnd = value + valueLen; } return true; } void WebUtil::XmlStripTags(char* xml) { while (char *start = strchr(xml, '<')) { char *end = strchr(start, '>'); if (!end) { break; } memset(start, ' ', end - start + 1); xml = end + 1; } } void WebUtil::XmlRemoveEntities(char* raw) { char* output = raw; for (char* p = raw;;) { switch (*p) { case '\0': goto BreakLoop; case '&': { char* p2 = p+1; while (isalpha(*p2) || strchr("0123456789#", *p2)) p2++; if (*p2 == ';') { *output++ = ' '; p = p2+1; } else { *output++ = *p++; } break; } default: *output++ = *p++; break; } } BreakLoop: *output = '\0'; } CString WebUtil::JsonEncode(const char* raw) { // calculate the required outputstring-size based on number of escape-entities and their sizes int reqSize = strlen(raw); for (const char* p = raw; *p; p++) { uchar ch = *p; switch (ch) { case '\"': case '\\': case '/': case '\b': case '\f': case '\n': case '\r': case '\t': reqSize++; break; default: if (ch < 0x20 || ch >= 0x80) { reqSize += 6; break; } } } CString result; result.Reserve(reqSize); // copy string char* output = result; for (const char* p = raw; ; p++) { uchar ch = *p; switch (ch) { case '\0': goto BreakLoop; case '"': strcpy(output, "\\\""); output += 2; break; case '\\': strcpy(output, "\\\\"); output += 2; break; case '/': strcpy(output, "\\/"); output += 2; break; case '\b': strcpy(output, "\\b"); output += 2; break; case '\f': strcpy(output, "\\f"); output += 2; break; case '\n': strcpy(output, "\\n"); output += 2; break; case '\r': strcpy(output, "\\r"); output += 2; break; case '\t': strcpy(output, "\\t"); output += 2; break; default: if (ch < 0x20 || ch > 0x80) { uint32 cp = ch; // decode utf8 if ((cp >> 5) == 0x6 && (p[1] & 0xc0) == 0x80) { // 2 bytes if (!(ch = *++p)) goto BreakLoop; // read next char cp = ((cp << 6) & 0x7ff) + (ch & 0x3f); } else if ((cp >> 4) == 0xe && (p[1] & 0xc0) == 0x80) { // 3 bytes if (!(ch = *++p)) goto BreakLoop; // read next char cp = ((cp << 12) & 0xffff) + ((ch << 6) & 0xfff); if (!(ch = *++p)) goto BreakLoop; // read next char cp += ch & 0x3f; } else if ((cp >> 3) == 0x1e && (p[1] & 0xc0) == 0x80) { // 4 bytes if (!(ch = *++p)) goto BreakLoop; // read next char cp = ((cp << 18) & 0x1fffff) + ((ch << 12) & 0x3ffff); if (!(ch = *++p)) goto BreakLoop; // read next char cp += (ch << 6) & 0xfff; if (!(ch = *++p)) goto BreakLoop; // read next char cp += ch & 0x3f; } // we support only Unicode range U+0000-U+FFFF sprintf(output, "\\u%04x", cp <= 0xFFFF ? cp : '.'); output += 6; } else { *output++ = ch; } break; } } BreakLoop: *output = '\0'; return result; } void WebUtil::JsonDecode(char* raw) { char* output = raw; for (char* p = raw;;) { switch (*p) { case '\0': goto BreakLoop; case '\\': { p++; switch (*p) { case '"': *output++ = '"'; break; case '\\': *output++ = '\\'; break; case '/': *output++ = '/'; break; case 'b': *output++ = '\b'; break; case 'f': *output++ = '\f'; break; case 'n': *output++ = '\n'; break; case 'r': *output++ = '\r'; break; case 't': *output++ = '\t'; break; case 'u': *output++ = (char)strtol(p + 1, nullptr, 16); p += 4; break; default: // unknown escape-sequence, should never occur *output++ = *p; break; } p++; break; } default: *output++ = *p++; break; } } BreakLoop: *output = '\0'; } const char* WebUtil::JsonFindField(const char* jsonText, const char* fieldName, int* valueLength) { BString<100> openTag("\"%s\"", fieldName); const char* pstart = strstr(jsonText, openTag); if (!pstart) return nullptr; pstart += strlen(openTag); return JsonNextValue(pstart, valueLength); } const char* WebUtil::JsonNextValue(const char* jsonText, int* valueLength) { const char* pstart = jsonText; while (*pstart && strchr(" ,[{:\r\n\t\f", *pstart)) pstart++; if (!*pstart) return nullptr; const char* pend = pstart; char ch = *pend; bool str = ch == '"'; if (str) { ch = *++pend; } while (ch) { if (ch == '\\') { if (!*++pend || !*++pend) return nullptr; ch = *pend; } if (str && ch == '"') { pend++; break; } else if (!str && strchr(" ,]}\r\n\t\f", ch)) { break; } ch = *++pend; } *valueLength = (int)(pend - pstart); return pstart; } void WebUtil::HttpUnquote(char* raw) { if (*raw != '"') { return; } char *output = raw; for (char *p = raw+1;;) { switch (*p) { case '\0': case '"': goto BreakLoop; case '\\': p++; *output++ = *p; break; default: *output++ = *p++; break; } } BreakLoop: *output = '\0'; } void WebUtil::UrlDecode(char* raw) { char* output = raw; for (char* p = raw;;) { switch (*p) { case '\0': goto BreakLoop; case '%': { p++; uchar c1 = *p++; uchar c2 = *p++; c1 = '0' <= c1 && c1 <= '9' ? c1 - '0' : 'A' <= c1 && c1 <= 'F' ? c1 - 'A' + 10 : 'a' <= c1 && c1 <= 'f' ? c1 - 'a' + 10 : 0; c2 = '0' <= c2 && c2 <= '9' ? c2 - '0' : 'A' <= c2 && c2 <= 'F' ? c2 - 'A' + 10 : 'a' <= c2 && c2 <= 'f' ? c2 - 'a' + 10 : 0; uchar ch = (c1 << 4) + c2; *output++ = (char)ch; break; } default: *output++ = *p++; break; } } BreakLoop: *output = '\0'; } CString WebUtil::UrlEncode(const char* raw) { // calculate the required outputstring-size based on number of spaces int reqSize = strlen(raw); for (const char* p = raw; *p; p++) { if (*p == ' ') { reqSize += 3; // length of "%20" } } CString result; result.Reserve(reqSize); // copy string char* output = result; for (const char* p = raw; ; p++) { uchar ch = *p; switch (ch) { case '\0': goto BreakLoop; case ' ': strcpy(output, "%20"); output += 3; break; default: *output++ = ch; } } BreakLoop: *output = '\0'; return result; } CString WebUtil::Latin1ToUtf8(const char* str) { CString res; res.Reserve(strlen(str) * 2); const uchar *in = (const uchar*)str; uchar *out = (uchar*)(char*)res; while (*in) { if (*in < 128) { *out++ = *in++; } else { *out++ = 0xc2 + (*in > 0xbf); *out++ = (*in++ & 0x3f) + 0x80; } } *out = '\0'; return res; } /* The date/time can be formatted according to RFC822 in different ways. Examples: Wed, 26 Jun 2013 01:02:54 -0600 Wed, 26 Jun 2013 01:02:54 GMT 26 Jun 2013 01:02:54 -0600 26 Jun 2013 01:02 -0600 26 Jun 2013 01:02 A This function however supports only the first format! */ time_t WebUtil::ParseRfc822DateTime(const char* dateTimeStr) { char month[4]; int day, year, hours, minutes, seconds, zonehours, zoneminutes; int r = sscanf(dateTimeStr, "%*s %d %3s %d %d:%d:%d %3d %2d", &day, &month[0], &year, &hours, &minutes, &seconds, &zonehours, &zoneminutes); if (r != 8) { return 0; } int mon = 0; if (!strcasecmp(month, "Jan")) mon = 0; else if (!strcasecmp(month, "Feb")) mon = 1; else if (!strcasecmp(month, "Mar")) mon = 2; else if (!strcasecmp(month, "Apr")) mon = 3; else if (!strcasecmp(month, "May")) mon = 4; else if (!strcasecmp(month, "Jun")) mon = 5; else if (!strcasecmp(month, "Jul")) mon = 6; else if (!strcasecmp(month, "Aug")) mon = 7; else if (!strcasecmp(month, "Sep")) mon = 8; else if (!strcasecmp(month, "Oct")) mon = 9; else if (!strcasecmp(month, "Nov")) mon = 10; else if (!strcasecmp(month, "Dec")) mon = 11; struct tm rawtime; memset(&rawtime, 0, sizeof(rawtime)); rawtime.tm_year = year - 1900; rawtime.tm_mon = mon; rawtime.tm_mday = day; rawtime.tm_hour = hours; rawtime.tm_min = minutes; rawtime.tm_sec = seconds; time_t enctime = Util::Timegm(&rawtime); enctime -= (zonehours * 60 + (zonehours > 0 ? zoneminutes : -zoneminutes)) * 60; return enctime; } URL::URL(const char* address) : m_address(address) { if (address) { ParseUrl(); } } void URL::ParseUrl() { // Examples: // http://user:password@host:port/path/to/resource?param // http://user@host:port/path/to/resource?param // http://host:port/path/to/resource?param // http://host/path/to/resource?param // http://host char* protEnd = strstr(m_address, "://"); if (!protEnd) { // Bad URL return; } m_protocol.Set(m_address, protEnd - m_address); char* hostStart = protEnd + 3; char* slash = strchr(hostStart, '/'); char* hostEnd = nullptr; char* amp = strchr(hostStart, '@'); if (amp && (!slash || amp < slash)) { // parse user/password char* userend = amp - 1; char* pass = strchr(hostStart, ':'); if (pass && pass < amp) { int len = (int)(amp - pass - 1); if (len > 0) { m_password.Set(pass + 1, len); } userend = pass - 1; } int len = (int)(userend - hostStart + 1); if (len > 0) { m_user.Set(hostStart, len); } hostStart = amp + 1; } if (slash) { char* resEnd = m_address + strlen(m_address); m_resource.Set(slash, resEnd - slash + 1); hostEnd = slash - 1; } else { m_resource = "/"; hostEnd = m_address + strlen(m_address); } char* colon = strchr(hostStart, ':'); if (colon && colon < hostEnd) { hostEnd = colon - 1; m_port = atoi(colon + 1); } m_host.Set(hostStart, hostEnd - hostStart + 1); m_valid = true; } RegEx::RegEx(const char *pattern, int matchBufSize) : m_matchBufSize(matchBufSize) { #ifdef HAVE_REGEX_H m_valid = regcomp(&m_context, pattern, REG_EXTENDED | REG_ICASE | (matchBufSize > 0 ? 0 : REG_NOSUB)) == 0; if (matchBufSize > 0) { m_matches = std::make_unique(matchBufSize); } else { m_matches = nullptr; } #else m_valid = false; #endif } RegEx::~RegEx() { #ifdef HAVE_REGEX_H regfree(&m_context); #endif } bool RegEx::Match(const char *str) { #ifdef HAVE_REGEX_H return m_valid ? regexec(&m_context, str, m_matchBufSize, m_matches.get(), 0) == 0 : false; #else return false; #endif } int RegEx::GetMatchCount() { #ifdef HAVE_REGEX_H int count = 0; if (m_matches) { while (count < m_matchBufSize && m_matches[count].rm_so > -1) { count++; } } return count; #else return 0; #endif } int RegEx::GetMatchStart(int index) { #ifdef HAVE_REGEX_H return m_matches[index].rm_so; #else return nullptr; #endif } int RegEx::GetMatchLen(int index) { #ifdef HAVE_REGEX_H return m_matches[index].rm_eo - m_matches[index].rm_so; #else return 0; #endif } void WildMask::ExpandArray() { m_wildCount++; m_wildStart.resize(m_wildCount); m_wildLen.resize(m_wildCount); } // Based on code from http://bytes.com/topic/c/answers/212179-string-matching // Extended to save positions of matches. bool WildMask::Match(const char* text) { m_wildCount = 0; m_wildStart.clear(); m_wildStart.reserve(100); m_wildLen.clear(); m_wildLen.reserve(100); const char* pat = m_pattern; const char* str = text; const char *spos, *wpos; bool qmark = false; bool star = false; spos = wpos = str; while (*str && *pat != '*') { if (m_wantsPositions && (*pat == '?' || *pat == '#')) { if (!qmark) { ExpandArray(); m_wildStart[m_wildCount-1] = str - text; m_wildLen[m_wildCount-1] = 0; qmark = true; } } else if (m_wantsPositions && qmark) { m_wildLen[m_wildCount-1] = str - (text + m_wildStart[m_wildCount-1]); qmark = false; } if (!(tolower(*pat) == tolower(*str) || *pat == '?' || (*pat == '#' && strchr("0123456789", *str)))) { return false; } str++; pat++; } if (m_wantsPositions && qmark) { m_wildLen[m_wildCount-1] = str - (text + m_wildStart[m_wildCount-1]); qmark = false; } while (*str) { if (*pat == '*') { if (m_wantsPositions && qmark) { m_wildLen[m_wildCount-1] = str - (text + m_wildStart[m_wildCount-1]); qmark = false; } if (m_wantsPositions && !star) { ExpandArray(); m_wildStart[m_wildCount-1] = str - text; m_wildLen[m_wildCount-1] = 0; star = true; } if (*++pat == '\0') { if (m_wantsPositions && star) { m_wildLen[m_wildCount-1] = strlen(str); } return true; } wpos = pat; spos = str + 1; } else if (*pat == '?' || (*pat == '#' && strchr("0123456789", *str))) { if (m_wantsPositions && !qmark) { ExpandArray(); m_wildStart[m_wildCount-1] = str - text; m_wildLen[m_wildCount-1] = 0; qmark = true; } pat++; str++; } else if (tolower(*pat) == tolower(*str)) { if (m_wantsPositions && qmark) { m_wildLen[m_wildCount-1] = str - (text + m_wildStart[m_wildCount-1]); qmark = false; } else if (m_wantsPositions && star) { m_wildLen[m_wildCount-1] = str - (text + m_wildStart[m_wildCount-1]); star = false; } pat++; str++; } else { if (m_wantsPositions && qmark) { m_wildCount--; qmark = false; } pat = wpos; str = spos++; star = true; } } if (m_wantsPositions && qmark) { m_wildLen[m_wildCount-1] = str - (text + m_wildStart[m_wildCount-1]); } if (*pat == '*' && m_wantsPositions && !star) { ExpandArray(); m_wildStart[m_wildCount-1] = str - text; m_wildLen[m_wildCount-1] = strlen(str); } while (*pat == '*') { pat++; } return *pat == '\0'; } #ifndef DISABLE_GZIP uint32 ZLib::GZipLen(int inputBufferLength) { z_stream zstr{0}; return (uint32)deflateBound(&zstr, inputBufferLength); } uint32 ZLib::GZip(const void* inputBuffer, int inputBufferLength, void* outputBuffer, int outputBufferLength) { z_stream zstr; zstr.zalloc = Z_NULL; zstr.zfree = Z_NULL; zstr.opaque = Z_NULL; zstr.next_in = (Bytef*)inputBuffer; zstr.avail_in = inputBufferLength; zstr.next_out = (Bytef*)outputBuffer; zstr.avail_out = outputBufferLength; /* add 16 to MAX_WBITS to enforce gzip format */ if (Z_OK != deflateInit2(&zstr, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + 16, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY)) { return 0; } uint32 total_out = 0; if (deflate(&zstr, Z_FINISH) == Z_STREAM_END) { total_out = (uint32)zstr.total_out; } deflateEnd(&zstr); return total_out; } GUnzipStream::GUnzipStream(int BufferSize) : m_bufferSize(BufferSize) { m_outputBuffer = std::make_unique(BufferSize); /* add 16 to MAX_WBITS to enforce gzip format */ int ret = inflateInit2(&m_zStream, MAX_WBITS + 16); m_active = ret == Z_OK; } GUnzipStream::~GUnzipStream() { if (m_active) { inflateEnd(&m_zStream); } } void GUnzipStream::Write(const void *inputBuffer, int inputBufferLength) { m_zStream.next_in = (Bytef*)inputBuffer; m_zStream.avail_in = inputBufferLength; } GUnzipStream::EStatus GUnzipStream::Read(const void **outputBuffer, int *outputBufferLength) { m_zStream.next_out = (Bytef*)m_outputBuffer.get(); m_zStream.avail_out = m_bufferSize; *outputBufferLength = 0; if (!m_active) { return zlError; } int ret = inflate(&m_zStream, Z_NO_FLUSH); switch (ret) { case Z_STREAM_END: case Z_OK: *outputBufferLength = m_bufferSize - m_zStream.avail_out; *outputBuffer = m_outputBuffer.get(); return ret == Z_STREAM_END ? zlFinished : zlOK; case Z_BUF_ERROR: return zlOK; } return zlError; } #endif Tokenizer::Tokenizer(const char* dataString, const char* separators) : m_separators(separators) { // an optimization to avoid memory allocation for short data string int len = strlen(dataString); if (len < m_shortString.Capacity()) { m_shortString.Set(dataString); m_dataString = m_shortString; } else { m_longString.Set(dataString); m_dataString = m_longString; } } Tokenizer::Tokenizer(char* dataString, const char* separators, bool inplaceBuf) : m_separators(separators) { if (inplaceBuf) { m_dataString = dataString; } else { m_longString.Set(dataString); m_dataString = m_longString; } } char* Tokenizer::Next() { char* token = nullptr; while (!token || !*token) { token = strtok_r(m_working ? nullptr : m_dataString, m_separators, &m_savePtr); m_working = true; if (!token) { return nullptr; } token = Util::Trim(token); } return token; } nzbget-19.1/daemon/util/Log.cpp0000644000175000017500000002257113130203062016213 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "Options.h" #include "Log.h" #include "Util.h" #include "FileSystem.h" Log::Log() { g_Log = this; #ifdef DEBUG m_extraDebug = FileSystem::FileExists("extradebug"); #endif } Log::~Log() { g_Log = nullptr; } void Log::LogDebugInfo() { info("--------------------------------------------"); info("Dumping debug info to log"); info("--------------------------------------------"); Guard guard(m_debugMutex); for (Debuggable* debuggable : m_debuggables) { debuggable->LogDebugInfo(); } info("--------------------------------------------"); } void Log::Filelog(const char* msg, ...) { if (m_logFilename.Empty()) { return; } char tmp2[1024]; va_list ap; va_start(ap, msg); vsnprintf(tmp2, 1024, msg, ap); tmp2[1024-1] = '\0'; va_end(ap); time_t rawtime = Util::CurrentTime() + g_Options->GetTimeCorrection(); char time[50]; Util::FormatTime(rawtime, time, 50); if ((int)rawtime/86400 != (int)m_lastWritten/86400 && g_Options->GetWriteLog() == Options::wlRotate) { RotateLog(); } m_lastWritten = rawtime; DiskFile file; if (file.Open(m_logFilename, DiskFile::omAppend)) { #ifdef WIN32 uint64 processId = GetCurrentProcessId(); uint64 threadId = GetCurrentThreadId(); #else uint64 processId = (uint64)getpid(); uint64 threadId = (uint64)pthread_self(); #endif #ifdef DEBUG file.Print("%s\t%llu\t%llu\t%s%s", time, processId, threadId, tmp2, LINE_ENDING); #else file.Print("%s\t%s%s", time, tmp2, LINE_ENDING); #endif file.Close(); } else { perror(m_logFilename); } } #ifdef DEBUG #undef debug #ifdef HAVE_VARIADIC_MACROS void debug(const char* filename, const char* funcname, int lineNr, const char* msg, ...) #else void debug(const char* msg, ...) #endif { char tmp1[1024]; va_list ap; va_start(ap, msg); vsnprintf(tmp1, 1024, msg, ap); tmp1[1024-1] = '\0'; va_end(ap); BString<1024> tmp2; #ifdef HAVE_VARIADIC_MACROS if (funcname) { tmp2.Format("%s (%s:%i:%s)", tmp1, FileSystem::BaseFileName(filename), lineNr, funcname); } else { tmp2.Format("%s (%s:%i)", tmp1, FileSystem::BaseFileName(filename), lineNr); } #else tmp2.Format("%s", tmp1); #endif Guard guard(g_Log->m_logMutex); if (!g_Options && g_Log->m_extraDebug) { printf("%s\n", *tmp2); } Options::EMessageTarget messageTarget = g_Options ? g_Options->GetDebugTarget() : Options::mtScreen; if (messageTarget == Options::mtScreen || messageTarget == Options::mtBoth) { g_Log->AddMessage(Message::mkDebug, tmp2); } if (messageTarget == Options::mtLog || messageTarget == Options::mtBoth) { g_Log->Filelog("DEBUG\t%s", *tmp2); } } #endif void error(const char* msg, ...) { char tmp2[1024]; va_list ap; va_start(ap, msg); vsnprintf(tmp2, 1024, msg, ap); tmp2[1024-1] = '\0'; va_end(ap); Guard guard(g_Log->m_logMutex); Options::EMessageTarget messageTarget = g_Options ? g_Options->GetErrorTarget() : Options::mtBoth; if (messageTarget == Options::mtScreen || messageTarget == Options::mtBoth) { g_Log->AddMessage(Message::mkError, tmp2); } if (messageTarget == Options::mtLog || messageTarget == Options::mtBoth) { g_Log->Filelog("ERROR\t%s", tmp2); } } void warn(const char* msg, ...) { char tmp2[1024]; va_list ap; va_start(ap, msg); vsnprintf(tmp2, 1024, msg, ap); tmp2[1024-1] = '\0'; va_end(ap); Guard guard(g_Log->m_logMutex); Options::EMessageTarget messageTarget = g_Options ? g_Options->GetWarningTarget() : Options::mtScreen; if (messageTarget == Options::mtScreen || messageTarget == Options::mtBoth) { g_Log->AddMessage(Message::mkWarning, tmp2); } if (messageTarget == Options::mtLog || messageTarget == Options::mtBoth) { g_Log->Filelog("WARNING\t%s", tmp2); } } void info(const char* msg, ...) { char tmp2[1024]; va_list ap; va_start(ap, msg); vsnprintf(tmp2, 1024, msg, ap); tmp2[1024-1] = '\0'; va_end(ap); Guard guard(g_Log->m_logMutex); Options::EMessageTarget messageTarget = g_Options ? g_Options->GetInfoTarget() : Options::mtScreen; if (messageTarget == Options::mtScreen || messageTarget == Options::mtBoth) { g_Log->AddMessage(Message::mkInfo, tmp2); } if (messageTarget == Options::mtLog || messageTarget == Options::mtBoth) { g_Log->Filelog("INFO\t%s", tmp2); } } void detail(const char* msg, ...) { char tmp2[1024]; va_list ap; va_start(ap, msg); vsnprintf(tmp2, 1024, msg, ap); tmp2[1024-1] = '\0'; va_end(ap); Guard guard(g_Log->m_logMutex); Options::EMessageTarget messageTarget = g_Options ? g_Options->GetDetailTarget() : Options::mtScreen; if (messageTarget == Options::mtScreen || messageTarget == Options::mtBoth) { g_Log->AddMessage(Message::mkDetail, tmp2); } if (messageTarget == Options::mtLog || messageTarget == Options::mtBoth) { g_Log->Filelog("DETAIL\t%s", tmp2); } } void Log::Clear() { Guard guard(m_logMutex); m_messages.clear(); } void Log::AddMessage(Message::EKind kind, const char * text) { m_messages.emplace_back(++m_idGen, kind, Util::CurrentTime(), text); if (m_optInit && g_Options) { while (m_messages.size() > (uint32)g_Options->GetLogBufferSize()) { m_messages.pop_front(); } } } void Log::ResetLog() { FileSystem::DeleteFile(g_Options->GetLogFile()); } void Log::RotateLog() { BString<1024> directory = g_Options->GetLogFile(); // split the full filename into path, basename and extension char* baseName = FileSystem::BaseFileName(directory); if (baseName > directory) { baseName[-1] = '\0'; } BString<1024> baseExt; char* ext = strrchr(baseName, '.'); if (ext && ext > baseName) { baseExt = ext; ext[0] = '\0'; } BString<1024> fileMask("%s-####-##-##%s", baseName, *baseExt); time_t curTime = Util::CurrentTime() + g_Options->GetTimeCorrection(); int curDay = (int)curTime / 86400; BString<1024> fullFilename; WildMask mask(fileMask, true); DirBrowser dir(directory); while (const char* filename = dir.Next()) { if (mask.Match(filename)) { fullFilename.Format("%s%c%s", *directory, PATH_SEPARATOR, filename); struct tm tm; memset(&tm, 0, sizeof(tm)); tm.tm_year = atoi(filename + mask.GetMatchStart(0)) - 1900; tm.tm_mon = atoi(filename + mask.GetMatchStart(1)) - 1; tm.tm_mday = atoi(filename + mask.GetMatchStart(2)); time_t fileTime = Util::Timegm(&tm); int fileDay = (int)fileTime / 86400; if (fileDay <= curDay - g_Options->GetRotateLog()) { BString<1024> message("Deleting old log-file %s\n", filename); g_Log->AddMessage(Message::mkInfo, message); FileSystem::DeleteFile(fullFilename); } } } struct tm tm; gmtime_r(&curTime, &tm); fullFilename.Format("%s%c%s-%i-%.2i-%.2i%s", *directory, PATH_SEPARATOR, baseName, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, *baseExt); m_logFilename = fullFilename; } /* * During intializing stage (when options were not read yet) all messages * are saved in screen log, even if they shouldn't (according to options). * Method "InitOptions()" check all messages added to screen log during * intializing stage and does three things: * 1) save the messages to log-file (if they should according to options); * 2) delete messages from screen log (if they should not be saved in screen log). * 3) renumerate IDs */ void Log::InitOptions() { const char* messageType[] = { "INFO", "WARNING", "ERROR", "DEBUG", "DETAIL"}; if (g_Options->GetWriteLog() != Options::wlNone && g_Options->GetLogFile()) { m_logFilename = g_Options->GetLogFile(); if (g_Options->GetServerMode() && g_Options->GetWriteLog() == Options::wlReset) { g_Log->ResetLog(); } } m_idGen = 0; for (uint32 i = 0; i < m_messages.size(); ) { Message& message = m_messages.at(i); Options::EMessageTarget target = Options::mtNone; switch (message.GetKind()) { case Message::mkDebug: target = g_Options->GetDebugTarget(); break; case Message::mkDetail: target = g_Options->GetDetailTarget(); break; case Message::mkInfo: target = g_Options->GetInfoTarget(); break; case Message::mkWarning: target = g_Options->GetWarningTarget(); break; case Message::mkError: target = g_Options->GetErrorTarget(); break; } if (target == Options::mtLog || target == Options::mtBoth) { Filelog("%s\t%s", messageType[message.GetKind()], message.GetText()); } if (target == Options::mtLog || target == Options::mtNone) { m_messages.erase(m_messages.begin() + i); } else { message.m_id = ++m_idGen; i++; } } m_optInit = true; } void Log::RegisterDebuggable(Debuggable* debuggable) { Guard guard(m_debugMutex); m_debuggables.push_back(debuggable); } void Log::UnregisterDebuggable(Debuggable* debuggable) { Guard guard(m_debugMutex); m_debuggables.remove(debuggable); } nzbget-19.1/daemon/windows/0000755000175000017500000000000013130203062015474 5ustar andreasandreasnzbget-19.1/daemon/feed/0000755000175000017500000000000013130203062014705 5ustar andreasandreasnzbget-19.1/daemon/feed/FeedFilter.cpp0000644000175000017500000005313313130203062017427 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #include "nzbget.h" #include "Log.h" #include "DownloadInfo.h" #include "Util.h" #include "FeedFilter.h" bool FeedFilter::Term::Match(FeedItemInfo& feedItemInfo) { const char* strValue = nullptr; int64 intValue = 0; if (!GetFieldData(m_field, &feedItemInfo, &strValue, &intValue)) { return false; } bool match = MatchValue(strValue, intValue); if (m_positive != match) { return false; } return true; } bool FeedFilter::Term::MatchValue(const char* strValue, int64 intValue) { double fFloatValue = (double)intValue; BString<100> intBuf; if (m_command < fcEqual && !strValue) { intBuf.Format("%lld", intValue); strValue = intBuf; } else if (m_command >= fcEqual && strValue) { fFloatValue = atof(strValue); intValue = (int64)fFloatValue; } switch (m_command) { case fcText: return MatchText(strValue); case fcRegex: return MatchRegex(strValue); case fcEqual: return m_float ? fFloatValue == m_floatParam : intValue == m_intParam; case fcLess: return m_float ? fFloatValue < m_floatParam : intValue < m_intParam; case fcLessEqual: return m_float ? fFloatValue <= m_floatParam : intValue <= m_intParam; case fcGreater: return m_float ? fFloatValue > m_floatParam : intValue > m_intParam; case fcGreaterEqual: return m_float ? fFloatValue >= m_floatParam : intValue >= m_intParam; default: return false; } } bool FeedFilter::Term::MatchText(const char* strValue) { const char* WORD_SEPARATORS = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; // first check if we should make word-search or substring-search int paramLen = strlen(m_param); bool substr = paramLen >= 2 && m_param[0] == '*' && m_param[paramLen-1] == '*'; if (!substr) { for (const char* p = m_param; *p; p++) { char ch = *p; if (strchr(WORD_SEPARATORS, ch) && ch != '*' && ch != '?' && ch != '#') { substr = true; break; } } } bool match = false; if (!substr) { // Word-search // split szStrValue into tokens Tokenizer tok(strValue, WORD_SEPARATORS); while (const char* word = tok.Next()) { WildMask mask(m_param, m_refValues != nullptr); match = mask.Match(word); if (match) { FillWildMaskRefValues(word, &mask, 0); break; } } } else { // Substring-search int refOffset = 1; const char* format = "*%s*"; if (paramLen >= 2 && m_param[0] == '*' && m_param[paramLen-1] == '*') { format = "%s"; refOffset = 0; } else if (paramLen >= 1 && m_param[0] == '*') { format = "%s*"; refOffset = 0; } else if (paramLen >= 1 && m_param[paramLen-1] == '*') { format = "*%s"; } WildMask mask(CString::FormatStr(format, *m_param), m_refValues != nullptr); match = mask.Match(strValue); if (match) { FillWildMaskRefValues(strValue, &mask, refOffset); } } return match; } bool FeedFilter::Term::MatchRegex(const char* strValue) { if (!m_regEx) { m_regEx = std::make_unique(m_param, m_refValues == nullptr ? 0 : 100); } bool found = m_regEx->Match(strValue); if (found) { FillRegExRefValues(strValue, m_regEx.get()); } return found; } bool FeedFilter::Term::Compile(char* token) { debug("Token: %s", token); char ch = token[0]; m_positive = ch != '-'; if (ch == '-' || ch == '+') { token++; ch = token[0]; } char ch2= token[1]; if ((ch == '(' || ch == ')' || ch == '|') && (ch2 == ' ' || ch2 == '\0')) { switch (ch) { case '(': m_command = fcOpeningBrace; return true; case ')': m_command = fcClosingBrace; return true; case '|': m_command = fcOrOperator; return true; } } char *field = nullptr; m_command = fcText; char* colon = nullptr; if (ch != '@' && ch != '$' && ch != '<' && ch != '>' && ch != '=') { colon = strchr(token, ':'); } if (colon) { field = token; colon[0] = '\0'; token = colon + 1; ch = token[0]; } if (ch == '\0') { return false; } ch2= token[1]; if (ch == '@') { m_command = fcText; token++; } else if (ch == '$') { m_command = fcRegex; token++; } else if (ch == '=') { m_command = fcEqual; token++; } else if (ch == '<' && ch2 == '=') { m_command = fcLessEqual; token += 2; } else if (ch == '>' && ch2 == '=') { m_command = fcGreaterEqual; token += 2; } else if (ch == '<') { m_command = fcLess; token++; } else if (ch == '>') { m_command = fcGreater; token++; } debug("%s, Field: %s, Command: %i, Param: %s", (m_positive ? "Positive" : "Negative"), field, m_command, token); const char* strValue; int64 intValue; if (!GetFieldData(field, nullptr, &strValue, &intValue)) { return false; } if (field && !ParseParam(field, token)) { return false; } m_field = field; m_param = token; return true; } /* * If pFeedItemInfo is nullptr, only field name is validated */ bool FeedFilter::Term::GetFieldData(const char* field, FeedItemInfo* feedItemInfo, const char** StrValue, int64* IntValue) { *StrValue = nullptr; *IntValue = 0; if (!field || !strcasecmp(field, "title")) { *StrValue = feedItemInfo ? feedItemInfo->GetTitle() : nullptr; return true; } else if (!strcasecmp(field, "filename")) { *StrValue = feedItemInfo ? feedItemInfo->GetFilename() : nullptr; return true; } else if (!strcasecmp(field, "category")) { *StrValue = feedItemInfo ? feedItemInfo->GetCategory() : nullptr; return true; } else if (!strcasecmp(field, "link") || !strcasecmp(field, "url")) { *StrValue = feedItemInfo ? feedItemInfo->GetUrl() : nullptr; return true; } else if (!strcasecmp(field, "size")) { *IntValue = feedItemInfo ? feedItemInfo->GetSize() : 0; return true; } else if (!strcasecmp(field, "age")) { *IntValue = feedItemInfo ? Util::CurrentTime() - feedItemInfo->GetTime() : 0; return true; } else if (!strcasecmp(field, "imdbid")) { *IntValue = feedItemInfo ? feedItemInfo->GetImdbId() : 0; return true; } else if (!strcasecmp(field, "rageid")) { *IntValue = feedItemInfo ? feedItemInfo->GetRageId() : 0; return true; } else if (!strcasecmp(field, "tvdbid")) { *IntValue = feedItemInfo ? feedItemInfo->GetTvdbId() : 0; return true; } else if (!strcasecmp(field, "tvmazeid")) { *IntValue = feedItemInfo ? feedItemInfo->GetTvmazeId() : 0; return true; } else if (!strcasecmp(field, "description")) { *StrValue = feedItemInfo ? feedItemInfo->GetDescription() : nullptr; return true; } else if (!strcasecmp(field, "season")) { *IntValue = feedItemInfo ? feedItemInfo->GetSeasonNum() : 0; return true; } else if (!strcasecmp(field, "episode")) { *IntValue = feedItemInfo ? feedItemInfo->GetEpisodeNum() : 0; return true; } else if (!strcasecmp(field, "priority")) { *IntValue = feedItemInfo ? feedItemInfo->GetPriority() : 0; return true; } else if (!strcasecmp(field, "dupekey")) { *StrValue = feedItemInfo ? feedItemInfo->GetDupeKey() : nullptr; return true; } else if (!strcasecmp(field, "dupescore")) { *IntValue = feedItemInfo ? feedItemInfo->GetDupeScore() : 0; return true; } else if (!strcasecmp(field, "dupestatus")) { *StrValue = feedItemInfo ? feedItemInfo->GetDupeStatus() : nullptr; return true; } else if (!strncasecmp(field, "attr-", 5)) { if (feedItemInfo) { FeedItemInfo::Attr* attr = feedItemInfo->GetAttributes()->Find(field + 5); *StrValue = attr ? attr->GetValue() : nullptr; } return true; } return false; } bool FeedFilter::Term::ParseParam(const char* field, const char* param) { if (!strcasecmp(field, "size")) { return ParseSizeParam(param); } else if (!strcasecmp(field, "age")) { return ParseAgeParam(param); } else if (m_command >= fcEqual) { return ParseNumericParam(param); } return true; } bool FeedFilter::Term::ParseSizeParam(const char* param) { double fParam = atof(param); const char* p; for (p = param; *p && ((*p >= '0' && *p <='9') || *p == '.'); p++) ; if (*p) { if (!strcasecmp(p, "K") || !strcasecmp(p, "KB")) { m_intParam = (int64)(fParam*1024); } else if (!strcasecmp(p, "M") || !strcasecmp(p, "MB")) { m_intParam = (int64)(fParam*1024*1024); } else if (!strcasecmp(p, "G") || !strcasecmp(p, "GB")) { m_intParam = (int64)(fParam*1024*1024*1024); } else { return false; } } else { m_intParam = (int64)fParam; } return true; } bool FeedFilter::Term::ParseAgeParam(const char* param) { double fParam = atof(param); const char* p; for (p = param; *p && ((*p >= '0' && *p <='9') || *p == '.'); p++) ; if (*p) { if (!strcasecmp(p, "m")) { // minutes m_intParam = (int64)(fParam*60); } else if (!strcasecmp(p, "h")) { // hours m_intParam = (int64)(fParam*60*60); } else if (!strcasecmp(p, "d")) { // days m_intParam = (int64)(fParam*60*60*24); } else { return false; } } else { // days by default m_intParam = (int64)(fParam*60*60*24); } return true; } bool FeedFilter::Term::ParseNumericParam(const char* param) { m_floatParam = atof(param); m_intParam = (int64)m_floatParam; m_float = strchr(param, '.'); const char* p; for (p = param; *p && ((*p >= '0' && *p <='9') || *p == '.' || *p == '-') ; p++) ; if (*p) { return false; } return true; } void FeedFilter::Term::FillWildMaskRefValues(const char* strValue, WildMask* mask, int refOffset) { if (!m_refValues) { return; } for (int i = refOffset; i < mask->GetMatchCount(); i++) { m_refValues->emplace_back(strValue + mask->GetMatchStart(i), mask->GetMatchLen(i)); } } void FeedFilter::Term::FillRegExRefValues(const char* strValue, RegEx* regEx) { if (!m_refValues) { return; } for (int i = 1; i < regEx->GetMatchCount(); i++) { m_refValues->emplace_back(strValue + regEx->GetMatchStart(i), regEx->GetMatchLen(i)); } } void FeedFilter::Rule::Compile(char* rule) { debug("Compiling rule: %s", rule); m_isValid = true; char* filter3 = Util::Trim(rule); char* term = CompileCommand(filter3); if (!term) { m_isValid = false; return; } if (m_command == frComment) { return; } term = Util::Trim(term); for (char* p = term; *p && m_isValid; p++) { char ch = *p; if (ch == ' ') { *p = '\0'; m_isValid = CompileTerm(term); term = p + 1; while (*term == ' ') term++; p = term; } } m_isValid = m_isValid && CompileTerm(term); if (m_isValid && m_hasPatCategory) { m_patCategory.Bind(m_category.Unbind()); } if (m_isValid && m_hasPatDupeKey) { m_patDupeKey.Bind(m_dupeKey.Unbind()); } if (m_isValid && m_hasPatAddDupeKey) { m_patAddDupeKey.Bind(m_addDupeKey.Unbind()); } } /* Checks if the rule starts with command and compiles it. * Returns a pointer to the next (first) term or nullptr in a case of compilation error. */ char* FeedFilter::Rule::CompileCommand(char* rule) { if (!strncasecmp(rule, "A:", 2) || !strncasecmp(rule, "Accept:", 7) || !strncasecmp(rule, "A(", 2) || !strncasecmp(rule, "Accept(", 7)) { m_command = frAccept; rule += rule[1] == ':' || rule[1] == '(' ? 2 : 7; } else if (!strncasecmp(rule, "O(", 2) || !strncasecmp(rule, "Options(", 8)) { m_command = frOptions; rule += rule[1] == ':' || rule[1] == '(' ? 2 : 8; } else if (!strncasecmp(rule, "R:", 2) || !strncasecmp(rule, "Reject:", 7)) { m_command = frReject; rule += rule[1] == ':' || rule[1] == '(' ? 2 : 7; } else if (!strncasecmp(rule, "Q:", 2) || !strncasecmp(rule, "Require:", 8)) { m_command = frRequire; rule += rule[1] == ':' || rule[1] == '(' ? 2 : 8; } else if (*rule == '#') { m_command = frComment; return rule; } else { // not a command return rule; } if ((m_command == frAccept || m_command == frOptions) && rule[-1] == '(') { rule = CompileOptions(rule); } return rule; } char* FeedFilter::Rule::CompileOptions(char* rule) { char* p = strchr(rule, ')'); if (!p) { // error return nullptr; } // split command into tokens *p = '\0'; Tokenizer tok(rule, ",", true); while (char* option = tok.Next()) { const char* value = ""; char* colon = strchr(option, ':'); if (colon) { *colon = '\0'; value = Util::Trim(colon + 1); } if (!strcasecmp(option, "category") || !strcasecmp(option, "cat") || !strcasecmp(option, "c")) { m_hasCategory = true; m_category = value; m_hasPatCategory = strstr(value, "${"); } else if (!strcasecmp(option, "pause") || !strcasecmp(option, "p")) { m_hasPause = true; m_pause = !*value || !strcasecmp(value, "yes") || !strcasecmp(value, "y"); if (!m_pause && !(!strcasecmp(value, "no") || !strcasecmp(value, "n"))) { // error return nullptr; } } else if (!strcasecmp(option, "priority") || !strcasecmp(option, "pr") || !strcasecmp(option, "r")) { if (!strchr("0123456789-+", *value)) { // error return nullptr; } m_hasPriority = true; m_priority = atoi(value); } else if (!strcasecmp(option, "priority+") || !strcasecmp(option, "pr+") || !strcasecmp(option, "r+")) { if (!strchr("0123456789-+", *value)) { // error return nullptr; } m_hasAddPriority = true; m_addPriority = atoi(value); } else if (!strcasecmp(option, "dupescore") || !strcasecmp(option, "ds") || !strcasecmp(option, "s")) { if (!strchr("0123456789-+", *value)) { // error return nullptr; } m_hasDupeScore = true; m_dupeScore = atoi(value); } else if (!strcasecmp(option, "dupescore+") || !strcasecmp(option, "ds+") || !strcasecmp(option, "s+")) { if (!strchr("0123456789-+", *value)) { // error return nullptr; } m_hasAddDupeScore = true; m_addDupeScore = atoi(value); } else if (!strcasecmp(option, "dupekey") || !strcasecmp(option, "dk") || !strcasecmp(option, "k")) { m_hasDupeKey = true; m_dupeKey = value; m_hasPatDupeKey = strstr(value, "${"); } else if (!strcasecmp(option, "dupekey+") || !strcasecmp(option, "dk+") || !strcasecmp(option, "k+")) { m_hasAddDupeKey = true; m_addDupeKey = value; m_hasPatAddDupeKey = strstr(value, "${"); } else if (!strcasecmp(option, "dupemode") || !strcasecmp(option, "dm") || !strcasecmp(option, "m")) { m_hasDupeMode = true; if (!strcasecmp(value, "score") || !strcasecmp(value, "s")) { m_dupeMode = dmScore; } else if (!strcasecmp(value, "all") || !strcasecmp(value, "a")) { m_dupeMode = dmAll; } else if (!strcasecmp(value, "force") || !strcasecmp(value, "f")) { m_dupeMode = dmForce; } else { // error return nullptr; } } else if (!strcasecmp(option, "rageid")) { m_hasRageId = true; m_rageId = value; } else if (!strcasecmp(option, "tvdbid")) { m_hasTvdbId = true; m_tvdbId = value; } else if (!strcasecmp(option, "tvmazeid")) { m_hasTvmazeId = true; m_tvmazeId = value; } else if (!strcasecmp(option, "series")) { m_hasSeries = true; m_series = value; } // for compatibility with older version we support old commands too else if (!strcasecmp(option, "paused") || !strcasecmp(option, "unpaused")) { m_hasPause = true; m_pause = !strcasecmp(option, "paused"); } else if (strchr("0123456789-+", *option)) { m_hasPriority = true; m_priority = atoi(option); } else { m_hasCategory = true; m_category = option; } } rule = p + 1; if (*rule == ':') { rule++; } return rule; } bool FeedFilter::Rule::CompileTerm(char* termstr) { m_terms.emplace_back(); m_terms.back().SetRefValues(m_hasPatCategory || m_hasPatDupeKey || m_hasPatAddDupeKey ? &m_refValues : nullptr); bool ok = m_terms.back().Compile(termstr); if (!ok) { m_terms.pop_back(); } return ok; } bool FeedFilter::Rule::Match(FeedItemInfo& feedItemInfo) { m_refValues.clear(); if (!MatchExpression(feedItemInfo)) { return false; } if (m_hasPatCategory) { ExpandRefValues(feedItemInfo, &m_category, m_patCategory); } if (m_hasPatDupeKey) { ExpandRefValues(feedItemInfo, &m_dupeKey, m_patDupeKey); } if (m_hasPatAddDupeKey) { ExpandRefValues(feedItemInfo, &m_addDupeKey, m_patAddDupeKey); } return true; } bool FeedFilter::Rule::MatchExpression(FeedItemInfo& feedItemInfo) { CString expr; expr.Reserve(m_terms.size()); int index = 0; for (Term& term : m_terms) { switch (term.GetCommand()) { case fcOpeningBrace: expr[index] = '('; break; case fcClosingBrace: expr[index] = ')'; break; case fcOrOperator: expr[index] = '|'; break; default: expr[index] = term.Match(feedItemInfo) ? 'T' : 'F'; break; } index++; } expr[index] = '\0'; // reduce result tree to one element (may be longer if expression has syntax errors) for (int oldLen = 0, newLen = strlen(expr); newLen != oldLen; oldLen = newLen, newLen = strlen(expr)) { // NOTE: there are no operator priorities. // the order of operators "OR" and "AND" is not defined, they can be checked in any order. // "OR" and "AND" should not be mixed in one group; instead braces should be used to define priorities. Util::ReduceStr(expr, "TT", "T"); Util::ReduceStr(expr, "TF", "F"); Util::ReduceStr(expr, "FT", "F"); Util::ReduceStr(expr, "FF", "F"); Util::ReduceStr(expr, "||", "|"); Util::ReduceStr(expr, "(|", "("); Util::ReduceStr(expr, "|)", ")"); Util::ReduceStr(expr, "T|T", "T"); Util::ReduceStr(expr, "T|F", "T"); Util::ReduceStr(expr, "F|T", "T"); Util::ReduceStr(expr, "F|F", "F"); Util::ReduceStr(expr, "(T)", "T"); Util::ReduceStr(expr, "(F)", "F"); } bool match = expr.Length() == 1 && expr[0] == 'T'; return match; } void FeedFilter::Rule::ExpandRefValues(FeedItemInfo& feedItemInfo, CString* destStr, const char* patStr) { CString curvalue = patStr; int attempts = 0; while (const char* dollar = strstr(curvalue, "${")) { attempts++; if (attempts > 100) { break; // error } const char* end = strchr(dollar, '}'); if (!end) { break; // error } int varlen = (int)(end - dollar - 2); BString<100> variable; variable.Set(dollar + 2, varlen); const char* varvalue = GetRefValue(feedItemInfo, variable); if (!varvalue) { break; // error } curvalue.Replace(dollar - curvalue, 2 + varlen + 1, varvalue); } *destStr = std::move(curvalue); } const char* FeedFilter::Rule::GetRefValue(FeedItemInfo& feedItemInfo, const char* varName) { if (!strcasecmp(varName, "season")) { feedItemInfo.GetSeasonNum(); // needed to parse title return feedItemInfo.GetSeason() ? feedItemInfo.GetSeason() : ""; } else if (!strcasecmp(varName, "episode")) { feedItemInfo.GetEpisodeNum(); // needed to parse title return feedItemInfo.GetEpisode() ? feedItemInfo.GetEpisode() : ""; } int index = atoi(varName) - 1; if (index >= 0 && index < (int)m_refValues.size()) { return m_refValues[index]; } return nullptr; } FeedFilter::FeedFilter(const char* filter) { Compile(filter); } void FeedFilter::Compile(const char* filter) { debug("Compiling filter: %s", filter); CString filter2 = filter; char* rule = filter2; for (char* p = rule; *p; p++) { char ch = *p; if (ch == '%') { *p = '\0'; CompileRule(rule); rule = p + 1; } } CompileRule(rule); } void FeedFilter::CompileRule(char* rulestr) { m_rules.emplace_back(); m_rules.back().Compile(rulestr); } void FeedFilter::Match(FeedItemInfo& feedItemInfo) { int index = 0; for (Rule& rule : m_rules) { index++; if (rule.IsValid()) { bool match = rule.Match(feedItemInfo); switch (rule.GetCommand()) { case frAccept: case frOptions: if (match) { feedItemInfo.SetMatchStatus(FeedItemInfo::msAccepted); feedItemInfo.SetMatchRule(index); ApplyOptions(rule, feedItemInfo); if (rule.GetCommand() == frAccept) { return; } } break; case frReject: if (match) { feedItemInfo.SetMatchStatus(FeedItemInfo::msRejected); feedItemInfo.SetMatchRule(index); return; } break; case frRequire: if (!match) { feedItemInfo.SetMatchStatus(FeedItemInfo::msRejected); feedItemInfo.SetMatchRule(index); return; } break; case frComment: break; } } } feedItemInfo.SetMatchStatus(FeedItemInfo::msIgnored); feedItemInfo.SetMatchRule(0); } void FeedFilter::ApplyOptions(Rule& rule, FeedItemInfo& feedItemInfo) { if (rule.HasPause()) { feedItemInfo.SetPauseNzb(rule.GetPause()); } if (rule.HasCategory()) { feedItemInfo.SetAddCategory(rule.GetCategory()); } if (rule.HasPriority()) { feedItemInfo.SetPriority(rule.GetPriority()); } if (rule.HasAddPriority()) { feedItemInfo.SetPriority(feedItemInfo.GetPriority() + rule.GetAddPriority()); } if (rule.HasDupeScore()) { feedItemInfo.SetDupeScore(rule.GetDupeScore()); } if (rule.HasAddDupeScore()) { feedItemInfo.SetDupeScore(feedItemInfo.GetDupeScore() + rule.GetAddDupeScore()); } if (rule.HasRageId() || rule.HasTvdbId() || rule.HasTvmazeId() || rule.HasSeries()) { feedItemInfo.BuildDupeKey(rule.GetRageId(), rule.GetTvdbId(), rule.GetTvmazeId(), rule.GetSeries()); } if (rule.HasDupeKey()) { feedItemInfo.SetDupeKey(rule.GetDupeKey()); } if (rule.HasAddDupeKey()) { feedItemInfo.AppendDupeKey(rule.GetAddDupeKey()); } if (rule.HasDupeMode()) { feedItemInfo.SetDupeMode(rule.GetDupeMode()); } } nzbget-19.1/daemon/feed/FeedCoordinator.cpp0000644000175000017500000004302413130203062020463 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #include "nzbget.h" #include "FeedCoordinator.h" #include "Options.h" #include "WebDownloader.h" #include "Util.h" #include "FileSystem.h" #include "FeedFile.h" #include "FeedFilter.h" #include "FeedScript.h" #include "DiskState.h" #include "DupeCoordinator.h" std::unique_ptr& FeedCoordinator::FilterHelper::GetRegEx(int id) { m_regExes.resize(id); return m_regExes[id - 1]; } void FeedCoordinator::FilterHelper::CalcDupeStatus(const char* title, const char* dupeKey, char* statusBuf, int bufLen) { const char* dupeStatusName[] = { "", "QUEUED", "DOWNLOADING", "3", "SUCCESS", "5", "6", "7", "WARNING", "9", "10", "11", "12", "13", "14", "15", "FAILURE" }; DupeCoordinator::EDupeStatus dupeStatus = g_DupeCoordinator->GetDupeStatus(DownloadQueue::Guard(), title, dupeKey); BString<1024> statuses; for (int i = 1; i <= (int)DupeCoordinator::dsFailure; i = i << 1) { if (dupeStatus & i) { if (!statuses.Empty()) { statuses.Append(","); } statuses.Append(dupeStatusName[i]); } } strncpy(statusBuf, statuses, bufLen); } FeedCoordinator::FeedCoordinator() { debug("Creating FeedCoordinator"); m_downloadQueueObserver.m_owner = this; DownloadQueue::Guard()->Attach(&m_downloadQueueObserver); } FeedCoordinator::~FeedCoordinator() { debug("Destroying FeedCoordinator"); for (FeedDownloader* feedDownloader : m_activeDownloads) { delete feedDownloader; } m_activeDownloads.clear(); } void FeedCoordinator::Run() { debug("Entering FeedCoordinator-loop"); while (!DownloadQueue::IsLoaded()) { usleep(20 * 1000); } if (g_Options->GetServerMode() && g_Options->GetSaveQueue() && g_Options->GetReloadQueue()) { Guard guard(m_downloadsMutex); g_DiskState->LoadFeeds(&m_feeds, &m_feedHistory); } int sleepInterval = 100; int updateCounter = 0; int cleanupCounter = 60000; while (!IsStopped()) { usleep(sleepInterval * 1000); updateCounter += sleepInterval; if (updateCounter >= 1000) { // this code should not be called too often, once per second is OK if (!g_Options->GetPauseDownload() || m_force || g_Options->GetUrlForce()) { Guard guard(m_downloadsMutex); time_t current = Util::CurrentTime(); if ((int)m_activeDownloads.size() < g_Options->GetUrlConnections()) { m_force = false; // check feed list and update feeds for (FeedInfo* feedInfo : &m_feeds) { if (((feedInfo->GetInterval() > 0 && (current - feedInfo->GetLastUpdate() >= feedInfo->GetInterval() * 60 || current < feedInfo->GetLastUpdate())) || feedInfo->GetFetch()) && feedInfo->GetStatus() != FeedInfo::fsRunning) { StartFeedDownload(feedInfo, feedInfo->GetFetch()); } else if (feedInfo->GetFetch()) { m_force = true; } } } } CheckSaveFeeds(); ResetHangingDownloads(); updateCounter = 0; } cleanupCounter += sleepInterval; if (cleanupCounter >= 60000) { // clean up feed history once a minute CleanupHistory(); CleanupCache(); CheckSaveFeeds(); cleanupCounter = 0; } } // waiting for downloads debug("FeedCoordinator: waiting for Downloads to complete"); bool completed = false; while (!completed) { { Guard guard(m_downloadsMutex); completed = m_activeDownloads.size() == 0; } CheckSaveFeeds(); usleep(100 * 1000); ResetHangingDownloads(); } debug("FeedCoordinator: Downloads are completed"); debug("Exiting FeedCoordinator-loop"); } void FeedCoordinator::Stop() { Thread::Stop(); debug("Stopping UrlDownloads"); Guard guard(m_downloadsMutex); for (FeedDownloader* feedDownloader : m_activeDownloads) { feedDownloader->Stop(); } debug("UrlDownloads are notified"); } void FeedCoordinator::ResetHangingDownloads() { const int timeout = g_Options->GetTerminateTimeout(); if (timeout == 0) { return; } Guard guard(m_downloadsMutex); time_t tm = Util::CurrentTime(); m_activeDownloads.erase(std::remove_if(m_activeDownloads.begin(), m_activeDownloads.end(), [timeout, tm](FeedDownloader* feedDownloader) { if (tm - feedDownloader->GetLastUpdateTime() > timeout && feedDownloader->GetStatus() == FeedDownloader::adRunning) { debug("Terminating hanging download %s", feedDownloader->GetInfoName()); if (feedDownloader->Terminate()) { error("Terminated hanging download %s", feedDownloader->GetInfoName()); feedDownloader->GetFeedInfo()->SetStatus(FeedInfo::fsUndefined); } else { error("Could not terminate hanging download %s", feedDownloader->GetInfoName()); } // it's not safe to destroy feedDownloader, because the state of object is unknown delete feedDownloader; return true; } return false; }), m_activeDownloads.end()); } void FeedCoordinator::LogDebugInfo() { info(" ---------- FeedCoordinator"); Guard guard(m_downloadsMutex); info(" Active Downloads: %i", (int)m_activeDownloads.size()); for (FeedDownloader* feedDownloader : m_activeDownloads) { feedDownloader->LogDebugInfo(); } } void FeedCoordinator::StartFeedDownload(FeedInfo* feedInfo, bool force) { debug("Starting new FeedDownloader for %s", feedInfo->GetName()); FeedDownloader* feedDownloader = new FeedDownloader(); feedDownloader->SetAutoDestroy(true); feedDownloader->Attach(this); feedDownloader->SetFeedInfo(feedInfo); feedDownloader->SetUrl(feedInfo->GetUrl()); feedDownloader->SetInfoName(feedInfo->GetName()); feedDownloader->SetForce(force || g_Options->GetUrlForce()); BString<1024> outFilename; if (feedInfo->GetId() > 0) { outFilename.Format("%s%cfeed-%i.tmp", g_Options->GetTempDir(), PATH_SEPARATOR, feedInfo->GetId()); } else { outFilename.Format("%s%cfeed-%i-%i.tmp", g_Options->GetTempDir(), PATH_SEPARATOR, (int)Util::CurrentTime(), rand()); } feedDownloader->SetOutputFilename(outFilename); feedInfo->SetStatus(FeedInfo::fsRunning); feedInfo->SetForce(force); feedInfo->SetFetch(false); m_activeDownloads.push_back(feedDownloader); feedDownloader->Start(); } void FeedCoordinator::Update(Subject* caller, void* aspect) { debug("Notification from FeedDownloader received"); FeedDownloader* feedDownloader = (FeedDownloader*) caller; if ((feedDownloader->GetStatus() == WebDownloader::adFinished) || (feedDownloader->GetStatus() == WebDownloader::adFailed) || (feedDownloader->GetStatus() == WebDownloader::adRetry)) { FeedCompleted(feedDownloader); } } void FeedCoordinator::FeedCompleted(FeedDownloader* feedDownloader) { debug("Feed downloaded"); FeedInfo* feedInfo = feedDownloader->GetFeedInfo(); bool statusOK = feedDownloader->GetStatus() == WebDownloader::adFinished; if (statusOK) { feedInfo->SetOutputFilename(feedDownloader->GetOutputFilename()); } // remove downloader from downloader list { Guard guard(m_downloadsMutex); m_activeDownloads.erase(std::find(m_activeDownloads.begin(), m_activeDownloads.end(), feedDownloader)); } if (statusOK) { if (!feedInfo->GetPreview()) { bool scriptSuccess = true; FeedScriptController::ExecuteScripts( !Util::EmptyStr(feedInfo->GetExtensions()) ? feedInfo->GetExtensions(): g_Options->GetExtensions(), feedInfo->GetOutputFilename(), feedInfo->GetId(), &scriptSuccess); if (!scriptSuccess) { feedInfo->SetStatus(FeedInfo::fsFailed); return; } std::unique_ptr feedFile = parseFeed(feedInfo); std::vector> addedNzbs; { Guard guard(m_downloadsMutex); if (feedFile) { std::unique_ptr feedItems = feedFile->DetachFeedItems(); addedNzbs = ProcessFeed(feedInfo, feedItems.get()); feedFile.reset(); } feedInfo->SetLastUpdate(Util::CurrentTime()); feedInfo->SetForce(false); m_save = true; } GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); for (std::unique_ptr& nzbInfo : addedNzbs) { downloadQueue->GetQueue()->Add(std::move(nzbInfo)); } downloadQueue->Save(); } feedInfo->SetStatus(FeedInfo::fsFinished); } else { feedInfo->SetStatus(FeedInfo::fsFailed); } } void FeedCoordinator::FilterFeed(FeedInfo* feedInfo, FeedItemList* feedItems) { debug("Filtering feed %s", feedInfo->GetName()); FilterHelper filterHelper; std::unique_ptr feedFilter; if (!Util::EmptyStr(feedInfo->GetFilter())) { feedFilter = std::make_unique(feedInfo->GetFilter()); } for (FeedItemInfo& feedItemInfo : feedItems) { feedItemInfo.SetMatchStatus(FeedItemInfo::msAccepted); feedItemInfo.SetMatchRule(0); feedItemInfo.SetPauseNzb(feedInfo->GetPauseNzb()); feedItemInfo.SetPriority(feedInfo->GetPriority()); feedItemInfo.SetAddCategory(feedInfo->GetCategory()); feedItemInfo.SetDupeScore(0); feedItemInfo.SetDupeMode(dmScore); feedItemInfo.SetFeedFilterHelper(&filterHelper); feedItemInfo.BuildDupeKey(nullptr, nullptr, nullptr, nullptr); if (feedFilter) { feedFilter->Match(feedItemInfo); } } } std::vector> FeedCoordinator::ProcessFeed(FeedInfo* feedInfo, FeedItemList* feedItems) { debug("Process feed %s", feedInfo->GetName()); FilterFeed(feedInfo, feedItems); std::vector> addedNzbs; bool firstFetch = feedInfo->GetLastUpdate() == 0; int added = 0; for (FeedItemInfo& feedItemInfo : feedItems) { if (feedItemInfo.GetMatchStatus() == FeedItemInfo::msAccepted) { FeedHistoryInfo* feedHistoryInfo = m_feedHistory.Find(feedItemInfo.GetUrl()); FeedHistoryInfo::EStatus status = FeedHistoryInfo::hsUnknown; if (firstFetch && feedInfo->GetBacklog()) { status = FeedHistoryInfo::hsBacklog; } else if (!feedHistoryInfo) { addedNzbs.push_back(CreateNzbInfo(feedInfo, feedItemInfo)); status = FeedHistoryInfo::hsFetched; added++; } if (feedHistoryInfo) { feedHistoryInfo->SetLastSeen(Util::CurrentTime()); } else { m_feedHistory.emplace_back(feedItemInfo.GetUrl(), status, Util::CurrentTime()); } } } if (added) { info("%s has %i new item(s)", feedInfo->GetName(), added); } else { detail("%s has no new items", feedInfo->GetName()); } return addedNzbs; } std::unique_ptr FeedCoordinator::CreateNzbInfo(FeedInfo* feedInfo, FeedItemInfo& feedItemInfo) { debug("Download %s from %s", feedItemInfo.GetUrl(), feedInfo->GetName()); std::unique_ptr nzbInfo = std::make_unique(); nzbInfo->SetKind(NzbInfo::nkUrl); nzbInfo->SetFeedId(feedInfo->GetId()); nzbInfo->SetUrl(feedItemInfo.GetUrl()); // add .nzb-extension if not present BString<1024> nzbName = feedItemInfo.GetFilename(); char* ext = strrchr(nzbName, '.'); if (ext && !strcasecmp(ext, ".nzb")) { *ext = '\0'; } if (!nzbName.Empty()) { BString<1024> nzbName2("%s.nzb", *nzbName); nzbInfo->SetFilename(FileSystem::MakeValidFilename(nzbName2)); } nzbInfo->SetCategory(feedItemInfo.GetAddCategory()); nzbInfo->SetPriority(feedItemInfo.GetPriority()); nzbInfo->SetAddUrlPaused(feedItemInfo.GetPauseNzb()); nzbInfo->SetDupeKey(feedItemInfo.GetDupeKey()); nzbInfo->SetDupeScore(feedItemInfo.GetDupeScore()); nzbInfo->SetDupeMode(feedItemInfo.GetDupeMode()); return nzbInfo; } std::shared_ptr FeedCoordinator::ViewFeed(int id) { if (id < 1 || id > (int)m_feeds.size()) { return nullptr; } std::unique_ptr& feedInfo = m_feeds[id - 1]; return PreviewFeed(feedInfo->GetId(), feedInfo->GetName(), feedInfo->GetUrl(), feedInfo->GetFilter(), feedInfo->GetBacklog(), feedInfo->GetPauseNzb(), feedInfo->GetCategory(), feedInfo->GetPriority(), feedInfo->GetInterval(), feedInfo->GetExtensions(), 0, nullptr); } std::shared_ptr FeedCoordinator::PreviewFeed(int id, const char* name, const char* url, const char* filter, bool backlog, bool pauseNzb, const char* category, int priority, int interval, const char* feedScript, int cacheTimeSec, const char* cacheId) { debug("Preview feed %s", name); std::unique_ptr feedInfo = std::make_unique(id, name, url, backlog, interval, filter, pauseNzb, category, priority, feedScript); feedInfo->SetPreview(true); std::shared_ptr feedItems; bool hasCache = false; if (cacheTimeSec > 0 && *cacheId != '\0') { Guard guard(m_downloadsMutex); for (FeedCacheItem& feedCacheItem : m_feedCache) { if (!strcmp(feedCacheItem.GetCacheId(), cacheId)) { feedCacheItem.SetLastUsage(Util::CurrentTime()); feedItems = feedCacheItem.GetFeedItems(); hasCache = true; break; } } } if (!hasCache) { bool firstFetch = true; { Guard guard(m_downloadsMutex); for (FeedInfo* feedInfo2 : &m_feeds) { if (!strcmp(feedInfo2->GetUrl(), feedInfo->GetUrl()) && !strcmp(feedInfo2->GetFilter(), feedInfo->GetFilter()) && feedInfo2->GetLastUpdate() > 0) { firstFetch = false; break; } } StartFeedDownload(feedInfo.get(), true); } // wait until the download in a separate thread completes while (feedInfo->GetStatus() == FeedInfo::fsRunning) { usleep(100 * 1000); } // now can process the feed if (feedInfo->GetStatus() != FeedInfo::fsFinished) { return nullptr; } FeedScriptController::ExecuteScripts( !Util::EmptyStr(feedInfo->GetExtensions()) ? feedInfo->GetExtensions(): g_Options->GetExtensions(), feedInfo->GetOutputFilename(), feedInfo->GetId(), nullptr); std::unique_ptr feedFile = parseFeed(feedInfo.get()); if (!feedFile) { return nullptr; } feedItems = feedFile->DetachFeedItems(); feedFile.reset(); for (FeedItemInfo& feedItemInfo : feedItems.get()) { feedItemInfo.SetStatus(firstFetch && feedInfo->GetBacklog() ? FeedItemInfo::isBacklog : FeedItemInfo::isNew); FeedHistoryInfo* feedHistoryInfo = m_feedHistory.Find(feedItemInfo.GetUrl()); if (feedHistoryInfo) { feedItemInfo.SetStatus((FeedItemInfo::EStatus)feedHistoryInfo->GetStatus()); } } } FilterFeed(feedInfo.get(), feedItems.get()); feedInfo.reset(); if (cacheTimeSec > 0 && *cacheId != '\0' && !hasCache) { Guard guard(m_downloadsMutex); m_feedCache.emplace_back(url, cacheTimeSec, cacheId, Util::CurrentTime(), feedItems); } return feedItems; } void FeedCoordinator::FetchFeed(int id) { debug("FetchFeeds"); Guard guard(m_downloadsMutex); for (FeedInfo* feedInfo : &m_feeds) { if (feedInfo->GetId() == id || id == 0) { feedInfo->SetFetch(true); m_force = true; } } } std::unique_ptr FeedCoordinator::parseFeed(FeedInfo* feedInfo) { std::unique_ptr feedFile = std::make_unique(feedInfo->GetOutputFilename(), feedInfo->GetName()); if (feedFile->Parse()) { FileSystem::DeleteFile(feedInfo->GetOutputFilename()); } else { error("Feed file %s kept for troubleshooting (will be deleted on next successful feed fetch)", feedInfo->GetOutputFilename()); feedFile.reset(); } return std::move(feedFile); } void FeedCoordinator::DownloadQueueUpdate(Subject* caller, void* aspect) { debug("Notification from URL-Coordinator received"); DownloadQueue::Aspect* queueAspect = (DownloadQueue::Aspect*)aspect; if (queueAspect->action == DownloadQueue::eaUrlCompleted) { Guard guard(m_downloadsMutex); FeedHistoryInfo* feedHistoryInfo = m_feedHistory.Find(queueAspect->nzbInfo->GetUrl()); if (feedHistoryInfo) { feedHistoryInfo->SetStatus(FeedHistoryInfo::hsFetched); } else { m_feedHistory.emplace_back(queueAspect->nzbInfo->GetUrl(), FeedHistoryInfo::hsFetched, Util::CurrentTime()); } m_save = true; } } bool FeedCoordinator::HasActiveDownloads() { Guard guard(m_downloadsMutex); return !m_activeDownloads.empty(); } void FeedCoordinator::CheckSaveFeeds() { debug("CheckSaveFeeds"); Guard guard(m_downloadsMutex); if (m_save) { if (g_Options->GetSaveQueue() && g_Options->GetServerMode()) { g_DiskState->SaveFeeds(&m_feeds, &m_feedHistory); } m_save = false; } } void FeedCoordinator::CleanupHistory() { debug("CleanupHistory"); Guard guard(m_downloadsMutex); time_t oldestUpdate = Util::CurrentTime(); for (FeedInfo* feedInfo : &m_feeds) { if (feedInfo->GetLastUpdate() < oldestUpdate) { oldestUpdate = feedInfo->GetLastUpdate(); } } time_t borderDate = oldestUpdate - g_Options->GetFeedHistory() * 60*60*24; m_feedHistory.erase(std::remove_if(m_feedHistory.begin(), m_feedHistory.end(), [borderDate, this](FeedHistoryInfo& feedHistoryInfo) { if (feedHistoryInfo.GetLastSeen() < borderDate) { detail("Deleting %s from feed history", feedHistoryInfo.GetUrl()); m_save = true; return true; } return false; }), m_feedHistory.end()); } void FeedCoordinator::CleanupCache() { debug("CleanupCache"); Guard guard(m_downloadsMutex); time_t curTime = Util::CurrentTime(); m_feedCache.remove_if( [curTime](FeedCacheItem& feedCacheItem) { if (feedCacheItem.GetLastUsage() + feedCacheItem.GetCacheTimeSec() < curTime || feedCacheItem.GetLastUsage() > curTime) { debug("Deleting %s from feed cache", feedCacheItem.GetUrl()); return true; } return false; }); } nzbget-19.1/daemon/feed/FeedFilter.h0000644000175000017500000001254413130203062017075 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #ifndef FEEDFILTER_H #define FEEDFILTER_H #include "NString.h" #include "DownloadInfo.h" #include "FeedInfo.h" #include "Util.h" class FeedFilter { public: FeedFilter(const char* filter); void Match(FeedItemInfo& feedItemInfo); private: typedef std::vector RefValues; enum ETermCommand { fcText, fcRegex, fcEqual, fcLess, fcLessEqual, fcGreater, fcGreaterEqual, fcOpeningBrace, fcClosingBrace, fcOrOperator }; class Term { public: Term() {} Term(Term&&) = delete; // catch performance issues void SetRefValues(RefValues* refValues) { m_refValues = refValues; } bool Compile(char* token); bool Match(FeedItemInfo& feedItemInfo); ETermCommand GetCommand() { return m_command; } private: bool m_positive; CString m_field; ETermCommand m_command; CString m_param; int64 m_intParam = 0; double m_floatParam = 0.0; bool m_float = false; std::unique_ptr m_regEx; RefValues* m_refValues = nullptr; bool GetFieldData(const char* field, FeedItemInfo* feedItemInfo, const char** StrValue, int64* IntValue); bool ParseParam(const char* field, const char* param); bool ParseSizeParam(const char* param); bool ParseAgeParam(const char* param); bool ParseNumericParam(const char* param); bool MatchValue(const char* strValue, int64 intValue); bool MatchText(const char* strValue); bool MatchRegex(const char* strValue); void FillWildMaskRefValues(const char* strValue, WildMask* mask, int refOffset); void FillRegExRefValues(const char* strValue, RegEx* regEx); }; typedef std::deque TermList; enum ERuleCommand { frAccept, frReject, frRequire, frOptions, frComment }; class Rule { public: Rule() {} Rule(Rule&&) = delete; // catch performance issues void Compile(char* rule); bool IsValid() { return m_isValid; } ERuleCommand GetCommand() { return m_command; } const char* GetCategory() { return m_category; } int GetPriority() { return m_priority; } int GetAddPriority() { return m_addPriority; } bool GetPause() { return m_pause; } const char* GetDupeKey() { return m_dupeKey; } const char* GetAddDupeKey() { return m_addDupeKey; } int GetDupeScore() { return m_dupeScore; } int GetAddDupeScore() { return m_addDupeScore; } EDupeMode GetDupeMode() { return m_dupeMode; } const char* GetRageId() { return m_rageId; } const char* GetTvdbId() { return m_tvdbId; } const char* GetTvmazeId() { return m_tvmazeId; } const char* GetSeries() { return m_series; } bool HasCategory() { return m_hasCategory; } bool HasPriority() { return m_hasPriority; } bool HasAddPriority() { return m_hasAddPriority; } bool HasPause() { return m_hasPause; } bool HasDupeScore() { return m_hasDupeScore; } bool HasAddDupeScore() { return m_hasAddDupeScore; } bool HasDupeKey() { return m_hasDupeKey; } bool HasAddDupeKey() { return m_hasAddDupeKey; } bool HasDupeMode() { return m_hasDupeMode; } bool HasRageId() { return m_hasRageId; } bool HasTvdbId() { return m_hasTvdbId; } bool HasTvmazeId() { return m_hasTvmazeId; } bool HasSeries() { return m_hasSeries; } bool Match(FeedItemInfo& feedItemInfo); void ExpandRefValues(FeedItemInfo& feedItemInfo, CString* destStr, const char* patStr); const char* GetRefValue(FeedItemInfo& feedItemInfo, const char* varName); private: bool m_isValid = false; ERuleCommand m_command = frAccept; CString m_category; int m_priority = 0; int m_addPriority = 0; bool m_pause = false; int m_dupeScore; int m_addDupeScore = 0; CString m_dupeKey; CString m_addDupeKey; EDupeMode m_dupeMode = dmScore; CString m_series; CString m_rageId; CString m_tvdbId; CString m_tvmazeId; bool m_hasCategory = false; bool m_hasPriority = false; bool m_hasAddPriority = false; bool m_hasPause = false; bool m_hasDupeScore = false; bool m_hasAddDupeScore = false; bool m_hasDupeKey = false; bool m_hasAddDupeKey = false; bool m_hasDupeMode = false; bool m_hasPatCategory = false; bool m_hasPatDupeKey = false; bool m_hasPatAddDupeKey = false; bool m_hasSeries = false; bool m_hasRageId = false; bool m_hasTvdbId = false; bool m_hasTvmazeId = false; CString m_patCategory; CString m_patDupeKey; CString m_patAddDupeKey; TermList m_terms; RefValues m_refValues; char* CompileCommand(char* rule); char* CompileOptions(char* rule); bool CompileTerm(char* term); bool MatchExpression(FeedItemInfo& feedItemInfo); }; typedef std::deque RuleList; RuleList m_rules; void Compile(const char* filter); void CompileRule(char* rule); void ApplyOptions(Rule& rule, FeedItemInfo& feedItemInfo); }; #endif nzbget-19.1/daemon/feed/FeedFile.cpp0000644000175000017500000003371513130203062017065 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #include "nzbget.h" #include "FeedFile.h" #include "Log.h" #include "DownloadInfo.h" #include "Options.h" #include "Util.h" FeedFile::FeedFile(const char* fileName, const char* infoName) : m_fileName(fileName), m_infoName(infoName) { debug("Creating FeedFile"); m_feedItems = std::make_unique(); #ifndef WIN32 m_feedItemInfo = nullptr; m_tagContent.Clear(); #endif } void FeedFile::LogDebugInfo() { info(" FeedFile %s", *m_fileName); } void FeedFile::ParseSubject(FeedItemInfo& feedItemInfo) { // if title has quatation marks we use only part within quatation marks char* p = (char*)feedItemInfo.GetTitle(); char* start = strchr(p, '\"'); if (start) { start++; char* end = strchr(start + 1, '\"'); if (end) { int len = (int)(end - start); char* point = strchr(start + 1, '.'); if (point && point < end) { CString filename(start, len); char* ext = strrchr(filename, '.'); if (ext && !strcasecmp(ext, ".par2")) { *ext = '\0'; } feedItemInfo.SetFilename(filename); return; } } } feedItemInfo.SetFilename(feedItemInfo.GetTitle()); } #ifdef WIN32 bool FeedFile::Parse() { CoInitialize(nullptr); HRESULT hr; MSXML::IXMLDOMDocumentPtr doc; hr = doc.CreateInstance(MSXML::CLSID_DOMDocument); if (FAILED(hr)) { return false; } // Load the XML document file... doc->put_resolveExternals(VARIANT_FALSE); doc->put_validateOnParse(VARIANT_FALSE); doc->put_async(VARIANT_FALSE); _variant_t vFilename(*WString(m_fileName)); // 1. first trying to load via filename without URL-encoding (certain charaters doesn't work when encoded) VARIANT_BOOL success = doc->load(vFilename); if (success == VARIANT_FALSE) { // 2. now trying filename encoded as URL char url[2048]; EncodeUrl(m_fileName, url, 2048); debug("url=\"%s\"", url); _variant_t vUrl(url); success = doc->load(vUrl); } if (success == VARIANT_FALSE) { _bstr_t r(doc->GetparseError()->reason); const char* errMsg = r; error("Error parsing rss feed %s: %s", *m_infoName, errMsg); return false; } bool ok = ParseFeed(doc); return ok; } void FeedFile::EncodeUrl(const char* filename, char* url, int bufLen) { WString widefilename(filename); char* end = url + bufLen; for (wchar_t* p = widefilename; *p && url < end - 3; p++) { wchar_t ch = *p; if (('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ch == '-' || ch == '.' || ch == '_' || ch == '~') { *url++ = (char)ch; } else { *url++ = '%'; uint32 a = (uint32)ch >> 4; *url++ = a > 9 ? a - 10 + 'A' : a + '0'; a = ch & 0xF; *url++ = a > 9 ? a - 10 + 'A' : a + '0'; } } *url = '\0'; } bool FeedFile::ParseFeed(IUnknown* nzb) { MSXML::IXMLDOMDocumentPtr doc = nzb; MSXML::IXMLDOMNodePtr root = doc->documentElement; MSXML::IXMLDOMNodeListPtr itemList = root->selectNodes("/rss/channel/item"); for (int i = 0; i < itemList->Getlength(); i++) { MSXML::IXMLDOMNodePtr node = itemList->Getitem(i); m_feedItems->emplace_back(); FeedItemInfo& feedItemInfo = m_feedItems->back(); MSXML::IXMLDOMNodePtr tag; MSXML::IXMLDOMNodePtr attr; // Debian 6 tag = node->selectSingleNode("title"); if (!tag) { // bad rss feed return false; } _bstr_t title(tag->Gettext()); feedItemInfo.SetTitle(title); ParseSubject(feedItemInfo); // Wed, 26 Jun 2013 00:02:54 -0600 tag = node->selectSingleNode("pubDate"); if (tag) { _bstr_t time(tag->Gettext()); time_t unixtime = WebUtil::ParseRfc822DateTime(time); if (unixtime > 0) { feedItemInfo.SetTime(unixtime); } } // Movies > HD tag = node->selectSingleNode("category"); if (tag) { _bstr_t category(tag->Gettext()); feedItemInfo.SetCategory(category); } // long text tag = node->selectSingleNode("description"); if (tag) { _bstr_t bdescription(tag->Gettext()); // cleanup CDATA CString description = (const char*)bdescription; WebUtil::XmlStripTags(description); WebUtil::XmlDecode(description); WebUtil::XmlRemoveEntities(description); feedItemInfo.SetDescription(description); } // tag = node->selectSingleNode("enclosure"); if (tag) { attr = tag->Getattributes()->getNamedItem("url"); if (attr) { _bstr_t url(attr->Gettext()); feedItemInfo.SetUrl(url); } attr = tag->Getattributes()->getNamedItem("length"); if (attr) { _bstr_t bsize(attr->Gettext()); int64 size = atoll(bsize); feedItemInfo.SetSize(size); } } if (!feedItemInfo.GetUrl()) { // https://nzb.org/fetch/334534ce/4364564564 tag = node->selectSingleNode("link"); if (!tag) { // bad rss feed return false; } _bstr_t link(tag->Gettext()); feedItemInfo.SetUrl(link); } // newznab special // if (feedItemInfo.GetSize() == 0) { tag = node->selectSingleNode("newznab:attr[@name='size'] | nZEDb:attr[@name='size']"); if (tag) { attr = tag->Getattributes()->getNamedItem("value"); if (attr) { _bstr_t bsize(attr->Gettext()); int64 size = atoll(bsize); feedItemInfo.SetSize(size); } } } // tag = node->selectSingleNode("newznab:attr[@name='imdb'] | nZEDb:attr[@name='imdb']"); if (tag) { attr = tag->Getattributes()->getNamedItem("value"); if (attr) { _bstr_t bval(attr->Gettext()); int val = atoi(bval); feedItemInfo.SetImdbId(val); } } // tag = node->selectSingleNode("newznab:attr[@name='rageid'] | nZEDb:attr[@name='rageid']"); if (tag) { attr = tag->Getattributes()->getNamedItem("value"); if (attr) { _bstr_t bval(attr->Gettext()); int val = atoi(bval); feedItemInfo.SetRageId(val); } } // tag = node->selectSingleNode("newznab:attr[@name='tvdbid'] | nZEDb:attr[@name='tvdbid']"); if (tag) { attr = tag->Getattributes()->getNamedItem("value"); if (attr) { _bstr_t bval(attr->Gettext()); int val = atoi(bval); feedItemInfo.SetTvdbId(val); } } // tag = node->selectSingleNode("newznab:attr[@name='tvmazeid'] | nZEDb:attr[@name='tvmazeid']"); if (tag) { attr = tag->Getattributes()->getNamedItem("value"); if (attr) { _bstr_t bval(attr->Gettext()); int val = atoi(bval); feedItemInfo.SetTvmazeId(val); } } // // tag = node->selectSingleNode("newznab:attr[@name='episode'] | nZEDb:attr[@name='episode']"); if (tag) { attr = tag->Getattributes()->getNamedItem("value"); if (attr) { _bstr_t val(attr->Gettext()); feedItemInfo.SetEpisode(val); } } // // tag = node->selectSingleNode("newznab:attr[@name='season'] | nZEDb:attr[@name='season']"); if (tag) { attr = tag->Getattributes()->getNamedItem("value"); if (attr) { _bstr_t val(attr->Gettext()); feedItemInfo.SetSeason(val); } } MSXML::IXMLDOMNodeListPtr itemList = node->selectNodes("newznab:attr | nZEDb:attr"); for (int i = 0; i < itemList->Getlength(); i++) { MSXML::IXMLDOMNodePtr node = itemList->Getitem(i); MSXML::IXMLDOMNodePtr name = node->Getattributes()->getNamedItem("name"); MSXML::IXMLDOMNodePtr value = node->Getattributes()->getNamedItem("value"); if (name && value) { _bstr_t bname(name->Gettext()); _bstr_t bval(value->Gettext()); feedItemInfo.GetAttributes()->emplace_back(bname, bval); } } } return true; } #else bool FeedFile::Parse() { xmlSAXHandler SAX_handler = {0}; SAX_handler.startElement = reinterpret_cast(SAX_StartElement); SAX_handler.endElement = reinterpret_cast(SAX_EndElement); SAX_handler.characters = reinterpret_cast(SAX_characters); SAX_handler.error = reinterpret_cast(SAX_error); SAX_handler.getEntity = reinterpret_cast(SAX_getEntity); m_ignoreNextError = false; int ret = xmlSAXUserParseFile(&SAX_handler, this, m_fileName); if (ret != 0) { error("Failed to parse rss feed %s", *m_infoName); return false; } return true; } void FeedFile::Parse_StartElement(const char *name, const char **atts) { ResetTagContent(); if (!strcmp("item", name)) { m_feedItems->emplace_back(); m_feedItemInfo = &m_feedItems->back(); } else if (!strcmp("enclosure", name) && m_feedItemInfo) { // for (; *atts; atts+=2) { if (!strcmp("url", atts[0])) { CString url = atts[1]; WebUtil::XmlDecode(url); m_feedItemInfo->SetUrl(url); } else if (!strcmp("length", atts[0])) { int64 size = atoll(atts[1]); m_feedItemInfo->SetSize(size); } } } else if (m_feedItemInfo && (!strcmp("newznab:attr", name) || !strcmp("nZEDb:attr", name)) && atts[0] && atts[1] && atts[2] && atts[3] && !strcmp("name", atts[0]) && !strcmp("value", atts[2])) { m_feedItemInfo->GetAttributes()->emplace_back(atts[1], atts[3]); // if (m_feedItemInfo->GetSize() == 0 && !strcmp("size", atts[1])) { int64 size = atoll(atts[3]); m_feedItemInfo->SetSize(size); } // else if (!strcmp("imdb", atts[1])) { m_feedItemInfo->SetImdbId(atoi(atts[3])); } // else if (!strcmp("rageid", atts[1])) { m_feedItemInfo->SetRageId(atoi(atts[3])); } // else if (!strcmp("tvdbid", atts[1])) { m_feedItemInfo->SetTvdbId(atoi(atts[3])); } // else if (!strcmp("tvmazeid", atts[1])) { m_feedItemInfo->SetTvmazeId(atoi(atts[3])); } // // else if (!strcmp("episode", atts[1])) { m_feedItemInfo->SetEpisode(atts[3]); } // // else if (!strcmp("season", atts[1])) { m_feedItemInfo->SetSeason(atts[3]); } } } void FeedFile::Parse_EndElement(const char *name) { if (!strcmp("title", name) && m_feedItemInfo) { m_feedItemInfo->SetTitle(m_tagContent); ResetTagContent(); ParseSubject(*m_feedItemInfo); } else if (!strcmp("link", name) && m_feedItemInfo && (!m_feedItemInfo->GetUrl() || strlen(m_feedItemInfo->GetUrl()) == 0)) { m_feedItemInfo->SetUrl(m_tagContent); ResetTagContent(); } else if (!strcmp("category", name) && m_feedItemInfo) { m_feedItemInfo->SetCategory(m_tagContent); ResetTagContent(); } else if (!strcmp("description", name) && m_feedItemInfo) { // cleanup CDATA CString description = *m_tagContent; WebUtil::XmlStripTags(description); WebUtil::XmlDecode(description); WebUtil::XmlRemoveEntities(description); m_feedItemInfo->SetDescription(description); ResetTagContent(); } else if (!strcmp("pubDate", name) && m_feedItemInfo) { time_t unixtime = WebUtil::ParseRfc822DateTime(m_tagContent); if (unixtime > 0) { m_feedItemInfo->SetTime(unixtime); } ResetTagContent(); } } void FeedFile::Parse_Content(const char *buf, int len) { m_tagContent.Append(buf, len); } void FeedFile::ResetTagContent() { m_tagContent.Clear(); } void FeedFile::SAX_StartElement(FeedFile* file, const char *name, const char **atts) { file->Parse_StartElement(name, atts); } void FeedFile::SAX_EndElement(FeedFile* file, const char *name) { file->Parse_EndElement(name); } void FeedFile::SAX_characters(FeedFile* file, const char * xmlstr, int len) { char* str = (char*)xmlstr; // trim starting blanks int off = 0; for (int i = 0; i < len; i++) { char ch = str[i]; if (ch == ' ' || ch == 10 || ch == 13 || ch == 9) { off++; } else { break; } } int newlen = len - off; // trim ending blanks for (int i = len - 1; i >= off; i--) { char ch = str[i]; if (ch == ' ' || ch == 10 || ch == 13 || ch == 9) { newlen--; } else { break; } } if (newlen > 0) { // interpret tag content file->Parse_Content(str + off, newlen); } } void* FeedFile::SAX_getEntity(FeedFile* file, const char * name) { xmlEntityPtr e = xmlGetPredefinedEntity((xmlChar* )name); if (!e) { warn("entity not found"); file->m_ignoreNextError = true; } return e; } void FeedFile::SAX_error(FeedFile* file, const char *msg, ...) { if (file->m_ignoreNextError) { file->m_ignoreNextError = false; return; } va_list argp; va_start(argp, msg); char errMsg[1024]; vsnprintf(errMsg, sizeof(errMsg), msg, argp); errMsg[1024-1] = '\0'; va_end(argp); // remove trailing CRLF for (char* pend = errMsg + strlen(errMsg) - 1; pend >= errMsg && (*pend == '\n' || *pend == '\r' || *pend == ' '); pend--) *pend = '\0'; error("Error parsing rss feed %s: %s", *file->m_infoName, errMsg); } #endif nzbget-19.1/daemon/feed/FeedInfo.cpp0000644000175000017500000001150213130203062017067 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #include "nzbget.h" #include "FeedInfo.h" #include "Util.h" FeedInfo::FeedInfo(int id, const char* name, const char* url, bool backlog, int interval, const char* filter, bool pauseNzb, const char* category, int priority, const char* extensions) : m_backlog(backlog), m_interval(interval), m_pauseNzb(pauseNzb), m_priority(priority) { m_id = id; m_name = name ? name : ""; if (m_name.Length() == 0) { m_name.Format("Feed%i", m_id); } m_url = url ? url : ""; m_filter = filter ? filter : ""; m_filterHash = Util::HashBJ96(m_filter, strlen(m_filter), 0); m_category = category ? category : ""; m_extensions = extensions ? extensions : ""; } FeedItemInfo::Attr* FeedItemInfo::Attributes::Find(const char* name) { for (Attr& attr : this) { if (!strcasecmp(attr.GetName(), name)) { return &attr; } } return nullptr; } void FeedItemInfo::SetSeason(const char* season) { m_season = season; m_seasonNum = season ? ParsePrefixedInt(season) : 0; } void FeedItemInfo::SetEpisode(const char* episode) { m_episode = episode; m_episodeNum = episode ? ParsePrefixedInt(episode) : 0; } int FeedItemInfo::ParsePrefixedInt(const char *value) { const char* val = value; if (!strchr("0123456789", *val)) { val++; } return atoi(val); } void FeedItemInfo::AppendDupeKey(const char* extraDupeKey) { if (!m_dupeKey.Empty() && !Util::EmptyStr(extraDupeKey)) { m_dupeKey.AppendFmt("-%s", extraDupeKey); } } void FeedItemInfo::BuildDupeKey(const char* rageId, const char* tvdbId, const char* tvmazeId, const char* series) { int rageIdVal = !Util::EmptyStr(rageId) ? atoi(rageId) : m_rageId; int tvdbIdVal = !Util::EmptyStr(tvdbId) ? atoi(tvdbId) : m_tvdbId; int tvmazeIdVal = !Util::EmptyStr(tvmazeId) ? atoi(tvmazeId) : m_tvmazeId; if (m_imdbId != 0) { m_dupeKey.Format("imdb=%i", m_imdbId); } else if (!Util::EmptyStr(series) && GetSeasonNum() != 0 && GetEpisodeNum() != 0) { m_dupeKey.Format("series=%s-%s-%s", series, *m_season, *m_episode); } else if (rageIdVal != 0 && GetSeasonNum() != 0 && GetEpisodeNum() != 0) { m_dupeKey.Format("rageid=%i-%s-%s", rageIdVal, *m_season, *m_episode); } else if (tvdbIdVal != 0 && GetSeasonNum() != 0 && GetEpisodeNum() != 0) { m_dupeKey.Format("tvdbid=%i-%s-%s", tvdbIdVal, *m_season, *m_episode); } else if (tvmazeIdVal != 0 && GetSeasonNum() != 0 && GetEpisodeNum() != 0) { m_dupeKey.Format("tvmazeid=%i-%s-%s", tvmazeIdVal, *m_season, *m_episode); } else { m_dupeKey = ""; } } int FeedItemInfo::GetSeasonNum() { if (!m_season && !m_seasonEpisodeParsed) { ParseSeasonEpisode(); } return m_seasonNum; } int FeedItemInfo::GetEpisodeNum() { if (!m_episode && !m_seasonEpisodeParsed) { ParseSeasonEpisode(); } return m_episodeNum; } void FeedItemInfo::ParseSeasonEpisode() { m_seasonEpisodeParsed = true; const char* pattern = "[^[:alnum:]]s?([0-9]+)[ex]([0-9]+(-?e[0-9]+)?)[^[:alnum:]]"; std::unique_ptr& regEx = m_feedFilterHelper->GetRegEx(1); if (!regEx) { regEx = std::make_unique(pattern, 10); } if (regEx->Match(m_title)) { SetSeason(BString<100>("S%02d", atoi(m_title + regEx->GetMatchStart(1)))); BString<100> regValue; regValue.Set(m_title + regEx->GetMatchStart(2), regEx->GetMatchLen(2)); BString<100> episode("E%s", *regValue); Util::ReduceStr(episode, "-", ""); for (char* p = episode; *p; p++) *p = toupper(*p); // convert string to uppercase e02 -> E02 SetEpisode(episode); } } const char* FeedItemInfo::GetDupeStatus() { if (!m_dupeStatus) { BString<1024> statuses; m_feedFilterHelper->CalcDupeStatus(m_title, m_dupeKey, statuses, statuses.Capacity()); m_dupeStatus = statuses; } return m_dupeStatus; } void FeedHistory::Remove(const char* url) { for (iterator it = begin(); it != end(); it++) { FeedHistoryInfo& feedHistoryInfo = *it; if (!strcmp(feedHistoryInfo.GetUrl(), url)) { erase(it); break; } } } FeedHistoryInfo* FeedHistory::Find(const char* url) { for (FeedHistoryInfo& feedHistoryInfo : this) { if (!strcmp(feedHistoryInfo.GetUrl(), url)) { return &feedHistoryInfo; } } return nullptr; } nzbget-19.1/daemon/feed/FeedCoordinator.h0000644000175000017500000001034413130203062020127 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #ifndef FEEDCOORDINATOR_H #define FEEDCOORDINATOR_H #include "NString.h" #include "Log.h" #include "Thread.h" #include "WebDownloader.h" #include "DownloadInfo.h" #include "FeedFile.h" #include "FeedInfo.h" #include "Observer.h" #include "Util.h" class FeedDownloader; class FeedCoordinator : public Thread, public Observer, public Subject, public Debuggable { public: FeedCoordinator(); virtual ~FeedCoordinator(); virtual void Run(); virtual void Stop(); void Update(Subject* caller, void* aspect); void AddFeed(std::unique_ptr feedInfo) { m_feeds.push_back(std::move(feedInfo)); } /* may return empty pointer on error */ std::shared_ptr PreviewFeed(int id, const char* name, const char* url, const char* filter, bool backlog, bool pauseNzb, const char* category, int priority, int interval, const char* feedScript, int cacheTimeSec, const char* cacheId); /* may return empty pointer on error */ std::shared_ptr ViewFeed(int id); void FetchFeed(int id); bool HasActiveDownloads(); Feeds* GetFeeds() { return &m_feeds; } protected: virtual void LogDebugInfo(); private: class DownloadQueueObserver: public Observer { public: FeedCoordinator* m_owner; virtual void Update(Subject* caller, void* aspect) { m_owner->DownloadQueueUpdate(caller, aspect); } }; class FeedCacheItem { public: FeedCacheItem(const char* url, int cacheTimeSec,const char* cacheId, time_t lastUsage, std::shared_ptr feedItems) : m_url(url), m_cacheTimeSec(cacheTimeSec), m_cacheId(cacheId), m_lastUsage(lastUsage), m_feedItems(feedItems) {} const char* GetUrl() { return m_url; } int GetCacheTimeSec() { return m_cacheTimeSec; } const char* GetCacheId() { return m_cacheId; } time_t GetLastUsage() { return m_lastUsage; } void SetLastUsage(time_t lastUsage) { m_lastUsage = lastUsage; } std::shared_ptr GetFeedItems() { return m_feedItems; } private: CString m_url; int m_cacheTimeSec; CString m_cacheId; time_t m_lastUsage; std::shared_ptr m_feedItems; }; class FilterHelper : public FeedFilterHelper { public: virtual std::unique_ptr& GetRegEx(int id); virtual void CalcDupeStatus(const char* title, const char* dupeKey, char* statusBuf, int bufLen); private: std::vector> m_regExes; }; typedef std::list FeedCache; typedef std::deque ActiveDownloads; Feeds m_feeds; ActiveDownloads m_activeDownloads; FeedHistory m_feedHistory; Mutex m_downloadsMutex; DownloadQueueObserver m_downloadQueueObserver; bool m_force = false; bool m_save = false; FeedCache m_feedCache; void StartFeedDownload(FeedInfo* feedInfo, bool force); void FeedCompleted(FeedDownloader* feedDownloader); void FilterFeed(FeedInfo* feedInfo, FeedItemList* feedItems); std::vector> ProcessFeed(FeedInfo* feedInfo, FeedItemList* feedItems); std::unique_ptr CreateNzbInfo(FeedInfo* feedInfo, FeedItemInfo& feedItemInfo); void ResetHangingDownloads(); void DownloadQueueUpdate(Subject* caller, void* aspect); void CleanupHistory(); void CleanupCache(); void CheckSaveFeeds(); std::unique_ptr parseFeed(FeedInfo* feedInfo); }; extern FeedCoordinator* g_FeedCoordinator; class FeedDownloader : public WebDownloader { public: void SetFeedInfo(FeedInfo* feedInfo) { m_feedInfo = feedInfo; } FeedInfo* GetFeedInfo() { return m_feedInfo; } private: FeedInfo* m_feedInfo; }; #endif nzbget-19.1/daemon/feed/FeedInfo.h0000644000175000017500000001731113130203062016540 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #ifndef FEEDINFO_H #define FEEDINFO_H #include "NString.h" #include "Util.h" #include "DownloadInfo.h" class FeedInfo { public: enum EStatus { fsUndefined, fsRunning, fsFinished, fsFailed }; FeedInfo(int id, const char* name, const char* url, bool backlog, int interval, const char* filter, bool pauseNzb, const char* category, int priority, const char* extensions); int GetId() { return m_id; } const char* GetName() { return m_name; } const char* GetUrl() { return m_url; } int GetInterval() { return m_interval; } const char* GetFilter() { return m_filter; } uint32 GetFilterHash() { return m_filterHash; } bool GetPauseNzb() { return m_pauseNzb; } const char* GetCategory() { return m_category; } int GetPriority() { return m_priority; } const char* GetExtensions() { return m_extensions; } time_t GetLastUpdate() { return m_lastUpdate; } void SetLastUpdate(time_t lastUpdate) { m_lastUpdate = lastUpdate; } bool GetPreview() { return m_preview; } void SetPreview(bool preview) { m_preview = preview; } EStatus GetStatus() { return m_status; } void SetStatus(EStatus Status) { m_status = Status; } const char* GetOutputFilename() { return m_outputFilename; } void SetOutputFilename(const char* outputFilename) { m_outputFilename = outputFilename; } bool GetFetch() { return m_fetch; } void SetFetch(bool fetch) { m_fetch = fetch; } bool GetForce() { return m_force; } void SetForce(bool force) { m_force = force; } bool GetBacklog() { return m_backlog; } void SetBacklog(bool backlog) { m_backlog = backlog; } private: int m_id; CString m_name; CString m_url; int m_interval; CString m_filter; uint32 m_filterHash; bool m_pauseNzb; CString m_category; CString m_extensions; int m_priority; time_t m_lastUpdate = 0; bool m_preview = false; EStatus m_status = fsUndefined; CString m_outputFilename; bool m_fetch = false; bool m_force = false; bool m_backlog; }; typedef std::deque> Feeds; class FeedFilterHelper { public: virtual std::unique_ptr& GetRegEx(int id) = 0; virtual void CalcDupeStatus(const char* title, const char* dupeKey, char* statusBuf, int bufLen) = 0; }; class FeedItemInfo { public: enum EStatus { isUnknown, isBacklog, isFetched, isNew }; enum EMatchStatus { msIgnored, msAccepted, msRejected }; class Attr { public: Attr(const char* name, const char* value) : m_name(name ? name : ""), m_value(value ? value : "") {} const char* GetName() { return m_name; } const char* GetValue() { return m_value; } private: CString m_name; CString m_value; }; typedef std::deque AttributesBase; class Attributes: public AttributesBase { public: Attr* Find(const char* name); }; FeedItemInfo() {} FeedItemInfo(FeedItemInfo&&) = delete; // catch performance issues void SetFeedFilterHelper(FeedFilterHelper* feedFilterHelper) { m_feedFilterHelper = feedFilterHelper; } const char* GetTitle() { return m_title; } void SetTitle(const char* title) { m_title = title; } const char* GetFilename() { return m_filename; } void SetFilename(const char* filename) { m_filename = filename; } const char* GetUrl() { return m_url; } void SetUrl(const char* url) { m_url = url; } int64 GetSize() { return m_size; } void SetSize(int64 size) { m_size = size; } const char* GetCategory() { return m_category; } void SetCategory(const char* category) { m_category = category; } int GetImdbId() { return m_imdbId; } void SetImdbId(int imdbId) { m_imdbId = imdbId; } int GetRageId() { return m_rageId; } void SetRageId(int rageId) { m_rageId = rageId; } int GetTvdbId() { return m_tvdbId; } void SetTvdbId(int tvdbId) { m_tvdbId = tvdbId; } int GetTvmazeId() { return m_tvmazeId; } void SetTvmazeId(int tvmazeId) { m_tvmazeId = tvmazeId; } const char* GetDescription() { return m_description; } void SetDescription(const char* description) { m_description = description ? description: ""; } const char* GetSeason() { return m_season; } void SetSeason(const char* season); const char* GetEpisode() { return m_episode; } void SetEpisode(const char* episode); int GetSeasonNum(); int GetEpisodeNum(); const char* GetAddCategory() { return m_addCategory; } void SetAddCategory(const char* addCategory) { m_addCategory = addCategory ? addCategory : ""; } bool GetPauseNzb() { return m_pauseNzb; } void SetPauseNzb(bool pauseNzb) { m_pauseNzb = pauseNzb; } int GetPriority() { return m_priority; } void SetPriority(int priority) { m_priority = priority; } time_t GetTime() { return m_time; } void SetTime(time_t time) { m_time = time; } EStatus GetStatus() { return m_status; } void SetStatus(EStatus status) { m_status = status; } EMatchStatus GetMatchStatus() { return m_matchStatus; } void SetMatchStatus(EMatchStatus matchStatus) { m_matchStatus = matchStatus; } int GetMatchRule() { return m_matchRule; } void SetMatchRule(int matchRule) { m_matchRule = matchRule; } const char* GetDupeKey() { return m_dupeKey; } void SetDupeKey(const char* dupeKey) { m_dupeKey = dupeKey ? dupeKey : ""; } void AppendDupeKey(const char* extraDupeKey); void BuildDupeKey(const char* rageId, const char* tvdbId, const char* tvmazeId, const char* series); int GetDupeScore() { return m_dupeScore; } void SetDupeScore(int dupeScore) { m_dupeScore = dupeScore; } EDupeMode GetDupeMode() { return m_dupeMode; } void SetDupeMode(EDupeMode dupeMode) { m_dupeMode = dupeMode; } const char* GetDupeStatus(); Attributes* GetAttributes() { return &m_attributes; } private: CString m_title; CString m_filename; CString m_url; time_t m_time = 0; int64 m_size = 0; CString m_category = ""; int m_imdbId = 0; int m_rageId = 0; int m_tvdbId = 0; int m_tvmazeId = 0; CString m_description = ""; CString m_season; CString m_episode; int m_seasonNum = 0; int m_episodeNum = 0; bool m_seasonEpisodeParsed = false; CString m_addCategory = ""; bool m_pauseNzb = false; int m_priority = 0; EStatus m_status = isUnknown; EMatchStatus m_matchStatus = msIgnored; int m_matchRule = 0; CString m_dupeKey; int m_dupeScore = 0; EDupeMode m_dupeMode = dmScore; CString m_dupeStatus; FeedFilterHelper* m_feedFilterHelper = nullptr; Attributes m_attributes; int ParsePrefixedInt(const char *value); void ParseSeasonEpisode(); }; typedef std::deque FeedItemList; class FeedHistoryInfo { public: enum EStatus { hsUnknown, hsBacklog, hsFetched }; FeedHistoryInfo(const char* url, EStatus status, time_t lastSeen) : m_url(url), m_status(status), m_lastSeen(lastSeen) {} const char* GetUrl() { return m_url; } EStatus GetStatus() { return m_status; } void SetStatus(EStatus Status) { m_status = Status; } time_t GetLastSeen() { return m_lastSeen; } void SetLastSeen(time_t lastSeen) { m_lastSeen = lastSeen; } private: CString m_url; EStatus m_status; time_t m_lastSeen; }; typedef std::deque FeedHistoryBase; class FeedHistory : public FeedHistoryBase { public: void Remove(const char* url); FeedHistoryInfo* Find(const char* url); }; #endif nzbget-19.1/daemon/feed/FeedFile.h0000644000175000017500000000370213130203062016523 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #ifndef FEEDFILE_H #define FEEDFILE_H #include "NString.h" #include "FeedInfo.h" class FeedFile { public: FeedFile(const char* fileName, const char* infoName); bool Parse(); std::unique_ptr DetachFeedItems() { return std::move(m_feedItems); } void LogDebugInfo(); private: std::unique_ptr m_feedItems; CString m_fileName; CString m_infoName; void ParseSubject(FeedItemInfo& feedItemInfo); #ifdef WIN32 bool ParseFeed(IUnknown* nzb); static void EncodeUrl(const char* filename, char* url, int bufLen); #else FeedItemInfo* m_feedItemInfo; StringBuilder m_tagContent; bool m_ignoreNextError; static void SAX_StartElement(FeedFile* file, const char *name, const char **atts); static void SAX_EndElement(FeedFile* file, const char *name); static void SAX_characters(FeedFile* file, const char * xmlstr, int len); static void* SAX_getEntity(FeedFile* file, const char * name); static void SAX_error(FeedFile* file, const char *msg, ...); void Parse_StartElement(const char *name, const char **atts); void Parse_EndElement(const char *name); void Parse_Content(const char *buf, int len); void ResetTagContent(); #endif }; #endif nzbget-19.1/daemon/frontend/0000755000175000017500000000000013130203062015621 5ustar andreasandreasnzbget-19.1/daemon/frontend/Frontend.cpp0000644000175000017500000001772513130203062020120 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "Options.h" #include "Frontend.h" #include "Log.h" #include "Connection.h" #include "MessageBase.h" #include "RemoteClient.h" #include "Util.h" #include "StatMeter.h" Frontend::Frontend() { debug("Creating Frontend"); m_updateInterval = g_Options->GetUpdateInterval(); } bool Frontend::PrepareData() { if (IsRemoteMode()) { if (IsStopped()) { return false; } if (!RequestMessages() || ((m_summary || m_fileList) && !RequestFileList())) { const char* controlIp = !strcmp(g_Options->GetControlIp(), "0.0.0.0") ? "127.0.0.1" : g_Options->GetControlIp(); printf("\nUnable to send request to nzbget-server at %s (port %i) \n", controlIp, g_Options->GetControlPort()); Stop(); return false; } } else { if (m_summary) { m_currentDownloadSpeed = g_StatMeter->CalcCurrentDownloadSpeed(); m_pauseDownload = g_Options->GetPauseDownload(); m_downloadLimit = g_Options->GetDownloadRate(); m_threadCount = Thread::GetThreadCount(); g_StatMeter->CalcTotalStat(&m_upTimeSec, &m_dnTimeSec, &m_allBytes, &m_standBy); GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); m_postJobCount = 0; for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { m_postJobCount += nzbInfo->GetPostInfo() ? 1 : 0; } downloadQueue->CalcRemainingSize(&m_remainingSize, nullptr); } } return true; } void Frontend::FreeData() { if (IsRemoteMode()) { m_remoteMessages.clear(); DownloadQueue::Guard()->GetQueue()->clear(); } } GuardedMessageList Frontend::GuardMessages() { if (IsRemoteMode()) { return GuardedMessageList(&m_remoteMessages, nullptr); } else { return g_Log->GuardMessages(); } } bool Frontend::IsRemoteMode() { return g_Options->GetRemoteClientMode(); } void Frontend::ServerPauseUnpause(bool pause) { if (IsRemoteMode()) { RequestPauseUnpause(pause); } else { g_Options->SetResumeTime(0); g_Options->SetPauseDownload(pause); } } void Frontend::ServerSetDownloadRate(int rate) { if (IsRemoteMode()) { RequestSetDownloadRate(rate); } else { g_Options->SetDownloadRate(rate); } } bool Frontend::ServerEditQueue(DownloadQueue::EEditAction action, int offset, int id) { if (IsRemoteMode()) { return RequestEditQueue(action, offset, id); } else { return DownloadQueue::Guard()->EditEntry(id, action, CString::FormatStr("%i", offset)); } } void Frontend::InitMessageBase(SNzbRequestBase* messageBase, int request, int size) { messageBase->m_signature = htonl(NZBMESSAGE_SIGNATURE); messageBase->m_type = htonl(request); messageBase->m_structSize = htonl(size); strncpy(messageBase->m_username, g_Options->GetControlUsername(), NZBREQUESTPASSWORDSIZE - 1); messageBase->m_username[NZBREQUESTPASSWORDSIZE - 1] = '\0'; strncpy(messageBase->m_password, g_Options->GetControlPassword(), NZBREQUESTPASSWORDSIZE); messageBase->m_password[NZBREQUESTPASSWORDSIZE - 1] = '\0'; } bool Frontend::RequestMessages() { const char* controlIp = !strcmp(g_Options->GetControlIp(), "0.0.0.0") ? "127.0.0.1" : g_Options->GetControlIp(); Connection connection(controlIp, g_Options->GetControlPort(), false); bool OK = connection.Connect(); if (!OK) { return false; } SNzbLogRequest LogRequest; InitMessageBase(&LogRequest.m_messageBase, rrLog, sizeof(LogRequest)); LogRequest.m_lines = htonl(m_neededLogEntries); if (m_neededLogEntries == 0) { LogRequest.m_idFrom = htonl(m_neededLogFirstId > 0 ? m_neededLogFirstId : 1); } else { LogRequest.m_idFrom = 0; } if (!connection.Send((char*)(&LogRequest), sizeof(LogRequest))) { return false; } // Now listen for the returned log SNzbLogResponse LogResponse; bool read = connection.Recv((char*) &LogResponse, sizeof(LogResponse)); if (!read || (int)ntohl(LogResponse.m_messageBase.m_signature) != (int)NZBMESSAGE_SIGNATURE || ntohl(LogResponse.m_messageBase.m_structSize) != sizeof(LogResponse)) { return false; } CharBuffer buf; if (ntohl(LogResponse.m_trailingDataLength) > 0) { buf.Reserve(ntohl(LogResponse.m_trailingDataLength)); if (!connection.Recv(buf, buf.Size())) { return false; } } connection.Disconnect(); if (ntohl(LogResponse.m_trailingDataLength) > 0) { char* bufPtr = (char*)buf; for (uint32 i = 0; i < ntohl(LogResponse.m_nrTrailingEntries); i++) { SNzbLogResponseEntry* logAnswer = (SNzbLogResponseEntry*) bufPtr; char* text = bufPtr + sizeof(SNzbLogResponseEntry); m_remoteMessages.emplace_back(ntohl(logAnswer->m_id), (Message::EKind)ntohl(logAnswer->m_kind), ntohl(logAnswer->m_time), text); bufPtr += sizeof(SNzbLogResponseEntry) + ntohl(logAnswer->m_textLen); } } return true; } bool Frontend::RequestFileList() { const char* controlIp = !strcmp(g_Options->GetControlIp(), "0.0.0.0") ? "127.0.0.1" : g_Options->GetControlIp(); Connection connection(controlIp, g_Options->GetControlPort(), false); bool OK = connection.Connect(); if (!OK) { return false; } SNzbListRequest ListRequest; InitMessageBase(&ListRequest.m_messageBase, rrList, sizeof(ListRequest)); ListRequest.m_fileList = htonl(m_fileList); ListRequest.m_serverState = htonl(m_summary); if (!connection.Send((char*)(&ListRequest), sizeof(ListRequest))) { return false; } // Now listen for the returned list SNzbListResponse ListResponse; bool read = connection.Recv((char*) &ListResponse, sizeof(ListResponse)); if (!read || (int)ntohl(ListResponse.m_messageBase.m_signature) != (int)NZBMESSAGE_SIGNATURE || ntohl(ListResponse.m_messageBase.m_structSize) != sizeof(ListResponse)) { return false; } CharBuffer buf; if (ntohl(ListResponse.m_trailingDataLength) > 0) { buf.Reserve(ntohl(ListResponse.m_trailingDataLength)); if (!connection.Recv(buf, buf.Size())) { return false; } } connection.Disconnect(); if (m_summary) { m_pauseDownload = ntohl(ListResponse.m_downloadPaused); m_remainingSize = Util::JoinInt64(ntohl(ListResponse.m_remainingSizeHi), ntohl(ListResponse.m_remainingSizeLo)); m_currentDownloadSpeed = ntohl(ListResponse.m_downloadRate); m_downloadLimit = ntohl(ListResponse.m_downloadLimit); m_threadCount = ntohl(ListResponse.m_threadCount); m_postJobCount = ntohl(ListResponse.m_postJobCount); m_upTimeSec = ntohl(ListResponse.m_upTimeSec); m_dnTimeSec = ntohl(ListResponse.m_downloadTimeSec); m_standBy = ntohl(ListResponse.m_downloadStandBy); m_allBytes = Util::JoinInt64(ntohl(ListResponse.m_downloadedBytesHi), ntohl(ListResponse.m_downloadedBytesLo)); } if (m_fileList && ntohl(ListResponse.m_trailingDataLength) > 0) { RemoteClient client; client.SetVerbose(false); client.BuildFileList(&ListResponse, buf, DownloadQueue::Guard()); } return true; } bool Frontend::RequestPauseUnpause(bool pause) { RemoteClient client; client.SetVerbose(false); return client.RequestServerPauseUnpause(pause, rpDownload); } bool Frontend::RequestSetDownloadRate(int rate) { RemoteClient client; client.SetVerbose(false); return client.RequestServerSetDownloadRate(rate); } bool Frontend::RequestEditQueue(DownloadQueue::EEditAction action, int offset, int id) { RemoteClient client; client.SetVerbose(false); IdList ids = { id }; return client.RequestServerEditQueue(action, offset, nullptr, &ids, nullptr, rmId); } nzbget-19.1/daemon/frontend/ColoredFrontend.h0000644000175000017500000000243613130203062021066 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef COLOREDFRONTEND_H #define COLOREDFRONTEND_H #include "LoggableFrontend.h" #include "Log.h" class ColoredFrontend : public LoggableFrontend { public: ColoredFrontend(); protected: virtual void BeforePrint(); virtual void PrintMessage(Message& message); virtual void PrintStatus(); virtual void PrintSkip(); virtual void BeforeExit(); private: bool m_needGoBack = false; #ifdef WIN32 HANDLE m_console; #endif }; #endif nzbget-19.1/daemon/frontend/NCursesFrontend.h0000644000175000017500000000611313130203062021055 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef NCURSESFRONTEND_H #define NCURSESFRONTEND_H #ifndef DISABLE_CURSES #include "NString.h" #include "Frontend.h" #include "Log.h" #include "DownloadInfo.h" class NCursesFrontend : public Frontend { public: NCursesFrontend(); virtual ~NCursesFrontend(); protected: virtual void Run(); private: enum EInputMode { normal, editQueue, downloadRate }; bool m_useColor = true; int m_dataUpdatePos = 0; bool m_updateNextTime = false; int m_screenHeight = 0; int m_screenWidth = 0; int m_queueWinTop = 0; int m_queueWinHeight = 0; int m_queueWinClientHeight = 0; int m_messagesWinTop = 0; int m_messagesWinHeight = 0; int m_messagesWinClientHeight = 0; int m_selectedQueueEntry = 0; int m_lastEditEntry = -1; bool m_lastPausePars = false; int m_queueScrollOffset = 0; CString m_hint; time_t m_startHint; int m_colWidthFiles; int m_colWidthTotal; int m_colWidthLeft; // Inputting numbers int m_inputNumberIndex = 0; int m_inputValue; #ifdef WIN32 std::vector m_screenBuffer; std::vector m_oldScreenBuffer; std::vector m_colorAttr; #else void* m_window; // WINDOW* #endif EInputMode m_inputMode = normal; bool m_showNzbname; bool m_showTimestamp; bool m_groupFiles; int m_queueWindowPercentage = 50; #ifdef WIN32 void init_pair(int colorNumber, WORD wForeColor, WORD wBackColor); #endif void PlotLine(const char * string, int row, int pos, int colorPair); void PlotText(const char * string, int row, int pos, int colorPair, bool blink); void PrintMessages(); void PrintQueue(); void PrintFileQueue(); void PrintFilename(FileInfo* fileInfo, int row, bool selected); void PrintGroupQueue(); void ResetColWidths(); void PrintGroupname(NzbInfo* nzbInfo, int row, bool selected, bool calcColWidth); void PrintTopHeader(char* header, int lineNr, bool upTime); int PrintMessage(Message& msg, int row, int maxLines); void PrintKeyInputBar(); void PrintStatus(); void UpdateInput(int initialKey); void Update(int key); void SetCurrentQueueEntry(int entry); void CalcWindowSizes(); void RefreshScreen(); int ReadConsoleKey(); int CalcQueueSize(); void NeedUpdateData(); bool EditQueue(DownloadQueue::EEditAction action, int offset); void SetHint(const char* hint); }; #endif #endif nzbget-19.1/daemon/frontend/ColoredFrontend.cpp0000644000175000017500000000763213130203062021424 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "ColoredFrontend.h" #include "Util.h" ColoredFrontend::ColoredFrontend() { m_summary = true; #ifdef WIN32 m_console = GetStdHandle(STD_OUTPUT_HANDLE); #endif } void ColoredFrontend::BeforePrint() { if (m_needGoBack) { // go back one line #ifdef WIN32 CONSOLE_SCREEN_BUFFER_INFO BufInfo; GetConsoleScreenBufferInfo(m_console, &BufInfo); BufInfo.dwCursorPosition.Y--; SetConsoleCursorPosition(m_console, BufInfo.dwCursorPosition); #else printf("\r\033[1A"); #endif m_needGoBack = false; } } void ColoredFrontend::PrintStatus() { BString<100> timeString; int currentDownloadSpeed = m_standBy ? 0 : m_currentDownloadSpeed; if (currentDownloadSpeed > 0 && !m_pauseDownload) { int64 remain_sec = (int64)(m_remainingSize / currentDownloadSpeed); int h = (int)(remain_sec / 3600); int m = (int)((remain_sec % 3600) / 60); int s = (int)(remain_sec % 60); timeString.Format(" (~ %.2d:%.2d:%.2d)", h, m, s); } BString<100> downloadLimit; if (m_downloadLimit > 0) { downloadLimit.Format(", Limit %i KB/s", m_downloadLimit / 1024); } BString<100> postStatus; if (m_postJobCount > 0) { postStatus.Format(", %i post-job%s", m_postJobCount, m_postJobCount > 1 ? "s" : ""); } #ifdef WIN32 const char* controlSeq = ""; #else printf("\033[s"); const char* controlSeq = "\033[K"; #endif BString<1024> status(" %d threads, %s, %s remaining%s%s%s%s%s\n", m_threadCount, *Util::FormatSpeed(currentDownloadSpeed), *Util::FormatSize(m_remainingSize), *timeString, *postStatus, m_pauseDownload ? (m_standBy ? ", Paused" : ", Pausing") : "", *downloadLimit, controlSeq); printf("%s", *status); m_needGoBack = true; } void ColoredFrontend::PrintMessage(Message& message) { #ifdef WIN32 switch (message.GetKind()) { case Message::mkDebug: SetConsoleTextAttribute(m_console, 8); printf("[DEBUG]"); break; case Message::mkError: SetConsoleTextAttribute(m_console, 4); printf("[ERROR]"); break; case Message::mkWarning: SetConsoleTextAttribute(m_console, 5); printf("[WARNING]"); break; case Message::mkInfo: SetConsoleTextAttribute(m_console, 2); printf("[INFO]"); break; case Message::mkDetail: SetConsoleTextAttribute(m_console, 2); printf("[DETAIL]"); break; } SetConsoleTextAttribute(m_console, 7); CString msg = message.GetText(); CharToOem(msg, msg); printf(" %s\n", *msg); #else const char* msg = message.GetText(); switch (message.GetKind()) { case Message::mkDebug: printf("[DEBUG] %s\033[K\n", msg); break; case Message::mkError: printf("\033[31m[ERROR]\033[39m %s\033[K\n", msg); break; case Message::mkWarning: printf("\033[35m[WARNING]\033[39m %s\033[K\n", msg); break; case Message::mkInfo: printf("\033[32m[INFO]\033[39m %s\033[K\n", msg); break; case Message::mkDetail: printf("\033[32m[DETAIL]\033[39m %s\033[K\n", msg); break; } #endif } void ColoredFrontend::PrintSkip() { #ifdef WIN32 printf(".....\n"); #else printf(".....\033[K\n"); #endif } void ColoredFrontend::BeforeExit() { if (IsRemoteMode()) { printf("\n"); } } nzbget-19.1/daemon/frontend/NCursesFrontend.cpp0000644000175000017500000010035513130203062021413 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #ifndef DISABLE_CURSES // "ncurses.h" contains many global defines such as for "OK" or "clear" which we sure don't want // everywhere in the project. For that reason we include "ncurses.h" directly here instead of // putting it into global header file "nzbget.h". #ifdef HAVE_NCURSES_H #include #endif #ifdef HAVE_NCURSES_NCURSES_H #include #endif #include "NCursesFrontend.h" #include "Options.h" #include "Util.h" #ifndef WIN32 // curses.h on Solaris declares "clear()" via DEFINE. That causes problems, because // it also affects calls to deque's method "clear()", producing compiler errors. // We use function "curses_clear()" to call macro "clear" of curses, then // undefine macro "clear". void curses_clear() { clear(); } #undef clear #endif extern void ExitProc(); static const int NCURSES_COLORPAIR_TEXT = 1; static const int NCURSES_COLORPAIR_INFO = 2; static const int NCURSES_COLORPAIR_WARNING = 3; static const int NCURSES_COLORPAIR_ERROR = 4; static const int NCURSES_COLORPAIR_DEBUG = 5; static const int NCURSES_COLORPAIR_DETAIL = 6; static const int NCURSES_COLORPAIR_STATUS = 7; static const int NCURSES_COLORPAIR_KEYBAR = 8; static const int NCURSES_COLORPAIR_INFOLINE = 9; static const int NCURSES_COLORPAIR_TEXTHIGHL = 10; static const int NCURSES_COLORPAIR_CURSOR = 11; static const int NCURSES_COLORPAIR_HINT = 12; static const int MAX_SCREEN_WIDTH = 512; #ifdef WIN32 static const int COLOR_BLACK = 0; static const int COLOR_BLUE = FOREGROUND_BLUE; static const int COLOR_RED = FOREGROUND_RED; static const int COLOR_GREEN = FOREGROUND_GREEN; static const int COLOR_WHITE = FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN; static const int COLOR_MAGENTA = FOREGROUND_RED | FOREGROUND_BLUE; static const int COLOR_CYAN = FOREGROUND_BLUE | FOREGROUND_GREEN; static const int COLOR_YELLOW = FOREGROUND_RED | FOREGROUND_GREEN; static const int READKEY_EMPTY = 0; #define KEY_DOWN VK_DOWN #define KEY_UP VK_UP #define KEY_PPAGE VK_PRIOR #define KEY_NPAGE VK_NEXT #define KEY_END VK_END #define KEY_HOME VK_HOME #define KEY_BACKSPACE VK_BACK #else static const int READKEY_EMPTY = ERR; #endif NCursesFrontend::NCursesFrontend() { m_summary = true; m_fileList = true; m_showNzbname = g_Options->GetCursesNzbName(); m_showTimestamp = g_Options->GetCursesTime(); m_groupFiles = g_Options->GetCursesGroup(); // Setup curses #ifdef WIN32 HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO ConsoleCursorInfo; GetConsoleCursorInfo(hConsole, &ConsoleCursorInfo); ConsoleCursorInfo.bVisible = false; SetConsoleCursorInfo(hConsole, &ConsoleCursorInfo); if (IsRemoteMode()) { SetConsoleTitle("NZBGet - remote mode"); } else { SetConsoleTitle("NZBGet"); } #else m_window = initscr(); if (m_window == nullptr) { printf("ERROR: m_pWindow == nullptr\n"); exit(-1); } keypad(stdscr, true); nodelay((WINDOW*)m_window, true); noecho(); curs_set(0); m_useColor = has_colors(); #endif if (m_useColor) { #ifndef WIN32 start_color(); #endif init_pair(0, COLOR_WHITE, COLOR_BLUE); init_pair(NCURSES_COLORPAIR_TEXT, COLOR_WHITE, COLOR_BLACK); init_pair(NCURSES_COLORPAIR_INFO, COLOR_GREEN, COLOR_BLACK); init_pair(NCURSES_COLORPAIR_WARNING, COLOR_MAGENTA, COLOR_BLACK); init_pair(NCURSES_COLORPAIR_ERROR, COLOR_RED, COLOR_BLACK); init_pair(NCURSES_COLORPAIR_DEBUG, COLOR_WHITE, COLOR_BLACK); init_pair(NCURSES_COLORPAIR_DETAIL, COLOR_GREEN, COLOR_BLACK); init_pair(NCURSES_COLORPAIR_STATUS, COLOR_BLUE, COLOR_WHITE); init_pair(NCURSES_COLORPAIR_KEYBAR, COLOR_WHITE, COLOR_BLUE); init_pair(NCURSES_COLORPAIR_INFOLINE, COLOR_WHITE, COLOR_BLUE); init_pair(NCURSES_COLORPAIR_TEXTHIGHL, COLOR_BLACK, COLOR_CYAN); init_pair(NCURSES_COLORPAIR_CURSOR, COLOR_BLACK, COLOR_YELLOW); init_pair(NCURSES_COLORPAIR_HINT, COLOR_WHITE, COLOR_RED); } } NCursesFrontend::~NCursesFrontend() { #ifdef WIN32 HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO ConsoleCursorInfo; GetConsoleCursorInfo(hConsole, &ConsoleCursorInfo); ConsoleCursorInfo.bVisible = true; SetConsoleCursorInfo(hConsole, &ConsoleCursorInfo); #else keypad(stdscr, false); echo(); curs_set(1); endwin(); #endif printf("\n"); SetHint(nullptr); } void NCursesFrontend::Run() { debug("Entering NCursesFrontend-loop"); m_dataUpdatePos = 0; while (!IsStopped()) { // The data (queue and log) is updated each m_iUpdateInterval msec, // but the window is updated more often for better reaction on user's input bool updateNow = false; int key = ReadConsoleKey(); if (key != READKEY_EMPTY) { // Update now and next if a key is pressed. updateNow = true; m_updateNextTime = true; } else if (m_updateNextTime) { // Update due to key being pressed during previous call. updateNow = true; m_updateNextTime = false; } else if (m_dataUpdatePos <= 0) { updateNow = true; m_updateNextTime = false; } if (updateNow) { Update(key); } if (m_dataUpdatePos <= 0) { m_dataUpdatePos = m_updateInterval; } usleep(10 * 1000); m_dataUpdatePos -= 10; } FreeData(); debug("Exiting NCursesFrontend-loop"); } void NCursesFrontend::NeedUpdateData() { m_dataUpdatePos = 10; m_updateNextTime = true; } void NCursesFrontend::Update(int key) { // Figure out how big the screen is CalcWindowSizes(); if (m_dataUpdatePos <= 0) { FreeData(); m_neededLogEntries = m_messagesWinClientHeight; if (!PrepareData()) { return; } // recalculate frame sizes CalcWindowSizes(); } if (m_inputMode == editQueue) { int queueSize = CalcQueueSize(); if (queueSize == 0) { m_selectedQueueEntry = 0; m_inputMode = normal; } } //------------------------------------------ // Print Current NZBInfoList //------------------------------------------ if (m_queueWinHeight > 0) { PrintQueue(); } //------------------------------------------ // Print Messages //------------------------------------------ if (m_messagesWinHeight > 0) { PrintMessages(); } PrintStatus(); PrintKeyInputBar(); UpdateInput(key); RefreshScreen(); } void NCursesFrontend::CalcWindowSizes() { int nrRows, nrColumns; #ifdef WIN32 HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO BufInfo; GetConsoleScreenBufferInfo(hConsole, &BufInfo); nrRows = BufInfo.srWindow.Bottom - BufInfo.srWindow.Top + 1; nrColumns = BufInfo.srWindow.Right - BufInfo.srWindow.Left + 1; #else getmaxyx(stdscr, nrRows, nrColumns); #endif if (nrRows != m_screenHeight || nrColumns != m_screenWidth) { #ifdef WIN32 int screenAreaSize = nrRows * nrColumns; m_screenBuffer.resize(screenAreaSize); m_oldScreenBuffer.resize(screenAreaSize); #else curses_clear(); #endif m_screenHeight = nrRows; m_screenWidth = nrColumns; } int queueSize = CalcQueueSize(); m_queueWinTop = 0; m_queueWinHeight = (m_screenHeight - 2) * m_queueWindowPercentage / 100; if (m_queueWinHeight - 1 > queueSize) { m_queueWinHeight = queueSize > 0 ? queueSize + 1 : 1 + 1; } m_queueWinClientHeight = m_queueWinHeight - 1; if (m_queueWinClientHeight < 0) { m_queueWinClientHeight = 0; } m_messagesWinTop = m_queueWinTop + m_queueWinHeight; m_messagesWinHeight = m_screenHeight - m_queueWinHeight - 2; m_messagesWinClientHeight = m_messagesWinHeight - 1; if (m_messagesWinClientHeight < 0) { m_messagesWinClientHeight = 0; } } int NCursesFrontend::CalcQueueSize() { int queueSize = 0; if (m_groupFiles) { queueSize = DownloadQueue::Guard()->GetQueue()->size(); } else { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { queueSize += nzbInfo->GetFileList()->size(); } } return queueSize; } void NCursesFrontend::PlotLine(const char * string, int row, int pos, int colorPair) { BString<1024> buffer("%-*s", m_screenWidth, string); int len = buffer.Length(); if (len > m_screenWidth - pos && m_screenWidth - pos < MAX_SCREEN_WIDTH) { buffer[m_screenWidth - pos] = '\0'; } PlotText(buffer, row, pos, colorPair, false); } void NCursesFrontend::PlotText(const char * string, int row, int pos, int colorPair, bool blink) { #ifdef WIN32 int bufPos = row * m_screenWidth + pos; int len = strlen(string); for (int i = 0; i < len; i++) { char c = string[i]; CharToOemBuff(&c, &c, 1); m_screenBuffer[bufPos + i].Char.AsciiChar = c; m_screenBuffer[bufPos + i].Attributes = m_colorAttr[colorPair]; } #else if( m_useColor ) { attron(COLOR_PAIR(colorPair)); if (blink) { attron(A_BLINK); } } mvaddstr(row, pos, (char*)string); if( m_useColor ) { attroff(COLOR_PAIR(colorPair)); if (blink) { attroff(A_BLINK); } } #endif } void NCursesFrontend::RefreshScreen() { #ifdef WIN32 bool bufChanged = !std::equal(m_screenBuffer.begin(), m_screenBuffer.end(), m_oldScreenBuffer.begin(), m_oldScreenBuffer.end(), [](CHAR_INFO& a, CHAR_INFO& b) { return a.Char.AsciiChar == b.Char.AsciiChar && a.Attributes == b.Attributes; }); if (bufChanged) { COORD BufSize; BufSize.X = m_screenWidth; BufSize.Y = m_screenHeight; COORD BufCoord; BufCoord.X = 0; BufCoord.Y = 0; HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO BufInfo; GetConsoleScreenBufferInfo(hConsole, &BufInfo); WriteConsoleOutput(hConsole, m_screenBuffer.data(), BufSize, BufCoord, &BufInfo.srWindow); BufInfo.dwCursorPosition.X = BufInfo.srWindow.Right; BufInfo.dwCursorPosition.Y = BufInfo.srWindow.Bottom; SetConsoleCursorPosition(hConsole, BufInfo.dwCursorPosition); m_oldScreenBuffer = m_screenBuffer; } #else // Cursor placement wmove((WINDOW*)m_window, m_screenHeight, m_screenWidth); // NCurses refresh refresh(); #endif } #ifdef WIN32 void NCursesFrontend::init_pair(int colorNumber, WORD wForeColor, WORD wBackColor) { m_colorAttr.resize(colorNumber + 1); m_colorAttr[colorNumber] = wForeColor | (wBackColor << 4); } #endif void NCursesFrontend::PrintMessages() { int lineNr = m_messagesWinTop; BString<1024> buffer("%s Messages", m_useColor ? "" : "*** "); PlotLine(buffer, lineNr++, 0, NCURSES_COLORPAIR_INFOLINE); int line = lineNr + m_messagesWinClientHeight - 1; int linesToPrint = m_messagesWinClientHeight; GuardedMessageList messages = GuardMessages(); // print messages from bottom for (int i = (int)messages->size() - 1; i >= 0 && linesToPrint > 0; i--) { int printedLines = PrintMessage(messages->at(i), line, linesToPrint); line -= printedLines; linesToPrint -= printedLines; } if (linesToPrint > 0) { // too few messages, print them again from top line = lineNr + m_messagesWinClientHeight - 1; while (linesToPrint-- > 0) { PlotLine("", line--, 0, NCURSES_COLORPAIR_TEXT); } int linesToPrint2 = m_messagesWinClientHeight; for (int i = (int)messages->size() - 1; i >= 0 && linesToPrint2 > 0; i--) { int printedLines = PrintMessage(messages->at(i), line, linesToPrint2); line -= printedLines; linesToPrint2 -= printedLines; } } } int NCursesFrontend::PrintMessage(Message& msg, int row, int maxLines) { const char* messageType[] = { "INFO ", "WARNING ", "ERROR ", "DEBUG ", "DETAIL "}; const int messageTypeColor[] = { NCURSES_COLORPAIR_INFO, NCURSES_COLORPAIR_WARNING, NCURSES_COLORPAIR_ERROR, NCURSES_COLORPAIR_DEBUG, NCURSES_COLORPAIR_DETAIL }; CString text; if (m_showTimestamp) { time_t rawtime = msg.GetTime() + g_Options->GetTimeCorrection(); text.Format("%s - %s", *Util::FormatTime(rawtime), msg.GetText()); } else { text = msg.GetText(); } // replace some special characters with spaces for (char* p = (char*)text; *p; p++) { if (*p == '\n' || *p == '\r' || *p == '\b') { *p = ' '; } } int len = strlen(text); int winWidth = m_screenWidth - 8; int msgLines = len / winWidth; if (len % winWidth > 0) { msgLines++; } int lines = 0; for (int i = msgLines - 1; i >= 0 && lines < maxLines; i--) { int r = row - msgLines + i + 1; PlotLine(text + winWidth * i, r, 8, NCURSES_COLORPAIR_TEXT); if (i == 0) { PlotText(messageType[msg.GetKind()], r, 0, messageTypeColor[msg.GetKind()], false); } else { PlotText(" ", r, 0, messageTypeColor[msg.GetKind()], false); } lines++; } return lines; } void NCursesFrontend::PrintStatus() { int statusRow = m_screenHeight - 2; BString<100> timeString; int currentDownloadSpeed = m_standBy ? 0 : m_currentDownloadSpeed; if (currentDownloadSpeed > 0 && !m_pauseDownload) { int64 remain_sec = (int64)(m_remainingSize / currentDownloadSpeed); int h = (int)(remain_sec / 3600); int m = (int)((remain_sec % 3600) / 60); int s = (int)(remain_sec % 60); timeString.Format(" (~ %.2d:%.2d:%.2d)", h, m, s); } BString<100> downloadLimit; if (m_downloadLimit > 0) { downloadLimit.Format(", Limit %i KB/s", m_downloadLimit / 1024); } BString<100> postStatus; if (m_postJobCount > 0) { postStatus.Format(", %i post-job%s", m_postJobCount, m_postJobCount > 1 ? "s" : ""); } int averageSpeed = (int)(m_dnTimeSec > 0 ? m_allBytes / m_dnTimeSec : 0); BString<1024> status(" %d threads, %s, %s remaining%s%s%s%s, Avg. %s", m_threadCount, *Util::FormatSpeed(currentDownloadSpeed), *Util::FormatSize(m_remainingSize), *timeString, *postStatus, m_pauseDownload ? (m_standBy ? ", Paused" : ", Pausing") : "", *downloadLimit, *Util::FormatSpeed(averageSpeed)); PlotLine(status, statusRow, 0, NCURSES_COLORPAIR_STATUS); } void NCursesFrontend::PrintKeyInputBar() { int queueSize = CalcQueueSize(); int inputBarRow = m_screenHeight - 1; if (!m_hint.Empty()) { time_t time = Util::CurrentTime(); if (time - m_startHint < 5) { PlotLine(m_hint, inputBarRow, 0, NCURSES_COLORPAIR_HINT); return; } else { SetHint(nullptr); } } switch (m_inputMode) { case normal: if (m_groupFiles) { PlotLine("(Q)uit | (E)dit | (P)ause | (R)ate | (W)indow | (G)roup | (T)ime", inputBarRow, 0, NCURSES_COLORPAIR_KEYBAR); } else { PlotLine("(Q)uit | (E)dit | (P)ause | (R)ate | (W)indow | (G)roup | (T)ime | n(Z)b", inputBarRow, 0, NCURSES_COLORPAIR_KEYBAR); } break; case editQueue: { const char* status = nullptr; if (m_selectedQueueEntry > 0 && queueSize > 1 && m_selectedQueueEntry == queueSize - 1) { status = "(Q)uit | (E)xit | (P)ause | (D)elete | (U)p/(T)op"; } else if (queueSize > 1 && m_selectedQueueEntry == 0) { status = "(Q)uit | (E)xit | (P)ause | (D)elete | dow(N)/(B)ottom"; } else if (queueSize > 1) { status = "(Q)uit | (E)xit | (P)ause | (D)elete | (U)p/dow(N)/(T)op/(B)ottom"; } else { status = "(Q)uit | (E)xit | (P)ause | (D)elete"; } PlotLine(status, inputBarRow, 0, NCURSES_COLORPAIR_KEYBAR); break; } case downloadRate: BString<100> hint("Download rate: %i", m_inputValue); PlotLine(hint, inputBarRow, 0, NCURSES_COLORPAIR_KEYBAR); // Print the cursor PlotText(" ", inputBarRow, 15 + m_inputNumberIndex, NCURSES_COLORPAIR_CURSOR, true); break; } } void NCursesFrontend::SetHint(const char* hint) { m_hint = hint; if (!m_hint.Empty()) { m_startHint = Util::CurrentTime(); } } void NCursesFrontend::PrintQueue() { if (m_groupFiles) { PrintGroupQueue(); } else { PrintFileQueue(); } } void NCursesFrontend::PrintFileQueue() { int lineNr = m_queueWinTop + 1; int64 remaining = 0; int64 paused = 0; int pausedFiles = 0; int fileNum = 0; GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (fileNum >= m_queueScrollOffset && fileNum < m_queueScrollOffset + m_queueWinHeight -1) { PrintFilename(fileInfo, lineNr++, fileNum == m_selectedQueueEntry); } fileNum++; if (fileInfo->GetPaused()) { pausedFiles++; paused += fileInfo->GetRemainingSize(); } remaining += fileInfo->GetRemainingSize(); } } if (fileNum > 0) { BString<1024> header(" %sFiles for downloading - %i / %i files in queue - %s / %s", m_useColor ? "" : "*** ", fileNum, fileNum - pausedFiles, *Util::FormatSize(remaining), *Util::FormatSize(remaining - paused)); PrintTopHeader(header, m_queueWinTop, true); } else { lineNr--; BString<1024> header("%s Files for downloading", m_useColor ? "" : "*** "); PrintTopHeader(header, lineNr++, true); PlotLine("Ready to receive nzb-job", lineNr++, 0, NCURSES_COLORPAIR_TEXT); } } void NCursesFrontend::PrintFilename(FileInfo * fileInfo, int row, bool selected) { int color = 0; const char* Brace1 = "["; const char* Brace2 = "]"; if (m_inputMode == editQueue && selected) { color = NCURSES_COLORPAIR_TEXTHIGHL; if (!m_useColor) { Brace1 = "<"; Brace2 = ">"; } } else { color = NCURSES_COLORPAIR_TEXT; } const char* downloading = ""; if (fileInfo->GetActiveDownloads() > 0) { downloading = " *"; } BString<100> priority; if (fileInfo->GetNzbInfo()->GetPriority() != 0) { priority.Format(" [%+i]", fileInfo->GetNzbInfo()->GetPriority()); } BString<100> completed; if (fileInfo->GetRemainingSize() < fileInfo->GetSize()) { completed.Format(", %i%%", (int)(100 - fileInfo->GetRemainingSize() * 100 / fileInfo->GetSize())); } BString<1024> nzbNiceName; if (m_showNzbname) { nzbNiceName.Format("%s%c", fileInfo->GetNzbInfo()->GetName(), PATH_SEPARATOR); } BString<1024> text("%s%i%s%s%s %s%s (%s%s)%s", Brace1, fileInfo->GetId(), Brace2, *priority, downloading, *nzbNiceName, fileInfo->GetFilename(), *Util::FormatSize(fileInfo->GetSize()), *completed, fileInfo->GetPaused() ? " (paused)" : ""); PlotLine(text, row, 0, color); } void NCursesFrontend::PrintTopHeader(char* header, int lineNr, bool upTime) { BString<1024> buffer("%-*s", m_screenWidth, header); int headerLen = strlen(header); int charsLeft = m_screenWidth - headerLen - 2; int time = upTime ? m_upTimeSec : m_dnTimeSec; int d = time / 3600 / 24; int h = (time % (3600 * 24)) / 3600; int m = (time % 3600) / 60; int s = time % 60; BString<100> timeStr; if (d == 0) { timeStr.Format("%.2d:%.2d:%.2d", h, m, s); if ((int)strlen(timeStr) > charsLeft) { timeStr.Format("%.2d:%.2d", h, m); } } else { timeStr.Format("%i %s %.2d:%.2d:%.2d", d, (d == 1 ? "day" : "days"), h, m, s); if ((int)strlen(timeStr) > charsLeft) { timeStr.Format("%id %.2d:%.2d:%.2d", d, h, m, s); } if ((int)strlen(timeStr) > charsLeft) { timeStr.Format("%id %.2d:%.2d", d, h, m); } } const char* shortCap = upTime ? " Up " : "Dn "; const char* longCap = upTime ? " Uptime " : " Download-time "; int timeLen = strlen(timeStr); int shortCapLen = strlen(shortCap); int longCapLen = strlen(longCap); if (charsLeft - timeLen - longCapLen >= 0) { snprintf(buffer + m_screenWidth - timeLen - longCapLen, MAX_SCREEN_WIDTH - (m_screenWidth - timeLen - longCapLen), "%s%s", longCap, *timeStr); } else if (charsLeft - timeLen - shortCapLen >= 0) { snprintf(buffer + m_screenWidth - timeLen - shortCapLen, MAX_SCREEN_WIDTH - (m_screenWidth - timeLen - shortCapLen), "%s%s", shortCap, *timeStr); } else if (charsLeft - timeLen >= 0) { snprintf(buffer + m_screenWidth - timeLen, MAX_SCREEN_WIDTH - (m_screenWidth - timeLen), "%s", *timeStr); } PlotLine(buffer, lineNr, 0, NCURSES_COLORPAIR_INFOLINE); } void NCursesFrontend::PrintGroupQueue() { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); int lineNr = m_queueWinTop; if (downloadQueue->GetQueue()->empty()) { BString<1024> buffer("%s NZBs for downloading", m_useColor ? "" : "*** "); PrintTopHeader(buffer, lineNr++, false); PlotLine("Ready to receive nzb-job", lineNr++, 0, NCURSES_COLORPAIR_TEXT); } else { lineNr++; ResetColWidths(); int calcLineNr = lineNr; int i = 0; for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { if (i >= m_queueScrollOffset && i < m_queueScrollOffset + m_queueWinHeight -1) { PrintGroupname(nzbInfo, calcLineNr++, false, true); } i++; } int64 remaining = 0; int64 paused = 0; i = 0; for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { if (i >= m_queueScrollOffset && i < m_queueScrollOffset + m_queueWinHeight -1) { PrintGroupname(nzbInfo, lineNr++, i == m_selectedQueueEntry, false); } i++; remaining += nzbInfo->GetRemainingSize(); paused += nzbInfo->GetPausedSize(); } BString<1024> buffer(" %sNZBs for downloading - %i NZBs in queue - %s / %s", m_useColor ? "" : "*** ", (int)downloadQueue->GetQueue()->size(), *Util::FormatSize(remaining), *Util::FormatSize(remaining - paused)); PrintTopHeader(buffer, m_queueWinTop, false); } } void NCursesFrontend::ResetColWidths() { m_colWidthFiles = 0; m_colWidthTotal = 0; m_colWidthLeft = 0; } void NCursesFrontend::PrintGroupname(NzbInfo* nzbInfo, int row, bool selected, bool calcColWidth) { int color = NCURSES_COLORPAIR_TEXT; char chBrace1 = '['; char chBrace2 = ']'; if (m_inputMode == editQueue && selected) { color = NCURSES_COLORPAIR_TEXTHIGHL; if (!m_useColor) { chBrace1 = '<'; chBrace2 = '>'; } } const char* downloading = ""; if (nzbInfo->GetActiveDownloads() > 0) { downloading = " *"; } BString<100> priority; if (nzbInfo->GetPriority() != 0) { priority.Format(" [%+i]", nzbInfo->GetPriority()); } // Format: // [id - id] Name Left-Files/Paused Total Left Time // [1-2] Nzb-name 999/999 999.99 MB 999.99 MB 00:00:00 int nameLen = 0; if (calcColWidth) { nameLen = m_screenWidth - 1 - 9 - 11 - 11 - 9; } else { nameLen = m_screenWidth - 1 - m_colWidthFiles - 2 - m_colWidthTotal - 2 - m_colWidthLeft - 2 - 9; } BString<1024> buffer; bool printFormatted = nameLen > 20; if (printFormatted) { BString<100> files("%i/%i", (int)nzbInfo->GetFileList()->size(), nzbInfo->GetPausedFileCount()); BString<1024> nameWithIds("%c%i%c%s%s %s", chBrace1, nzbInfo->GetId(), chBrace2, *priority, downloading, nzbInfo->GetName()); int64 unpausedRemainingSize = nzbInfo->GetRemainingSize() - nzbInfo->GetPausedSize(); CString remaining = Util::FormatSize(unpausedRemainingSize); CString total = Util::FormatSize(nzbInfo->GetSize()); BString<100> time; int currentDownloadSpeed = m_standBy ? 0 : m_currentDownloadSpeed; if (nzbInfo->GetPausedSize() > 0 && unpausedRemainingSize == 0) { time = "[paused]"; remaining = Util::FormatSize(nzbInfo->GetRemainingSize()); } else if (currentDownloadSpeed > 0 && !m_pauseDownload) { int64 remain_sec = (int64)(unpausedRemainingSize / currentDownloadSpeed); int h = (int)(remain_sec / 3600); int m = (int)((remain_sec % 3600) / 60); int s = (int)(remain_sec % 60); if (h < 100) { time.Format("%.2d:%.2d:%.2d", h, m, s); } else { time.Format("99:99:99"); } } if (calcColWidth) { int colWidthFiles = strlen(files); m_colWidthFiles = colWidthFiles > m_colWidthFiles ? colWidthFiles : m_colWidthFiles; int colWidthTotal = strlen(total); m_colWidthTotal = colWidthTotal > m_colWidthTotal ? colWidthTotal : m_colWidthTotal; int colWidthLeft = strlen(remaining); m_colWidthLeft = colWidthLeft > m_colWidthLeft ? colWidthLeft : m_colWidthLeft; } else { buffer.Format("%-*s %*s %*s %*s %8s", nameLen, *nameWithIds, m_colWidthFiles, *files, m_colWidthTotal, *total, m_colWidthLeft, *remaining, *time); } } else { buffer.Format("%c%i%c%s %s", chBrace1, nzbInfo->GetId(), chBrace2, downloading, nzbInfo->GetName()); } if (!calcColWidth) { PlotLine(buffer, row, 0, color); } } bool NCursesFrontend::EditQueue(DownloadQueue::EEditAction action, int offset) { int ID = 0; if (m_groupFiles) { GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); if (m_selectedQueueEntry >= 0 && m_selectedQueueEntry < (int)downloadQueue->GetQueue()->size()) { std::unique_ptr& nzbInfo = downloadQueue->GetQueue()->at(m_selectedQueueEntry); ID = nzbInfo->GetId(); if (action == DownloadQueue::eaFilePause) { if (nzbInfo->GetRemainingSize() == nzbInfo->GetPausedSize()) { action = DownloadQueue::eaFileResume; } else if (nzbInfo->GetPausedSize() == 0 && (nzbInfo->GetRemainingParCount() > 0) && !(m_lastPausePars && m_lastEditEntry == m_selectedQueueEntry)) { action = DownloadQueue::eaFilePauseExtraPars; m_lastPausePars = true; } else { action = DownloadQueue::eaFilePause; m_lastPausePars = false; } } } // map file-edit-actions to group-edit-actions DownloadQueue::EEditAction FileToGroupMap[] = { (DownloadQueue::EEditAction)0, DownloadQueue::eaGroupMoveOffset, DownloadQueue::eaGroupMoveTop, DownloadQueue::eaGroupMoveBottom, DownloadQueue::eaGroupPause, DownloadQueue::eaGroupResume, DownloadQueue::eaGroupDelete, DownloadQueue::eaGroupPauseAllPars, DownloadQueue::eaGroupPauseExtraPars }; action = FileToGroupMap[action]; } else { int fileNum = 0; GuardedDownloadQueue downloadQueue = DownloadQueue::Guard(); for (NzbInfo* nzbInfo : downloadQueue->GetQueue()) { for (FileInfo* fileInfo : nzbInfo->GetFileList()) { if (m_selectedQueueEntry == fileNum) { ID = fileInfo->GetId(); if (action == DownloadQueue::eaFilePause) { action = !fileInfo->GetPaused() ? DownloadQueue::eaFilePause : DownloadQueue::eaFileResume; } } fileNum++; } } } m_lastEditEntry = m_selectedQueueEntry; NeedUpdateData(); if (ID != 0) { return ServerEditQueue(action, offset, ID); } else { return false; } } void NCursesFrontend::SetCurrentQueueEntry(int entry) { int queueSize = CalcQueueSize(); if (entry < 0) { entry = 0; } else if (entry > queueSize - 1) { entry = queueSize - 1; } if (entry > m_queueScrollOffset + m_queueWinClientHeight || entry < m_queueScrollOffset - m_queueWinClientHeight) { m_queueScrollOffset = entry - m_queueWinClientHeight / 2; } else if (entry < m_queueScrollOffset) { m_queueScrollOffset -= m_queueWinClientHeight; } else if (entry >= m_queueScrollOffset + m_queueWinClientHeight) { m_queueScrollOffset += m_queueWinClientHeight; } if (m_queueScrollOffset > queueSize - m_queueWinClientHeight) { m_queueScrollOffset = queueSize - m_queueWinClientHeight; } if (m_queueScrollOffset < 0) { m_queueScrollOffset = 0; } m_selectedQueueEntry = entry; } /* * Process keystrokes starting with the initialKey, which must not be * READKEY_EMPTY but has alread been set via ReadConsoleKey. */ void NCursesFrontend::UpdateInput(int initialKey) { int key = initialKey; while (key != READKEY_EMPTY) { int queueSize = CalcQueueSize(); // Normal or edit queue mode if (m_inputMode == normal || m_inputMode == editQueue) { switch (key) { case 'q': // Key 'q' for quit ExitProc(); break; case 'z': // show/hide NZBFilename m_showNzbname = !m_showNzbname; break; case 'w': // swicth window sizes if (m_queueWindowPercentage == 50) { m_queueWindowPercentage = 100; } else if (m_queueWindowPercentage == 100 && m_inputMode != editQueue) { m_queueWindowPercentage = 0; } else { m_queueWindowPercentage = 50; } CalcWindowSizes(); SetCurrentQueueEntry(m_selectedQueueEntry); break; case 'g': // group/ungroup files m_groupFiles = !m_groupFiles; SetCurrentQueueEntry(m_selectedQueueEntry); NeedUpdateData(); break; } } // Normal mode if (m_inputMode == normal) { switch (key) { case 'p': // Key 'p' for pause if (!IsRemoteMode()) { info(m_pauseDownload ? "Unpausing download" : "Pausing download"); } ServerPauseUnpause(!m_pauseDownload); break; case 'e': case 10: // return case 13: // enter if (queueSize > 0) { m_inputMode = editQueue; if (m_queueWindowPercentage == 0) { m_queueWindowPercentage = 50; } return; } break; case 'r': // Download rate m_inputMode = downloadRate; m_inputNumberIndex = 0; m_inputValue = 0; return; case 't': // show/hide Timestamps m_showTimestamp = !m_showTimestamp; break; } } // Edit Queue mode if (m_inputMode == editQueue) { switch (key) { case 'e': case 10: // return case 13: // enter m_inputMode = normal; return; case KEY_DOWN: if (m_selectedQueueEntry < queueSize - 1) { SetCurrentQueueEntry(m_selectedQueueEntry + 1); } break; case KEY_UP: if (m_selectedQueueEntry > 0) { SetCurrentQueueEntry(m_selectedQueueEntry - 1); } break; case KEY_PPAGE: if (m_selectedQueueEntry > 0) { if (m_selectedQueueEntry == m_queueScrollOffset) { m_queueScrollOffset -= m_queueWinClientHeight; SetCurrentQueueEntry(m_selectedQueueEntry - m_queueWinClientHeight); } else { SetCurrentQueueEntry(m_queueScrollOffset); } } break; case KEY_NPAGE: if (m_selectedQueueEntry < queueSize - 1) { if (m_selectedQueueEntry == m_queueScrollOffset + m_queueWinClientHeight - 1) { m_queueScrollOffset += m_queueWinClientHeight; SetCurrentQueueEntry(m_selectedQueueEntry + m_queueWinClientHeight); } else { SetCurrentQueueEntry(m_queueScrollOffset + m_queueWinClientHeight - 1); } } break; case KEY_HOME: SetCurrentQueueEntry(0); break; case KEY_END: SetCurrentQueueEntry(queueSize > 0 ? queueSize - 1 : 0); break; case 'p': // Key 'p' for pause EditQueue(DownloadQueue::eaFilePause, 0); break; case 'd': SetHint(" Use Uppercase \"D\" for delete"); break; case 'D': // Delete entry if (EditQueue(DownloadQueue::eaFileDelete, 0)) { SetCurrentQueueEntry(m_selectedQueueEntry); } break; case 'u': if (EditQueue(DownloadQueue::eaFileMoveOffset, -1)) { SetCurrentQueueEntry(m_selectedQueueEntry - 1); } break; case 'n': if (EditQueue(DownloadQueue::eaFileMoveOffset, +1)) { SetCurrentQueueEntry(m_selectedQueueEntry + 1); } break; case 't': if (EditQueue(DownloadQueue::eaFileMoveTop, 0)) { SetCurrentQueueEntry(0); } break; case 'b': if (EditQueue(DownloadQueue::eaFileMoveBottom, 0)) { SetCurrentQueueEntry(queueSize > 0 ? queueSize - 1 : 0); } break; } } // Edit download rate input mode if (m_inputMode == downloadRate) { // Numbers if (m_inputNumberIndex < 5 && key >= '0' && key <= '9') { m_inputValue = (m_inputValue * 10) + (key - '0'); m_inputNumberIndex++; } // Enter else if (key == 10 || key == 13) { ServerSetDownloadRate(m_inputValue * 1024); m_inputMode = normal; return; } // Escape else if (key == 27) { m_inputMode = normal; return; } // Backspace else if (m_inputNumberIndex > 0 && key == KEY_BACKSPACE) { int remain = m_inputValue % 10; m_inputValue = (m_inputValue - remain) / 10; m_inputNumberIndex--; } } key = ReadConsoleKey(); } } int NCursesFrontend::ReadConsoleKey() { #ifdef WIN32 HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE); DWORD NumberOfEvents; BOOL ok = GetNumberOfConsoleInputEvents(hConsole, &NumberOfEvents); if (ok && NumberOfEvents > 0) { while (NumberOfEvents--) { INPUT_RECORD InputRecord; DWORD NumberOfEventsRead; if (ReadConsoleInput(hConsole, &InputRecord, 1, &NumberOfEventsRead) && NumberOfEventsRead > 0 && InputRecord.EventType == KEY_EVENT && InputRecord.Event.KeyEvent.bKeyDown) { char c = tolower(InputRecord.Event.KeyEvent.wVirtualKeyCode); if (bool(InputRecord.Event.KeyEvent.dwControlKeyState & CAPSLOCK_ON) ^ bool(InputRecord.Event.KeyEvent.dwControlKeyState & SHIFT_PRESSED)) { c = toupper(c); } return c; } } } return READKEY_EMPTY; #else return getch(); #endif } #endif nzbget-19.1/daemon/frontend/Frontend.h0000644000175000017500000000405613130203062017556 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef FRONTEND_H #define FRONTEND_H #include "Thread.h" #include "Log.h" #include "DownloadInfo.h" #include "MessageBase.h" #include "QueueEditor.h" class Frontend : public Thread { public: Frontend(); protected: bool m_summary = false; bool m_fileList = false; uint32 m_neededLogEntries = 0; uint32 m_neededLogFirstId = 0; int m_updateInterval; // summary int m_currentDownloadSpeed = 0; int64 m_remainingSize = 0; bool m_pauseDownload = false; int m_downloadLimit = 0; int m_threadCount = 0; int m_postJobCount = 0; int m_upTimeSec = 0; int m_dnTimeSec = 0; int64 m_allBytes = 0; bool m_standBy = false; bool PrepareData(); void FreeData(); GuardedMessageList GuardMessages(); bool IsRemoteMode(); void InitMessageBase(SNzbRequestBase* messageBase, int request, int size); void ServerPauseUnpause(bool pause); bool RequestPauseUnpause(bool pause); void ServerSetDownloadRate(int rate); bool RequestSetDownloadRate(int rate); bool ServerEditQueue(DownloadQueue::EEditAction action, int offset, int entry); bool RequestEditQueue(DownloadQueue::EEditAction action, int offset, int id); private: MessageList m_remoteMessages; bool RequestMessages(); bool RequestFileList(); }; #endif nzbget-19.1/daemon/frontend/LoggableFrontend.cpp0000644000175000017500000000453613130203062021551 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #include "nzbget.h" #include "LoggableFrontend.h" #include "Log.h" void LoggableFrontend::Run() { debug("Entering LoggableFrontend-loop"); while (!IsStopped()) { Update(); usleep(m_updateInterval * 1000); } // Printing the last messages Update(); BeforeExit(); debug("Exiting LoggableFrontend-loop"); } void LoggableFrontend::Update() { if (!PrepareData()) { FreeData(); return; } BeforePrint(); { GuardedMessageList messages = GuardMessages(); if (!messages->empty()) { Message& firstMessage = messages->front(); int start = m_neededLogFirstId - firstMessage.GetId() + 1; if (start < 0) { PrintSkip(); start = 0; } for (uint32 i = (uint32)start; i < messages->size(); i++) { PrintMessage(messages->at(i)); m_neededLogFirstId = messages->at(i).GetId(); } } } PrintStatus(); FreeData(); fflush(stdout); } void LoggableFrontend::PrintMessage(Message& message) { #ifdef WIN32 CString cmsg = message.GetText(); CharToOem(cmsg, cmsg); const char* msg = cmsg; #else const char* msg = message.GetText(); #endif switch (message.GetKind()) { case Message::mkDebug: printf("[DEBUG] %s\n", msg); break; case Message::mkError: printf("[ERROR] %s\n", msg); break; case Message::mkWarning: printf("[WARNING] %s\n", msg); break; case Message::mkInfo: printf("[INFO] %s\n", msg); break; case Message::mkDetail: printf("[DETAIL] %s\n", msg); break; } } void LoggableFrontend::PrintSkip() { printf(".....\n"); } nzbget-19.1/daemon/frontend/LoggableFrontend.h0000644000175000017500000000233413130203062021210 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2004 Sven Henkel * Copyright (C) 2007-2016 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, see . */ #ifndef LOGGABLEFRONTEND_H #define LOGGABLEFRONTEND_H #include "Frontend.h" #include "Log.h" class LoggableFrontend : public Frontend { protected: virtual void Run(); virtual void Update(); virtual void BeforePrint() {}; virtual void BeforeExit() {}; virtual void PrintMessage(Message& message); virtual void PrintStatus() {}; virtual void PrintSkip(); }; #endif nzbget-19.1/daemon/extension/0000755000175000017500000000000013130203062016016 5ustar andreasandreasnzbget-19.1/daemon/extension/QueueScript.h0000644000175000017500000000406213130203062020442 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2007-2017 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, see . */ #ifndef QUEUESCRIPT_H #define QUEUESCRIPT_H #include "DownloadInfo.h" #include "ScriptConfig.h" class QueueScriptCoordinator { public: enum EEvent { qeFileDownloaded, // lowest priority qeUrlCompleted, qeNzbMarked, qeNzbAdded, qeNzbNamed, qeNzbDownloaded, qeNzbDeleted // highest priority }; void Stop() { m_stopped = true; } void InitOptions(); void EnqueueScript(NzbInfo* nzbInfo, EEvent event); void CheckQueue(); bool HasJob(int nzbId, bool* active); int GetQueueSize(); static NzbInfo* FindNzbInfo(DownloadQueue* downloadQueue, int nzbId); private: class QueueItem { public: QueueItem(int nzbId, ScriptConfig::Script* script, EEvent event) : m_nzbId(nzbId), m_script(script), m_event(event) {} int GetNzbId() { return m_nzbId; } ScriptConfig::Script* GetScript() { return m_script; } EEvent GetEvent() { return m_event; } private: int m_nzbId; ScriptConfig::Script* m_script; EEvent m_event; }; typedef std::deque> Queue; Queue m_queue; Mutex m_queueMutex; std::unique_ptr m_curItem; bool m_hasQueueScripts = false; bool m_stopped = false; bool UsableScript(ScriptConfig::Script& script, NzbInfo* nzbInfo, EEvent event); }; extern QueueScriptCoordinator* g_QueueScriptCoordinator; #endif nzbget-19.1/daemon/extension/ScriptConfig.h0000644000175000017500000000722213130203062020564 0ustar andreasandreas/* * This file is part of nzbget. See . * * Copyright (C) 2013-2016 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, see . */ #ifndef SCRIPTCONFIG_H #define SCRIPTCONFIG_H #include "NString.h" #include "Container.h" #include "Options.h" class ScriptConfig { public: class Script { public: Script(const char* name, const char* location) : m_name(name), m_location(location), m_displayName(name) {}; Script(Script&&) = default; const char* GetName() { return m_name; } const char* GetLocation() { return m_location; } void SetDisplayName(const char* displayName) { m_displayName = displayName; } const char* GetDisplayName() { return m_displayName; } bool GetPostScript() { return m_postScript; } void SetPostScript(bool postScript) { m_postScript = postScript; } bool GetScanScript() { return m_scanScript; } void SetScanScript(bool scanScript) { m_scanScript = scanScript; } bool GetQueueScript() { return m_queueScript; } void SetQueueScript(bool queueScript) { m_queueScript = queueScript; } bool GetSchedulerScript() { return m_schedulerScript; } void SetSchedulerScript(bool schedulerScript) { m_schedulerScript = schedulerScript; } bool GetFeedScript() { return m_feedScript; } void SetFeedScript(bool feedScript) { m_feedScript = feedScript; } void SetQueueEvents(const char* queueEvents) { m_queueEvents = queueEvents; } const char* GetQueueEvents() { return m_queueEvents; } void SetTaskTime(const char* taskTime) { m_taskTime = taskTime; } const char* GetTaskTime() { return m_taskTime; } private: CString m_name; CString m_location; CString m_displayName; bool m_postScript = false; bool m_scanScript = false; bool m_queueScript = false; bool m_schedulerScript = false; bool m_feedScript = false; CString m_queueEvents; CString m_taskTime; }; typedef std::list