diff options
75 files changed, 27902 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c97a742 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.so +.dependencies +*.o +*.a +*~ +*.mo +*.pot +lib/demo +lib/tst @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +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. + + <signature of Ty Coon>, 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 Lesser General +Public License instead of this License. diff --git a/HISTORY.h b/HISTORY.h new file mode 100644 index 0000000..c3f3836 --- /dev/null +++ b/HISTORY.h @@ -0,0 +1,616 @@ +/* + * ----------------------------------- + * epg2vdr Plugin - Revision History + * ----------------------------------- + * + */ + +#define _VERSION "1.1.42" +#define VERSION_DATE "01.03.2017" + +#define DB_API 4 + +#ifdef GIT_REV +# define VERSION _VERSION "-GIT" GIT_REV +#else +# define VERSION _VERSION +#endif + +/* + * ------------------------------------ + +2017-03-01: version 1.1.43 (horchi) + - bugfix: Fixed crash in meues without database connection + +2017-02-27: version 1.1.41 (horchi) + - bugfix: Fixed lookup of repeating events + +2017-02-27: version 1.1.40 (horchi) + - change: minor changes (logging, ...) + +2017-02-24: version 1.1.39 (horchi) + - change: modified null field handling for timersdonedb + +2017-02-14: version 1.1.38 (horchi) + - bugfix: fixed compile bug + +2017-02-14: version 1.1.37 (horchi) + - bugfix: fixed detection if recording is complete or not + +2017-02-14: version 1.1.36 (horchi) + - added: command menu for schedules (key 0) + +2017-02-14: version 1.1.35 (horchi) + - change: modified timer delete/reject handling + +2017-02-10: version 1.1.34 (horchi) + - bugfix: Fixed possible crash at timer update + +2017-02-09: version 1.1.33 (horchi) + - added: Added epg handler patch for VDR 2.3.2 + -> enhancement of epg handler interface to fix a threading + problem which can occur on systems with more than one tuner + +2017-02-08: version 1.1.32 (horchi) + - bugfix: first program menu item now also selectable + +2017-02-08: version 1.1.31 (horchi) + - added: Added patch for VDR >= 2.3.1 + Important, without this patch it will not work! + +2017-02-06: version 1.1.30 (horchi) + - bugfix: fix of '1.1.28' next try ;) + +2017-02-06: version 1.1.29 (horchi) + - bugfix: fixed compile issue with gcc 6.2.0 + +2017-02-06: version 1.1.28 (horchi) + - bugfix: fixed sql error in epg search menu + +2017-02-01: version 1.1.27 (horchi) + - bugfix: fixed timer naming problem + +2017-01-19: version 1.1.26 (horchi) + - bugfix: Fixed buf with cyclic db reconnect + +2017-01-19: version 1.1.25 (horchi) + - added: support of namingmode 'template' by epgd/epghttpd + +2017-01-16: version 1.1.24 (horchi) + - change: Finished porting to VDR 2.3.2 + +2017-01-08: version 1.1.23 (horchi) + - bugfix: fixed schedules lock problem for vdr < 2.3.x + +2017-01-08: version 1.1.22 (horchi) + - change: fixed problem with 2.3.1 / 2.3.2 + +2017-01-07: version 1.1.21 (horchi) + - change: now compiles with vdr 2.3.1 / 2.3.2 + +2017-01-03: version 1.1.20 (horchi) + - bugfix: fixed reinstate of events in epg handler + +2016-11-30: version 1.1.19 (horchi) + - change: merging of lib + +2016-11-30: version 1.1.18 (horchi) + - added: Delete / Modify of timers on event changes + +2016-11-30: version 1.1.17 (horchi) + - change: improved format strings for 32 bit systems + +2016-11-02: version 1.1.16 (horchi) + - change: improved cleanup handling for recordings + +2016-11-02: version 1.1.15 (horchi) + - added: epg-detail menu toggle even for schedules + +2016-11-01: version 1.1.14 (horchi) + - added: added title for green and yellow button in epg-detail menu + +2016-11-01: version 1.1.13 (horchi) + - added: channel toggle in Event-Info menu (green/yellow) + +2016-11-01: version 1.1.12 (horchi) + - added: started implementaion of schedule toggle in Event-Info menu + +2016-10-30: version 1.1.11 (horchi) + - added: mark epgseasrch timers (set source to 'epgs') + +2016-10-20: version 1.1.10 (horchi) + - added: update of db field timers._starttime + +2016-10-18: version 1.1.9 (horchi) + - bugfix: fixed missing close of socket handle + +2016-10-17: version 1.1.8 (horchi) + - added: search repetition of event + +2016-10-16: version 1.1.7 (horchi) + - change: improved recording search using LV distance < 50% + +2016-08-26: version 1.1.6 (horchi) + - added: support of long eventids for tvsp (merged dev into master) + +2016-07-19: version 1.1.5 (rechner) + - change: increased event id to unsigned int64 + +2016-07-15: version 1.1.4 (horchi) + - change: started increase of event id to unsigned int64 + +2016-07-06: version 1.1.3 (horchi) + - bugfix: fixed channel switch in schedule menu + +2016-07-06: version 1.1.2 (horchi) + - change: minor change on menu refresh + +2016-07-05: version 1.1.1 (horchi) + - bugfix: fixed refreah of schedules menu on timer create + +2016-07-04: version 1.1.0 (horchi) + - change: merged http branch into master + +2016-06-10: version 1.0.49 (horchi) + - change: fixed unitialized variable (thx to Joerg) + +2016-06-01: version 1.0.48 (horchi) + - change: adjustet some log levels in epg handler (more silent) + +2016-06-01: version 1.0.47 (horchi) + - change: remove 'alter table' + - change: added new dictionary version + +2016-05-25: version 1.0.46 (horchi) + - change: optimized scrap recording trigger + +2016-05-24: version 1.0.45 (horchi) + - change: fixed sequence of epg load and perform of timer jobs at VDR start + +2016-05-23: version 1.0.44 (horchi) + - bugfix: fixed crash at timer-service on empty epg + +2016-05-23: version 1.0.43 (horchi) + - bugfix: fixed crash on setup-menu close + +2016-05-23: version 1.0.42 (horchi) + - added: support of 'highlighted' flag for user times + +2016-05-20: version 1.0.41 (horchi) + - added: new column for textual rating and commentator + - change: removed unused info column + +2016-05-18: version 1.0.40 (horchi) + - bugfix: Fixed toggle of timer state with [red] + - bugfix: Fixed change of timer start/end timer + +2016-05-17: version 1.0.39 (horchi) + - added: service to notify timer changes + +2016-05-13: version 1.0.38 (horchi) + - added: optimized timer lookup for schedules menu + +2016-05-12: version 1.0.37 (horchi) + - added: store MAC address to vdrs table + +2016-05-11: version 1.0.36 (horchi) + - added: State query for service interface + +2016-05-11: version 1.0.35 (horchi) + - change: honor 'Share in Web' config for service 'EPG2VDR_TIMER_SERVICE' + - change: set of NAMINGMODE and AUTOTIMEINSSP in timers table + +2016-05-09: version 1.0.34 (horchi) + - fixed: added sql string-escape for python return + +2016-05-04: version 1.0.33 (horchi) + - bugfix: fixed cursor pos in timermenu after refresh + +2016-05-03: version 1.0.32 (horchi) + - change: column with for searchtimer result menu + +2016-05-03: version 1.0.31 (horchi) + - added: parallel processing für timer service + +2016-05-02: version 1.0.30 (horchi) + - change: finished service interface for timers + +2016-05-02: version 1.0.29 (horchi) + - change: redesign of service interface for timers + +2016-04-23: version 1.0.28 (horchi) + - bugfix: added missing table init + +2016-04-23: version 1.0.27 (horchi) + - change: added totoal count dor service interface (ForEachTimer) + +2016-04-22: version 1.0.26 (horchi) + - bugfix: fixed set of scrnew on info.epg2vdr changes + +2016-04-22: version 1.0.25 (horchi) + - added: reconnect to database server on change of connection settigns + +2016-04-22: version 1.0.24 (horchi) + - bugfix: fixed crash on toggle NAS option + +2016-04-22: version 1.0.23 (horchi) + - bugfix: fixed compile without GTFT patch + +2016-04-21: version 1.0.22 (horchi) + - added: channel switch for channels without epg + +2016-04-20: version 1.0.21 (horchi) + - bugfix: set plugin menu category to mcMain + +2016-04-20: version 1.0.20 (horchi) + - change: set plugin menu category to mcMain + - added: ForEachTimer to service interface + - bugfix: fixed active/deactive distribution + +2016-04-19: version 1.0.19 (horchi) + - bugfix: fixed timer menu refresh + +2016-04-19: version 1.0.18 (horchi) + - bugfix: fixed active/deactive handling in timers menu + +2016-04-18: version 1.0.17 (horchi) + - added: added hack to force skindesigner to use 'favorite menu view' for user defined searches + +2016-04-18: version 1.0.16 (horchi) + - added: more german translations (thx to utility) + +016-04-18: version 1.0.15 (horchi) + - bugfix: fixed fix init of trigger flag :o ;) + +2016-04-18: version 1.0.14 (horchi) + - bugfix: fixed init of trigger flag (thx to mini73) + +2016-04-18: version 1.0.13 (horchi) + - added: temporary shortcut to searchtimer dialog in epg menu via key 3 + +2016-04-18: version 1.0.12 (horchi) + - added: option to show channels without epg in schedules menu + +2016-04-17: version 1.0.11 (horchi) + - change: removed needless dependency + +2016-04-15: version 1.0.10 (horchi) + - bugfix: fixed column width of search result menu + +2016-04-15: version 1.0.9 (horchi) + - bugfix: fixed column width of search timer menu + +2016-04-15: version 1.0.8 (horchi) + - bugfix: fixed channel switch in details menu + +2016-04-15: version 1.0.7 (horchi) + - added: setup option to x-change kBlue and kOk + +2016-04-14: version 1.0.6 (horchi) + - bugfix: fixed crash on large quicktimes value + +2016-04-14: version 1.0.5 (horchi) + - change: increased parameter size to 500 character + +2016-04-14: version 1.0.4 (horchi) + - added: Added user defined favorite search to epg menu + +2016-04-14: version 1.0.3 (horchi) + - change: Fixed log message typo + +2016-04-14: version 1.0.2 (horchi) + - change: Update of lib-horchi + - change: Morde debug log messages + +2016-04-14: version 1.0.1 (horchi) + - bugfix: Fixed crash in timers-done menu + +2016-04-05: version 1.0.0 (horchi) + - change: First BETA release + - change: Added config option 'create timer local' + - change: Use WEB settings of 'default VDR' at timer create + +2016-03-30: version 0.3.49 (horchi) + - bugfix: fixed default network device, + using first of device list except 'lo' as default + +2016-03-30: version 0.3.48 (horchi) + - bugfix: fixed buffer error on large aux values + +2016-03-26: version 0.3.47 (horchi) + - bugfix: fixed char compare of db API + +2016-03-22: version 0.3.46 (horchi) + - bugfix: fixed format of epg.dat + +2016-03-22: version 0.3.45 (horchi) + - changed: removed reason from timers table + +2016-03-21: version 0.3.44 (horchi) + - changed: added default value für timer state + +2016-03-18: version 0.3.43 + - added: added make update to Makefile + +2016-03-17: version 0.3.42 + - change: minor speed improvement for timer menu + +2016-03-17: version 0.3.41 + - change: igoring already tooked 'any'VDR' timer in timer menu + +2016-03-16: version 0.3.40 + - added: added log message on forced epg reload + - change: reset 'reason' on successfull retried timers + +2016-03-16: version 0.3.39 + - added: force load of events for specific channel on missing event at timer creation + +2016-03-15: version 0.3.38 + - bugfix: fixed timer job for not vdr bound timers again + +2016-03-15: version 0.3.37 + - bugfix: fixed timer job for not vdr bound timers + +2016-03-10: version 0.3.36 + - bugfix: fixed usage of db connection in wrong thread + +2016-03-05: version 0.3.35 + - bugfix: fixed segfault on parameter read + +2016-03-04: version 0.3.34 + - bugfix: fixed bug with parameter handling + - added: trigger update of video space on recording change + +2016-03-03: version 0.3.33 + - added: config option for network device + +2016-03-03: version 0.3.32 + - change: Now and Next of epg menu now configured in webif + (Use @Now and @Next as Time like 'Jetzt=@Now') + +2016-02-29: version 0.3.31 + - added: configuration for start page of schedule menu + - change: internal redesign of schedule menu implementation + +2016-02-26: version 0.3.30 + - added: more translations + +2016-02-26: version 0.3.29 + - bugfix: fixed compile problem with new translations (thx to Ole) + +2016-02-26: version 0.3.28 + - change: added german translations (thx to Ole) + +2016-02-26: version 0.3.27 + - change: trigger scaping of changed recordings + - change: fixed menu display + +2016-02-24: version 0.3.26 + - added: added missing files :o + +2016-02-24: version 0.3.25 + - added: epg2vdr skin interface for timer + +2016-02-18: version 0.3.24 + - added: delete option for dones to search-result menu + +2016-02-18: version 0.3.23 + - added: show count of already done timers in search-result menu + +2016-02-18: version 0.3.22 + - added: test-search for searchtimer + +2016-02-16: version 0.3.21 + - bugfix: fixed potetioal crash on empty user list + +2016-02-16: version 0.3.20 + - change: chnaged state for moved/deleted timers + +2016-02-16: version 0.3.19 + - bugfix: fixed timer move + +2016-02-15: version 0.3.18 + - added: check for recoring running at 'timer move' + +2016-02-15: version 0.3.17 + - change: hold menu position in epg menu at iterate over times + - change: added 'Timer still running' confirmation on timer delete + +2016-02-15: version 0.3.16 + - change: minor change of timer on 'move' + +2016-02-11: version 0.3.15 + - added: code and log message review + +2016-02-10: version 0.3.14 + - added: Reorganice recordingslist table at chage of useCommonRecFolder setting + +2016-02-10: version 0.3.13 + - added: Seach of recordings in programm menu with key '2' + +2016-02-09: version 0.3.12 + - bugfix: fixed problem timer handling + +2016-02-09: version 0.3.11 + - bugfix: fixed problem with timer state handling + +2016-02-08: version 0.3.10 + - change: ported new db lib + - change: improved updates of timersdone table + +2016-02-07: version 0.3.9 + - change: added missing trigger for timer update + +2016-02-05: version 0.3.8 + - added: delete and activate/deactivate searchtimers + +2016-02-05: version 0.3.7 + - added: centralized some code + - added: implemented delete button for done timers + +2016-02-05: version 0.3.6 + - added: searchtimer menu + +2016-02-05: version 0.3.5 + - added: take timer requests even without vdruuid + - change: some code cleanup + +2016-02-05: version 0.3.4 + - added: Display OSD message after manual triggered successful reload/update of EPG + +2016-02-04: version 0.3.3 + - bugfix: fixed compile issue + +2016-02-04: version 0.3.2 + - change: removed obsolete table from epg.dat + +2016-02-04: version 0.3.1 + - change: redesign of timerjobs, remove table timerdistribution + +2016-02-03: version 0.3.0 + - change: desupport VDR versions before 2.2.0 + - change: code cleanup + +2016-02-02: version 0.2.8 + - bugfix: fixed compile error + +2016-02-02: version 0.2.7 + - bugfix: fixed display of filename (cutted at : sign) + +2016-02-02: version 0.2.6 + - change: minor code cleanup + +2016-02-01: version 0.2.5 + - bugfix: fixed crash un undefined channel + +2016-02-01: version 0.2.4 + - bugfix: minor fix of config display + +2016-02-01: version 0.2.3 + - added: improved display of timer status in case of error + +2016-01-30: version 0.2.2 + - added: timer status and action to timer edit menu + - change: show only future events at repeat query + +2016-01-30: version 0.2.1 + - bugfix: minor fix + +2016-01-30: version 0.2.0 + - added: Replacement of VDRs Timer and Program menus, now directly support remote timer + +2016-01-21: version 0.1.18 + - bugfix: fixed possible crash at shutdown + +2016-01-18: version 0.1.17 + - change: initialize epg handler still earlier + - bugfix: fixed timer delete (via menu) + - bugfix: increased source field of table timerdistribution (to fit uuid) + +2016-01-18: version 0.1.16 + - bugfix: fixed timer move (via menu) + +2016-01-18: version 0.1.15 + - bugfix: fixed problem with double epg entries on merge = 0 + +2016-01-15: version 0.1.14 + - change: updated dictionary + +2015-11-02: version 0.1.13 + - change: recording path (now without base path) + - added: service to register mysql_lib_init + +2015-11-02: version 0.1.13 + - change: recording path (now without base path) + - added: service to register mysql_lib_init + +2015-06-25: version 0.1.12 + - change: start implementation of a timer menu + +2015-02-02: version 0.1.11 + - change: merges lib from epgd + +2014-12-28: version 0.1.10 + - bugfix: fixed possible crash at exit + - change: deleting old timers from timers table + +2014-05-13: version 0.1.9 + - bugfix: fixed mutex problem in epg handler + +2014-03-22: version 0.1.8 + - bugfix: fixed name lookup for mapdb + +2014-01-03: version 0.1.7 + - bugfix: fixed epg handler + +2014-01-03: version 0.1.6 + - change: adapted epgdata plugin to modified header + - change: added fields producer, other and camera + - change: removed fields origtitle and team + +2013-02-05: version 0.1.5 + - change: removed activity check, instead wait up to 20 seconds to give the mail loop time to finish + +2013-12-31: version 0.1.4 + - change: increased field guest to 1000 + +2013-12-04: version 0.1.3 + - bugfix: fixed insert handling with multimerge + - change: speedup reload with multimerge + +2013-11-20: version 0.1.2 + - change: increased shorttext and compshorttext to 300 chars, topic and guest to 500 chars + therefore you have to alter your tables + +2013-10-28: version 0.1.1 + - bugfix: fixed core on missing database + +2013-10-23: version 0.1.0 + - change: first release with epg merge + - added: 'LoadImages', 'Shutdown On Busy', 'Schedule Boot For Update' + and 'Prohibit Shutdown On Busy epgd' to plugin setup menu + - removed: "UpdateTime" Option + +2013-09-27: version 0.0.7b + - change: first alpha version with epg merge + - added: optional schedule of wakeup + - added: optional "Prohibit Shutdown On Busy 'epgd'" + - change: improved picture load and link handling + +2013-09-23: version 0.0.7a + - change: started devel branch for epg merge + +2013-09-16: version 0.0.6 + - bugfix: fixed install of locale files + - bugfix: fixed handler bug (thread conflict), reported by 3po + - change: improved handler speed + - change: enhanced connection check reported by OleS + - bugfix: problem with blacklist, reportet by theChief + +2013-09-05: version 0.0.5 + - change: pause update only if epgd is busy with events (not while epgd is loading images) + +2013-09-04: version 0.0.4 + - added: removed cyclic update og EPG from database + -> instead auto trigger update after epgd finished download + - added: get changes by DVB of "vdr:000" channels every 5 minutes + - added: pause epg handler while epgd is busy + - added: pause update while epgd is busy + +2013-08-31: version 0.0.3 + - change: improved speed of connection check + (speed of 0.0.1 restored) + +2013-08-30: version 0.0.2 + - bugfix: improved database reconnect + - added: check DBAPI on startup + - bugfix: empty shorttext now NULL instead of "" + +2013-08-28: version 0.0.1 + - first official release + +2012-12-19: version 0.0.1-rc1 + - initiale version + + * ------------------------------------ + */ diff --git a/Make.config b/Make.config new file mode 100644 index 0000000..5d52c29 --- /dev/null +++ b/Make.config @@ -0,0 +1,41 @@ + +# Make.config +# +# See the README file for copyright information and how to reach the author. +# +# + +# user defined stuff + +PREFIX = /usr/local + +DEBUG = 1 + +# ----------------------- +# don't touch below ;) + +CC = g++ +doCompile = $(CC) -c $(CFLAGS) $(DEFINES) +doLink = $(CC) $(LFLAGS) +doLib = ar -rs + +USEPYTHON = 1 +USEEPGS = 1 + +USES = -DVDR_PLUGIN -DUSEUUID -DUSEMD5 -DUSEJSON -DUSEGUNZIP -DPLUGIN_NAME_I18N='"$(PLUGIN)"' + +ifdef DEBUG + CXXFLAGS += -ggdb -O0 +endif + +CXXFLAGS += -fPIC -Wreturn-type -Wall -Wno-parentheses -Wformat -pedantic \ + -Wno-long-long -Wunused-variable -Wunused-label -Wno-unused-result \ + -Wunused-value -Wunused-but-set-variable -Wunused-function -Wno-variadic-macros \ + -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 + +CXXFLAGS += -D__STDC_FORMAT_MACROS + +CFLAGS += $(CXXFLAGS) + +%.o: %.c + $(doCompile) -o $@ $< diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f392783 --- /dev/null +++ b/Makefile @@ -0,0 +1,229 @@ +# +# Makefile for a Video Disk Recorder plugin +# + +PLUGIN = epg2vdr +HLIB = -L./lib -lhorchi +HISTFILE = "HISTORY.h" + +include Make.config + +# enable graphtftng and/or pin plugin support if autodetection below don't work +#WITH_GTFT = 1 +#WITH_PIN = 1 + +### The version number of this plugin (taken from the main source file): + +VERSION = $(shell grep 'define _VERSION ' $(HISTFILE) | awk '{ print $$3 }' | sed -e 's/[";]//g') + +LASTHIST = $(shell grep '^20[0-3][0-9]' $(HISTFILE) | head -1) +LASTCOMMENT = $(subst |,\n,$(shell sed -n '/$(LASTHIST)/,/^ *$$/p' $(HISTFILE) | tr '\n' '|')) +LASTTAG = $(shell git describe --tags --abbrev=0) +BRANCH = $(shell git rev-parse --abbrev-ref HEAD) +GIT_REV = $(shell git describe --always 2>/dev/null) + +### The directory environment: + +# Use package data if installed...otherwise assume we're under the VDR source directory: +PKGCFG = $(if $(VDRDIR),$(shell pkg-config --variable=$(1) $(VDRDIR)/vdr.pc),$(shell pkg-config --variable=$(1) vdr || pkg-config --variable=$(1) ../../../vdr.pc)) + +LIBDIR = $(call PKGCFG,libdir) +LOCDIR = $(call PKGCFG,locdir) +PLGCFG = $(call PKGCFG,plgcfg) +CONFDEST = $(call PKGCFG,configdir)/plugins/$(PLUGIN) + +# autodetect related plugins + +ifeq (exists, $(shell test -e ../graphtftng && echo exists)) + WITH_GTFT = 1 +endif +ifeq (exists, $(shell test -e ../vdr-plugin-graphtftng && echo exists)) + WITH_GTFT = 1 +endif +ifeq (exists, $(shell test -e ../pin && echo exists)) + WITH_PIN = 1 +endif +ifeq (exists, $(shell test -e ../vdr-plugin-pin && echo exists)) + WITH_PIN = 1 +endif + +# + +TMPDIR ?= /tmp + +### The compiler options: + +export CFLAGS += $(call PKGCFG,cflags) +export CXXFLAGS += $(call PKGCFG,cxxflags) + +### The version number of VDR's plugin API: + +APIVERSION = $(call PKGCFG,apiversion) + +### Allow user defined options to overwrite defaults: + +-include $(PLGCFG) + +OBJS = $(PLUGIN).o \ + service.o update.o plgconfig.o parameters.o \ + timer.o recording.o recinfofile.o \ + status.o ttools.o svdrpclient.o \ + menu.o menusched.o menutimers.o menudone.o menusearchtimer.o + +LIBS = $(HLIB) +LIBS += -lrt -larchive -lcrypto -luuid +LIBS += $(shell mysql_config --libs_r) $(shell python-config --libs) $(shell pkg-config --cflags --libs jansson) + +EPG2VDR_DATA_DIR = "/var/cache/vdr" + +ifdef WITH_GTFT + DEFINES += -DWITH_GTFT +endif + +ifdef WITH_PIN + DEFINES += -DWITH_PIN +endif + +ifdef EPG2VDR_DATA_DIR + DEFINES += -DEPG2VDR_DATA_DIR='$(EPG2VDR_DATA_DIR)' +endif + +### The name of the distribution archive: + +ARCHIVE = $(PLUGIN)-$(VERSION) +PACKAGE = vdr-$(ARCHIVE) + +### The name of the shared object file: + +SOFILE = libvdr-$(PLUGIN).so + +### Includes and Defines (add further entries here): + +INCLUDES += $(shell mysql_config --include) + +DEFINES += -DEPG2VDR -DLOG_PREFIX='"$(PLUGIN): "' $(USES) + +ifdef GIT_REV + DEFINES += -DGIT_REV='"$(GIT_REV)"' +endif + +### The main target: + +all: $(SOFILE) i18n + +hlib: + (cd lib && $(MAKE) -s lib) + +### Implicit rules: + +%.o: %.c + $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) -o $@ $< + +### Dependencies: + +MAKEDEP = $(CXX) -MM -MG +DEPFILE = .dependencies +$(DEPFILE): Makefile + $(MAKEDEP) $(CXXFLAGS) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@ + +-include $(DEPFILE) + +### Internationalization (I18N): + +PODIR = po +I18Npo = $(wildcard $(PODIR)/*.po) +I18Nmo = $(addsuffix .mo, $(foreach file, $(I18Npo), $(basename $(file)))) +I18Nmsgs = $(addprefix $(DESTDIR)$(LOCDIR)/, $(addsuffix /LC_MESSAGES/vdr-$(PLUGIN).mo, $(notdir $(foreach file, $(I18Npo), $(basename $(file)))))) +I18Npot = $(PODIR)/$(PLUGIN).pot + +%.mo: %.po + msgfmt -c -o $@ $< + +$(I18Npot): $(wildcard *.c) + xgettext -C -cTRANSLATORS --no-wrap --no-location -k -ktr -ktrNOOP --package-name=vdr-$(PLUGIN) --package-version=$(VERSION) --msgid-bugs-address='<vdr@jwendel.de>' -o $@ `ls $^` + +%.po: $(I18Npot) + msgmerge -U --no-wrap --no-location --backup=none -q -N $@ $< + @touch $@ + +$(I18Nmsgs): $(DESTDIR)$(LOCDIR)/%/LC_MESSAGES/vdr-$(PLUGIN).mo: $(PODIR)/%.mo + install -D -m644 $< $@ + +.PHONY: i18n +i18n: $(I18Nmo) $(I18Npot) + +install-i18n: $(I18Nmsgs) + +### Targets: + +$(SOFILE): hlib $(OBJS) + $(CXX) $(CXXFLAGS) $(LDFLAGS) -shared $(OBJS) $(LIBS) -o $@ + +install-lib: $(SOFILE) + install -D -m644 $^ $(DESTDIR)$(LIBDIR)/$^.$(APIVERSION) + +install: install-lib install-i18n install-config + +install-config: + if ! test -d $(DESTDIR)$(CONFDEST); then \ + mkdir -p $(DESTDIR)$(CONFDEST); \ + chmod a+rx $(DESTDIR)$(CONFDEST); \ + fi + install --mode=644 -D ./configs/epg.dat $(DESTDIR)$(CONFDEST) +ifdef VDR_USER + if test -n $(VDR_USER); then \ + chown $(VDR_USER) $(DESTDIR)$(CONFDEST); \ + chown $(VDR_USER) $(DESTDIR)$(CONFDEST)/epg.dat; \ + fi +endif + +dist: clean + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a * $(TMPDIR)/$(ARCHIVE) + @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(PACKAGE).tgz + +clean: + @-rm -f $(OBJS) $(DEPFILE) *.so *.core* *~ ./configs/*~ + @-rm -f $(PODIR)/*.mo $(PODIR)/*.pot $(PODIR)/*~ + @-rm -f $(PACKAGE).tgz + (cd lib && $(MAKE) clean) + +cppchk: + cppcheck --language=c++ --template="{file}:{line}:{severity}:{message}" --quiet --force *.c *.h + +# ------------------------------------------------------ +# Git / Versioning / Tagging +# ------------------------------------------------------ + +vcheck: + git fetch + if test "$(LASTTAG)" = "$(VERSION)"; then \ + echo "Warning: tag/version '$(VERSION)' already exists, update HISTORY first. Aborting!"; \ + exit 1; \ + fi + +push: vcheck + echo "tagging git with $(VERSION)" + git tag $(VERSION) + git push --tags + git push + +commit: vcheck + git commit -m "$(LASTCOMMENT)" -a + +git: commit push + +showv: + @echo "Git ($(BRANCH)):\\n Version: $(LASTTAG) (tag)" + @echo "Local:" + @echo " Version: $(VERSION)" + @echo " Change:" + @echo -n " $(LASTCOMMENT)" + +update: + git pull + @make clean install + restart vdr @@ -0,0 +1,146 @@ +----------------------------------------------------------------------------------- +- epg2vdr +- +- This is a "plugin" for the Video Disk Recorder (VDR). +- +- Written by: C++/SQL - Jörg Wendel (vdr at jwendel dot de) +- SQL/Procedures - Christian Kaiser +- Documetation - Ingo Prochaska +- +- Homepage: http://projects.vdr-developer.org/projects/plg-epg2vdr +- Latest version at: http://projects.vdr-developer.org/git/vdr-plugin-epg2vdr.git +- +- This software is released under the GPL, version 2 (see COPYING). +- Additionally, compiling, linking, and/or using the OpenSSL toolkit in +- conjunction with this software is allowed. +----------------------------------------------------------------------------------- + + +Requirements: +------------- + + - VDR 1.7.27+ (since it use the EPG handler interface) + - libmysql >= 5.07 + - libarchive + - python libpython libpython-dev python-dev + - libjansson4 libjansson-dev + - uuid-dev + +Ubuntu (16.04): + - libarchive13, libarchive-dev + - libmysqlclient-dev libmysqlclient18 + - libjansson4 libjansson-dev + - python libpython libpython-dev python-dev + - uuid-dev + + +Description: +------------ + +This plugin is used to retrieve EPG data into the VDR. The EPG data +was loaded from a mysql database. + +Setup Menu: +----------- + + Log level + Logging level 0-4 { Errors, Infos, ..., Debug, ... } + + Update DVB EPG Database + Master/Slave Mode (one client should be master, e.g. 24/7 Server, all others slave) + - auto: Normally first VDR will be master - the master transfers the DVB events to the mySQL Server + - yes: Master mode on + - no: Master mode off + Make sure that only one VDR will be master or you run into DB performance issues. Makes just sense, if you have different channels on your clients. + + Show In Main Menu + Shows entry in main menu to perform update manually + + MySQL Host + IP of mySQL Server + + Port + Port of your mySQL Server + + Database Name + Database name + + User + Database user + + Password + Database password + + Blacklist not configured Channels + Blacklist (like noepg-plug) all channels which not listst in the channelmap table + + LoadImages + Load images from MySQL-Server to local file system + + Prohibit Shutdown On Busy 'epgd' + Don't shutdown VDR while epgd is busy + + Schedule Boot For Update + Schedule automatic boot for EPG update + + +Get the source from git: +------------------------ + +get the source - probably done, when you reading this locally: + git clone http://projects.vdr-developer.org/git/vdr-plugin-epg2vdr.git + +update the source: + cd /to/your/source/epg2vdr + git pull + +throwing away local changes and update to latest source: + cd /to/your/source/epg2vdr + git checkout -f master + git reset --hard + git pull + +setup.conf (put the IP of your mySQL server): +--------------------------------------------- + + epg2vdr.UpdateTime = 2 + epg2vdr.ShowInMainMenu = 1 + epg2vdr.Blacklist = 1 + epg2vdr.LogLevel = 1 + epg2vdr.DbHost = 192.168.xxx.xxx + epg2vdr.DbName = epg2vdr + epg2vdr.DbUser = epg2vdr + epg2vdr.DbPass = epg + + +SVDRP Commands: +--------------- + + The plugin provides SVDRP commands to control the plugin via command line or + shell scripts. + + RELOAD - Drop the whiole EPG and reload all events from the database + UPDATE - Trigger a update to load all new events from database + + +Installation: +------------- + +- Patch the VDR. Patches found in patches/ since vdr 1.7.27. + For Versions after 1.7.31 you need only the epghandler-segment-transfer.patch + Sinve VDR 2.1.1 no patch is needed! + +- Unpack the package into "PLUGINS/src" directory. +- Call "make plugins" in the VDR root directory. +- Start vdr with plugin epg2vdr (-Pepg2vdr) the database must running already + +Create database and user: +-------------------------- + +already done when the epgd was installed! + +HINTS: +------ +- Don't load the epgtableid0 (epgtableid0 disables VDRs new EPG handler interface) +- Don't load the noepg plugin +(best you don't load any other epg manipulating plugin or patch if you not shure how they work together ;)) @@ -0,0 +1,7 @@ + +- Fehler beim Anlegen eines Timer aus Suchergebniss, Dialog zeigt falschen Sender und File an +- beim Timer löschen und laufender Aufnahme kommt dieser Hinweis nicht + +- anlegen eines Suchtimer über das event +- timer mit action 'C' auch ohne vdruuid übernehmen +- db Verbindungssaten ohne Neustart übernehmen
\ No newline at end of file diff --git a/configs/epg.dat b/configs/epg.dat new file mode 100644 index 0000000..73f7abb --- /dev/null +++ b/configs/epg.dat @@ -0,0 +1,979 @@ +// -------------------------------------------------------------------------- +// +// Table Dictionary for EPG Daemon and related Plugins +// +// -------------------------------------------------------------------------- +// See the README file for copyright information and how to reach the author +// -------------------------------------------------------------------------- + +// ---------------------------------------------------------------- +// Table Events +// ---------------------------------------------------------------- + +Table events +{ + EVENTID "" eventid UBigInt 0 Primary, + CHANNELID "" channelid Ascii 50 Primary, + + MASTERID "" masterid UInt 0 Autoinc, + USEID "" useid UInt 0 Data, + + SOURCE "" source Ascii 10 Meta, + FILEREF "" fileref Ascii 100 Meta, + INSSP "" inssp Int 10 Meta, + UPDSP "" updsp Int 10 Meta, + UPDFLG "" updflg Ascii 1 Meta, + DELFLG "" delflg Ascii 1 Meta, + + TABLEID "" tableid Int 2 Data, + VERSION "" version Int 3 Data, + TITLE "" title Ascii 200 Data, + COMPTITLE "" comptitle Ascii 200 Data, + SHORTTEXT "" shorttext Ascii 300 Data, + COMPSHORTTEXT "" compshorttext Ascii 300 Data, + LONGDESCRIPTION "" longdescription MText 25000 Data, + COMPLONGDESCRIPTION "" complongdescription MText 25000 Data filter epgd|httpd|epg2vdr, + STARTTIME "" starttime Int 10 Data, + DURATION "" duration Int 5 Data, + PARENTALRATING "" parentalrating Int 2 Data, + VPS "" vps Int 10 Data, + CONTENTS "Genre code like table 28 of ETSI EN 300 468" contents ASCII 100 Data filter epgd|httpd|epg2vdr, + SHORTDESCRIPTION "" shortdescription MText 3000 Data, + ACTOR "" actor MText 5000 Data, + AUDIO "" audio Ascii 50 Data, + CATEGORY "" category Ascii 50 Data, + COUNTRY "" country Ascii 50 Data, + DIRECTOR "" director Text 1000 Data, + COMMENTATOR "" commentator Ascii 200 Data, + FLAGS "" flags Ascii 100 Data, + GENRE "" genre Ascii 100 Data, + MUSIC "" music Ascii 250 Data, + PRODUCER "" producer Text 1000 Data, + SCREENPLAY "" screenplay Ascii 500 Data, + SHORTREVIEW "" shortreview Ascii 500 Data, + TIPP "" tipp Ascii 250 Data, + TOPIC "" topic Ascii 1000 Data, + YEAR "" year Ascii 10 Data, + RATING "" rating Ascii 250 Data, + NUMRATING "" numrating Int 2 Data filter epgd|httpd|epg2vdr, + TXTRATING "" txtrating Ascii 100 Data, + MOVIEID "" movieid Ascii 20 Data, + MODERATOR "" moderator Ascii 250 Data, + OTHER "" other Text 2000 Data, + GUEST "" guest Text 1000 Data, + CAMERA "" camera Text 1000 Data, + EXTEPNUM "" extepnum Int 4 Data, + IMAGECOUNT "" imagecount Int 2 Data, + + EPISODECOMPNAME "" episodecompname Ascii 100 Data filter epgd|httpd|epg2vdr, + EPISODECOMPSHORTNAME "" episodecompshortname Ascii 100 Data filter epgd|httpd|epg2vdr, + EPISODECOMPPARTNAME "" episodecomppartname Ascii 200 Data filter epgd|httpd|epg2vdr, + EPISODELANG "" episodelang Ascii 10 Data filter epgd|httpd|epg2vdr, + + SCRSERIESID "" scrseriesid Int 0 Data, + SCRSERIESEPISODE "" scrseriesepisode Int 0 Data, + SCRMOVIEID "" scrmovieid Int 0 Data, + SCRSP "" scrsp Int 0 Data, +} + +// ---------------------------------------------------------------- +// Indices for Events +// ---------------------------------------------------------------- + +Index events +{ + comptitle "" COMPTITLE, + source "" SOURCE, + filerefsource "" FILEREF SOURCE, + channelid "" CHANNELID, + useid "" USEID, + useidchannelid "" USEID CHANNELID, + updflgupdsp "" UPDFLG UPDSP, + sourcechannelid "" SOURCE CHANNELID, + scrsp "" SCRSP, + sourceupdsp "" SOURCE UPDSP, + scrseriesid "" SCRSERIESID, + channelidstarttime "" CHANNELID STARTTIME, +} + +// ---------------------------------------------------------------- +// Table Components +// ---------------------------------------------------------------- + +Table components +{ + EVENTID "" eventid UBigInt 0 Primary, + CHANNELID "" channelid Ascii 50 Primary, + STREAM "" stream Int 3 Primary, + TYPE "" type Int 3 Primary, + LANG "" lang Ascii 8 Primary, + DESCRIPTION "" description Ascii 100 Primary, + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, +} + +// ---------------------------------------------------------------- +// Table FileRef +// ---------------------------------------------------------------- + +Table fileref +{ + NAME "" name Ascii 100 Primary, + SOURCE "" source Ascii 10 Primary, + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + EXTERNALID "" extid Ascii 10 Data, + FILEREF "" fileref Ascii 100 Data, + TAG "" tag Ascii 100 Data, +} + +// ---------------------------------------------------------------- +// Indices for FileRefs +// ---------------------------------------------------------------- + +Index filerefs +{ + SourceFileref "" SOURCE FILEREF, + Fileref "" FILEREF, +} + +// ---------------------------------------------------------------- +// Table ImageRefs +// ---------------------------------------------------------------- + +Table imagerefs +{ + EVENTID "" eventid UBigInt 0 Primary, + LFN "" lfn Int 0 Primary, + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + SOURCE "" source Ascii 10 Meta, + FILEREF "" fileref Ascii 100 Data, + IMGNAME "" imagename Ascii 100 Data, +} + +// ---------------------------------------------------------------- +// Indices for ImageRefs +// ---------------------------------------------------------------- + +Index imagerefs +{ + lfn "" LFN, + name "" IMGNAME, +} + +// ---------------------------------------------------------------- +// Table Images +// ---------------------------------------------------------------- + +Table images +{ + IMGNAME "" imagename Ascii 100 Primary, + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + IMAGE "" image Mlob 512000 Data, +} + +// ---------------------------------------------------------------- +// Table Episodes +// ---------------------------------------------------------------- + +Table episodes +{ + COMPNAME "" compname Ascii 100 Primary, + COMPPARTNAME "" comppartname Ascii 200 Primary, + LANG "" lang Ascii 10 Primary, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + + LINK "" link Int 0 Data, + SHORTNAME "" shortname Ascii 100 Data, + COMPSHORTNAME "" compshortname Ascii 100 Data, + EPISODENAME "" episodename Ascii 100 Data, + PARTNAME "" partname Ascii 300 Data, + SEASON "" season Int 0 Data, + PART "" part Int 0 Data, + PARTS "" parts Int 0 Data, + NUMBER "" number Int 0 Data, + EXTRACOL1 "" extracol1 Ascii 250 Data, + EXTRACOL2 "" extracol2 Ascii 250 Data, + EXTRACOL3 "" extracol3 Ascii 250 Data, + COMMENT "" comment Ascii 250 Data, +} + +// ---------------------------------------------------------------- +// Indices for Episodes +// ---------------------------------------------------------------- + +Index episodes +{ + updsp "" UPDSP, +} + +// ---------------------------------------------------------------- +// Table ChannelMap +// ---------------------------------------------------------------- + +Table channelmap +{ + EXTERNALID "" extid Ascii 10 Primary, + CHANNELID "" channelid Ascii 50 Primary, + SOURCE "" source Ascii 20 Primary, + + ORDER "" ord Int 0 Data, + VISIBLE "" visible Int 0 Data, + CHANNELNAME "" channelname Ascii 100 Data, + VPS "" vps Int 0 Data, + FORMAT "" format Ascii 50 Data, + UNKNOWNATVDR "" unknownatvdr UInt 1 Data, + MERGE "" merge Int 0 Data, + MERGESP "" mergesp Int 0 Data, + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + UPDFLG "" updflg Ascii 1 Meta, +} + +// ---------------------------------------------------------------- +// Indices for ChannelMap +// ---------------------------------------------------------------- + +Index channelmap +{ + sourceExtid "" SOURCE EXTERNALID, + source "" SOURCE, + updflg "" UPDFLG, + sourcechannelid "" SOURCE CHANNELID, + mergesp "" MERGESP, + channelid "" CHANNELID, +} + +// ---------------------------------------------------------------- +// Table Vdrs +// ---------------------------------------------------------------- + +Table vdrs +{ + UUID "" uuid Ascii 40 Primary, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + + NAME "" name Ascii 100 Data, + VERSION "" version Ascii 100 Data, + DBAPI "" dbapi UInt 0 Data, + LASTUPDATE "" lastupd Int 0 Data, + NEXTUPDATE "" nextupd Int 0 Data, + STATE "" state Ascii 20 Data, + MASTER "" master Ascii 1 Data, + IP "" ip Ascii 20 Data, + MAC "" mac Ascii 18 Data, + PID "" pid UInt 0 Data filter epgd|httpd|epg2vdr, + SVDRP "" svdrp UInt 0 Data filter epgd|httpd|epg2vdr, + TUNERCOUNT "" tunercount UInt 0 Data filter epgd|httpd|epg2vdr, + + SHAREINWEB "" shareinweb UInt 1 Data, + USECOMMONRECFOLDER "" usecommonrecfolder UInt 1 Data, + + VIDEODIR "" videodir Ascii 300 Data, + VIDEOTOTAL "" videototal UInt 0 Data, + VIDEOFREE "" videofree UInt 0 Data, +} + +// ---------------------------------------------------------------- +// Indices for Vdrs +// ---------------------------------------------------------------- + +Index vdrs +{ + state "" STATE, +} + +// ---------------------------------------------------------------- +// Table Users +// ---------------------------------------------------------------- + +Table users +{ + USER "" user Ascii 40 Primary, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + + PASSWD "" passwd Ascii 100 Data, + ACTIVE "" active Int 1 Data, + RIGHTS "" rights UInt 0 Data, +} + +// ---------------------------------------------------------------- +// Table Parameters +// ---------------------------------------------------------------- + +Table parameters +{ + OWNER "" owner Ascii 40 Primary, + NAME "" name Ascii 40 Primary, + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + VALUE "" value Ascii 500 Data, +} + +// ---------------------------------------------------------------- +// Table Analyse +// ---------------------------------------------------------------- + +Table analyse +{ + CHANNELID "" channelid Ascii 50 Primary, + VDRMASTERID "" vdr_masterid UInt 0 Data, + VDREVENTID "" vdr_eventid UBigInt 0 Primary, + VDRSTARTTIME "" vdr_starttime Int 10 Data, + VDRDURATION "" vdr_duration Int 5 Data, + VDRTITLE "" vdr_title Ascii 200 Data, + VDRSHORTTEXT "" vdr_shorttext Ascii 300 Data, + EXTMASTERID "" ext_masterid UInt 0 Data, + EXTEVENTID "" ext_eventid UBigInt 0 Data, + EXTSTARTTIME "" ext_starttime Int 10 Data, + EXTDURATION "" ext_duration Int 5 Data, + EXTTITLE "" ext_title Ascii 200 Data, + EXTSHORTTEXT "" ext_shorttext Ascii 300 Data, + EXTEPISODE "" ext_episode Ascii 1 Data, + EXTMERGE "" ext_merge Int 11 Data, + EXIIMAGES "" ext_images Ascii 1 Data, + LVMIN "" lvmin Int 3 Data, + RANK "" rank Int 5 Data, +} + +// ---------------------------------------------------------------- +// Indices for Analyse +// ---------------------------------------------------------------- + +Index analyse +{ + vdr_masterid "" VDRMASTERID, +} + +// ---------------------------------------------------------------- +// Table Snapshot +// ---------------------------------------------------------------- + +Table snapshot +{ + CHANNELID "" channelid Ascii 50 Data, + SOURCE "" source Ascii 10 Data, + VDRMASTERID "" masterid UInt 0 Data, + EVENTID "" eventid UBigInt 0 Data, + USEID "" useid UInt 0 Data, + STARTTIME "" starttime Int 10 Data, + DURATION "" duration Int 5 Data, + TITLE "" title Ascii 200 Data, + COMPTITLE "" comptitle Ascii 200 Data, + SHORTTEXT "" shorttext Ascii 300 Data, + COMPSHORTTEXT "" compshorttext Ascii 300 Data, + UPDSP "" updsp Int 10 Data, + EPISODE "" episode Ascii 1 Data, + MERGE "" merge Int 0 Data, + IMAGES "" images Ascii 1 Data, +} + +// ---------------------------------------------------------------- +// Indices for Snapshot +// ---------------------------------------------------------------- + +Index snapshot +{ + channelid "" CHANNELID, + starttimeSource "" STARTTIME SOURCE, +} + +// ---------------------------------------------------------------- +// Table UseEvents +// ---------------------------------------------------------------- + +Table useevents +{ + CNTSOURCE "" cnt_source Ascii 10 Primary, + CHANNELID "" cnt_channelid Ascii 50 Primary, + CNTEVENTID "" cnt_eventid UBigInt 0 Primary|Meta, + + MASTERID "" cnt_masterid UInt 0 Data|Meta, + USEID "" cnt_useid UInt 0 Data, + + SUBSOURCE "" sub_source Ascii 10 Data|Meta, + SUBEVENTID "" sub_eventid UBigInt 0 Data|Meta, + UPDSP "" all_updsp Int 0 Data, + UPDFLG "" cnt_updflg Ascii 1 Data|Meta, + DELFLG "" cnt_delflg Ascii 1 Data, + FILEREF "" cnt_fileref Ascii 100 Data, + TABLEID "" cnt_tableid Int 2 Data, + VERSION "" cnt_version Int 3 Data, + TITLE "" sub_title Ascii 200 Data, + SHORTTEXT "" sub_shorttext Ascii 300 Data, + COMPTITLE "" sub_comptitle Ascii 200 Data, + COMPSHORTTEXT "" sub_compshorttext Ascii 300 Data, + GENRE "" sub_genre Ascii 100 Data, + COUNTRY "" sub_country Ascii 50 Data, + YEAR "" sub_year Ascii 10 Data, + STARTTIME "" cnt_starttime Int 10 Data, + DURATION "" cnt_duration Int 5 Data, + PARENTALRATING "" cnt_parentalrating Int 2 Data, + VPS "" cnt_vps Int 10 Data, + CONTENTS "Genre code like table 28 of ETSI EN 300 468" cnt_contents ASCII 100 Data, + CATEGORY "" sub_category Ascii 50 Data, + SHORTDESCRIPTION "" sub_shortdescription MText 3000 Data, + SHORTREVIEW "" sub_shortreview Ascii 500 Data, + TIPP "" sub_tipp Ascii 250 Data, + RATING "" sub_rating Ascii 250 Data, + NUMRATING "" sub_numrating Int 2 Data, + TXTRATING "" sub_txtrating Ascii 100 Data, + TOPIC "" sub_topic Ascii 1000 Data, + LONGDESCRIPTION "" sub_longdescription MText 25000 Data, + COMPLONGDESCRIPTION "" sub_complongdescription MText 25000 Data, + CNTLONGDESCRIPTION "" cnt_longdescription MText 25000 Data, + MODERATOR "" sub_moderator Ascii 250 Data, + GUEST "" sub_guest Text 1000 Data, + ACTOR "" sub_actor MText 5000 Data, + PRODUCER "" sub_producer Text 1000 Data, + OTHER "" sub_other Text 2000 Data, + DIRECTOR "" sub_director Text 1000 Data, + COMMENTATOR "" sub_commentator Ascii 200 Data, + SCREENPLAY "" sub_screenplay Ascii 500 Data, + CAMERA "" sub_camera Text 1000 Data, + MUSIC "" sub_music Ascii 250 Data, + AUDIO "" sub_audio Ascii 50 Data, + FLAGS "" sub_flags Ascii 100 Data, + IMAGECOUNT "" sub_imagecount Int 2 Data, + + SCRSERIESID "" sub_scrseriesid Int 0 Data, + SCRSERIESEPISODE "" sub_scrseriesepisode Int 0 Data, + SCRMOVIEID "" sub_scrmovieid Int 0 Data, + SCRSP "" sub_scrsp Int 0 Data|Meta, + + EPISODECOMPNAME "" sub_episodecompname Ascii 100 Data, + EPISODECOMPSHORTNAME "" sub_episodecompshortname Ascii 100 Data, + EPISODECOMPPARTNAME "" sub_episodecomppartname Ascii 200 Data, + + EPISODENAME "" epi_episodename Ascii 100 Data, + EPISODESHORTNAME "" epi_shortname Ascii 100 Data, + EPISODEPARTNAME "" epi_partname Ascii 300 Data, + EPISODELANG "" epi_lang Ascii 10 Data, + EPISODEEXTRACOL1 "" epi_extracol1 Ascii 250 Data, + EPISODEEXTRACOL2 "" epi_extracol2 Ascii 250 Data, + EPISODEEXTRACOL3 "" epi_extracol3 Ascii 250 Data, + EPISODESEASON "" epi_season Int 0 Data, + EPISODEPART "" epi_part Int 0 Data, + EPISODEPARTS "" epi_parts Int 0 Data, + EPISODENUMBER "" epi_number Int 0 Data, +} + +// ---------------------------------------------------------------- +// Indices for UseEvents +// ---------------------------------------------------------------- + +Index useevents +{ + channelidstarttime "" CHANNELID STARTTIME, + useid "" USEID, + channelidupdflgupdsp "" CHANNELID UPDFLG UPDSP, + channelid "" CHANNELID, + updflgstarttimeduration "" UPDSP STARTTIME DURATION, +} + +// ---------------------------------------------------------------- +// Table Recording List +// ---------------------------------------------------------------- + +Table recordinglist +{ + MD5PATH "" md5path Ascii 40 Primary, + STARTTIME "" starttime UInt 0 Primary, + OWNER "uuid of vdr" owner Ascii 40 Primary, + + INSSP "" inssp Int 10 Meta, + UPDSP "" updsp Int 10 Meta, + LASTIFOUPD "" lastifoupd Int 10 Meta, + + VDRUUID "" vdruuid Ascii 40 Data, + PATH "" path Ascii 1000 Data, + NAME "" name Ascii 1000 Data, + FOLDER "" folder Ascii 1000 Data, + TITLE "" title Ascii 200 Data, + SHORTTEXT "" shorttext Ascii 300 Data, + LONGDESCRIPTION "" longdescription MText 25000 Data, + DURATION "" duration UInt 0 Data, + FSK "" fsk UInt 1 Data, + + EVENTID "useid" eventid UInt 0 Data, + CHANNELID "" channelid Ascii 50 Data, + CHANNELNAME "just a copy" channelname Ascii 100 Data, + + STATE "" state Ascii 1 Data, + INUSE "" inuse UInt 1 Data, + JOB "" job Ascii 1 Data, + + // enriched by 'external' data of events + + ACTOR "" actor MText 5000 Data, + AUDIO "" audio Ascii 50 Data, + CATEGORY "" category Ascii 50 Data, + COUNTRY "" country Ascii 50 Data, + DIRECTOR "" director Text 1000 Data, + FLAGS "" flags Ascii 100 Data, + GENRE "" genre Ascii 100 Data, + MUSIC "" music Ascii 250 Data, + PRODUCER "" producer Text 1000 Data, + SCREENPLAY "" screenplay Ascii 500 Data, + SHORTREVIEW "" shortreview Ascii 500 Data, + TIPP "" tipp Ascii 250 Data, + TOPIC "" topic Ascii 1000 Data, + YEAR "" year Ascii 10 Data, + RATING "" rating Ascii 250 Data, + NUMRATING "" numrating Int 2 Data, + TXTRATING "" txtrating Ascii 100 Data, + MODERATOR "" moderator Ascii 250 Data, + OTHER "" other Text 2000 Data, + GUEST "" guest Text 1000 Data, + CAMERA "" camera Text 1000 Data, + + // episode reference + + EPISODECOMPNAME "" episodecompname Ascii 100 Data filter epgd|httpd|epg2vdr, + EPISODECOMPSHORTNAME "" episodecompshortname Ascii 100 Data filter epgd|httpd|epg2vdr, + EPISODECOMPPARTNAME "" episodecomppartname Ascii 200 Data filter epgd|httpd|epg2vdr, + EPISODELANG "" episodelang Ascii 10 Data filter epgd|httpd|epg2vdr, + + // scraper fields + // IDs found while scraping (reference to scraper tables), managed by epgd + + SCRSERIESID "" scrseriesid UInt 0 Data, + SCRSERIESEPISODE "" scrseriesepisode UInt 0 Data, + SCRMOVIEID "" scrmovieid UInt 0 Data, + + // this fields are written by the scraper plugin, + // the id fields hold the user hint for scraping + + SCRINFOMOVIEID "" scrinfomovieid UInt 0 Data, + SCRINFOSERIESID "" scrinfoseriesid UInt 0 Data, + SCRINFOEPISODEID "" scrinfoepisodeid UInt 0 Data, + + SCRNEW "" scrnew UInt 0 Data, + SCRSP "" scrsp Int 0 Data, +} + +// ---------------------------------------------------------------- +// Table RecordingDirs +// ---------------------------------------------------------------- + +Table recordingdirs +{ + VDRUUID "" vdruuid Ascii 40 Primary, + DIRECTORY "" directory Ascii 255 Primary, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, +} + +// ---------------------------------------------------------------- +// Table Timers +// ---------------------------------------------------------------- + +Table timers +{ + ID "" id UInt 0 Primary|Autoinc, + VDRUUID "" vdruuid Ascii 40 Primary, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + + EVENTID "useid" eventid UInt 0 Data, + CHANNELID "" channelid Ascii 50 Data, + _STARTTIME "pre filled start timer for trigger" _starttime Int 10 Data, + + SOURCE "like osd, webif, epgd" source Ascii 40 Data, + TYPE "'R'ecord, 'V'iew (umschalt)" type Ascii 1 Data, + STATE "'D'eleted, 'R'unning, 'F'inished" state Ascii 1 Data default u, + INFO "error reason if state is failed" info Ascii 255 Data, + ACTION "" action Ascii 1 Data default a, + TCCMAILCNT "" tccmailcnt UInt 0 Data, + RETRYS "" retrys UInt 0 Data, + + NAMINGMODE "" namingmode Int 0 Data, + TEMPLATE "" template Ascii 100 Data, + ACTIVE "" active UInt 0 Data, + DAY "" day Int 10 Data, + WEEKDAYS "" weekdays Int 10 Data, + STARTTIME "" starttime Int 10 Data, + ENDTIME "" endtime Int 10 Data, + FILE "" file Ascii 512 Data, + DIRECTORY "" directory Ascii 512 Data, + + PRIORITY "" priority Int 0 Data, + LIFETIME "" lifetime Int 0 Data, + VPS "" vps Int 0 Data, + CHILDLOCK "" childlock Int 0 Data, + AUX "" aux Ascii 1000 Data, + + AUTOTIMERNAME "Bezeichung des Suchtimers" autotimername Ascii 100 Data, + AUTOTIMERID "id of autotimer" autotimerid UInt 0 Data, + AUTOTIMERINSSP "" autotimerinssp Int 0 Data, + + // just to update the done state by the plugin !! + + DONEID "id of done entry" doneid UInt 0 Data, + EXPRESSION "" expression Ascii 200 Data, +} + +// ---------------------------------------------------------------- +// Indices for Timers +// ---------------------------------------------------------------- + +Index timers +{ + eventidchannelidvdruuid "" EVENTID CHANNELID VDRUUID, + vdruuidstate "" VDRUUID STATE +} + +// ---------------------------------------------------------------- +// Table +// ---------------------------------------------------------------- + +Table searchtimers +{ + ID "" id UInt 0 Primary|Autoinc, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + + CHANNELIDS "comma separated list of channleids or empty" channelids Ascii 500 Data, + CHEXCLUDE "" chexclude UInt 0 Data, + CHFORMAT "HD,SD" chformat Ascii 50 Data, + + NAME "Bezeichung des Suchtimers" name Ascii 100 Data, + EXPRESSION "" expression Ascii 200 Data, + EXPRESSION1 "" expression1 Ascii 200 Data, + SEARCHMODE "1 exact, 2 regexp, 3 like, 4 enthalten, .." searchmode UInt 0 Data, + SEARCHFIELDS "Bitmaske: 1 title, 2 shorttext, 4 - desc, .." searchfields UInt 0 Data, + SEARCHFIELDS1 "Bitmaske: 0 off, 1 title, 2 shorttext, 4 - desc, .." searchfields1 UInt 0 Data, + + CASESENSITIV "0,1" casesensitiv UInt 0 Data, + + REPEATFIELDS "Bitmaske: 1 title, 2 shorttext, 4 - desc, .." repeatfields UInt 0 Data, + + // optional EPG Detail options + + EPISODENAME "" episodename Ascii 100 Data, + SEASON "e.g. '1-7'" season Ascii 10 Data, + SEASONPART "e.g. '20-'" seasonpart Ascii 10 Data, + CATEGORY "e.g. 'Spielfilm','Serie'" category Ascii 150 Data, + GENRE "e.g. 'Krimi','Action'" genre Ascii 150 Data, + YEAR "e.g. '2010-2015'" year Ascii 10 Data, + TIPP "" tipp Ascii 250 Data, + NOEPGMATCH "" noepgmatch Int 0 Data, + + // steering + + TYPE "'R'ecord, 'V'iew, 'S'earch" type Ascii 1 Data, + STATE "D - Deleted" state Ascii 1 Data, + NAMINGMODE "" namingmode Int 0 Data, + TEMPLATE "" template Ascii 100 Data, + + ACTIVE "0,1" active UInt 0 Data, + SOURCE "webif ,osd, ..." source Ascii 40 Data, + HITS "stastic counter, just for info" hits UInt 0 Data, + MODSP "last user modification time" modsp Int 0 Data, + LASTRUN "last execution of search timer" lastrun Int 0 Data, + VDRUUID "empty or uuid of vdr" vdruuid Ascii 40 Data, + + // timer details + + WEEKDAYS "bitmask like timers, 0 foy all" weekdays Int 0 Data, + NEXTDAYS "" nextdays UInt 0 Data, + STARTTIME "null for unlimited search" starttime Int 0 Data, + ENDTIME "null for unlimited search" endtime Int 0 Data, + DIRECTORY "" directory Ascii 512 Data, + PRIORITY "" priority Int 0 Data, + LIFETIME "" lifetime Int 0 Data, + VPS "" vps Int 0 Data, + CHILDLOCK "" childlock Int 0 Data, +} + +// ---------------------------------------------------------------- +// Table TimersDone +// ---------------------------------------------------------------- + +Table timersdone +{ + ID "" id UInt 0 Primary|Autoinc, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + + SOURCE "" source Ascii 40 Data, + STATE "Q requested, C created, R recorded, F Failed" state Ascii 1 Data, + + TIMERID "not filled completely in all cases yet" timerid UInt 0 Data, + AUTOTIMERID "" autotimerid UInt 0 Data, + AUTOTIMERNAME "Bezeichung des Suchtimers" autotimername Ascii 100 Data, + + // to compare + + TITLE "" title Ascii 200 Data, + COMPTITLE "" comptitle Ascii 200 Data, + SHORTTEXT "" shorttext Ascii 300 Data, + COMPSHORTTEXT "" compshorttext Ascii 300 Data, + LONGDESCRIPTION "" longdescription MText 25000 Data, + COMPLONGDESCRIPTION "" complongdescription MText 25000 Data, + + EPISODECOMPNAME "" episodecompname Ascii 100 Data filter epgd|httpd|epg2vdr, + EPISODECOMPSHORTNAME "" episodecompshortname Ascii 100 Data filter epgd|httpd|epg2vdr, + EPISODECOMPPARTNAME "" episodecomppartname Ascii 200 Data filter epgd|httpd|epg2vdr, + EPISODELANG "" episodelang Ascii 10 Data filter epgd|httpd|epg2vdr, + EPISODESEASON "" episodeseason Int 0 Data, + EPISODEPART "" episodepart Int 0 Data, + + // just for info + + CHANNELID "" channelid Ascii 50 Data, + CHANNELNAME "" channelname Ascii 100 Data; + EXPRESSION "expression of autotimer" expression Ascii 200 Data, + STARTTIME "" starttime Int 10 Data, + DURATION "" duration Int 5 Data, + AUX "" aux Ascii 512 Data, +} + +// ---------------------------------------------------------------- +// Indices for TimersDone +// ---------------------------------------------------------------- + +Index TimersDone +{ + checkdoubles "" COMPTITLE, COMPSHORTTEXT +} + +// ---------------------------------------------------------------- +// Table Messages / Notifications +// ---------------------------------------------------------------- + +Table messages +{ + ID "" id UInt 0 Primary|Autoinc, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + + TYPE "Warning, Info, Error, Fatal" type Ascii 1 Data, + TITLE "" title Ascii 200 Data, + STATE "Read, New, Deleted" state Ascii 1 Data, + TEXT "" text MText 20000 Data, +} + +// ---------------------------------------------------------------- +// SCRAPER stuff +// ---------------------------------------------------------------- +// ---------------------------------------------------------------- +// Table Series +// ---------------------------------------------------------------- + +Table series +{ + SERIESID "" series_id UInt 0 Primary, + SERIESNAME "" series_name Ascii 200 Data, + SERIESLASTSCRAPED "" series_last_scraped UInt 0 Data, + SERIESLASTUPDATED "" series_last_updated UInt 0 Data, + SERIESOVERVIEW "" series_overview Text 10000 Data, + SERIESFIRSTAIRED "" series_firstaired Ascii 50 Data, + SERIESNETWORK "" series_network Ascii 100 Data, + SERIESIMDBID "" series_imdb_id Ascii 20 Data, + SERIESGENRE "" series_genre Ascii 100 Data, + SERIESRATING "" series_rating Float 31 Data, + SERIESSTATUS "" series_status Ascii 50 Data, +} + +// ---------------------------------------------------------------- +// Indices for Series +// ---------------------------------------------------------------- + +Index series +{ + seriesname "" SERIESNAME, +} + +// ---------------------------------------------------------------- +// Table SeriesEpisode +// ---------------------------------------------------------------- + +Table series_episode +{ + EPISODEID "" episode_id UInt 0 Primary, + EPISODENUMBER "" episode_number UInt 0 Data, + SEASONNUMBER "" season_number UInt 0 Data, + EPISODENAME "" episode_name Ascii 300 Data, + EPISODEOVERVIEW "" episode_overview Text 10000 Data, + EPISODEFIRSTAIRED "" episode_firstaired Ascii 20 Data, + EPISODEGUESTSTARS "" episode_gueststars Ascii 1000 Data, + EPISODERATING "" episode_rating Float 31 Data, + EPISODELASTUPDATED "" episode_last_updated UInt 0 Data, + SERIESID "" series_id UInt 0 Data, +} + +// ---------------------------------------------------------------- +// Indices for SeriesEpisode +// ---------------------------------------------------------------- + +Index series_episode +{ + series_id "" SERIESID, +} + +// ---------------------------------------------------------------- +// Table SeriesMedia +// ---------------------------------------------------------------- + +Table series_media +{ + SERIESID "" series_id UInt 0 Primary, + SEASONNUMBER "" season_number UInt 0 Primary, + EPISODEID "" episode_id UInt 0 Primary, + ACTORID "" actor_id UInt 0 Primary, + MEDIATYPE "" media_type UInt 0 Primary, + + INSSP "" inssp Int 0 Meta filter epgd|httpd|epg2vdr, + UPDSP "" updsp Int 0 Meta filter epgd|httpd|epg2vdr, + + MEDIAURL "" media_url Ascii 100 Data, + MEDIAWIDTH "" media_width UInt 0 Data, + MEDIAHEIGHT "" media_height UInt 0 Data, + MEDIARATING "" media_rating Float 31 Data, + MEDIACONTENT "" media_content Mlob 1000000 Data, +} + +// ---------------------------------------------------------------- +// Indices for SeriesMedia +// ---------------------------------------------------------------- + +Index series_media +{ + series_id "" SERIESID, + season_number "" SEASONNUMBER, + episode_id "" EPISODEID, + actor_id "" ACTORID, +} + +// ---------------------------------------------------------------- +// Table SeriesActor +// ---------------------------------------------------------------- + +Table series_actor +{ + ACTORID "" actor_id UInt 0 Primary, + ACTORNAME "" actor_name Ascii 100 Data, + ACTORROLE "" actor_role Ascii 500 Data, + SORTORDER "" actor_sortorder UInt 0 Data, +} + +// ---------------------------------------------------------------- +// Indices for SeriesActor +// ---------------------------------------------------------------- + +Index series_actor +{ +} + +// ---------------------------------------------------------------- +// Table Movie +// ---------------------------------------------------------------- + +Table movie +{ + MOVIEID "" movie_id UInt 0 Primary, + TITLE "" movie_title Ascii 300 Data, + ORIGINALTITLE "" movie_original_title Ascii 300 Data, + TAGLINE "" movie_tagline Ascii 1000 Data, + OVERVIEW "" movie_overview Text 5000 Data, + ISADULT "" movie_adult UInt 0 Data, + COLLECTIONID "" movie_collection_id UInt 0 Data, + COLLECTIONNAME "" movie_collection_name Ascii 300 Data, + BUDGET "" movie_budget UInt 0 Data, + REVENUE "" movie_revenue UInt 0 Data, + GENRES "" movie_genres Ascii 500 Data, + HOMEPAGE "" movie_homepage Ascii 300 Data, + RELEAASEDATE "" movie_release_date Ascii 20 Data, + RUNTIME "" movie_runtime UInt 0 Data, + POPULARITY "" movie_popularity Float 31 Data, + VOTEAVERAGE "" movie_vote_average Float 31 Data, +} + +// ---------------------------------------------------------------- +// Indices for Movies +// ---------------------------------------------------------------- + +Index movie +{ + movie_id "" MOVIEID, + movietitle "" TITLE, +} + +// ---------------------------------------------------------------- +// Table MovieActor +// ---------------------------------------------------------------- + +Table movie_actor +{ + ACTORID "" actor_id UInt 0 Primary, + ACTORNAME "" actor_name Ascii 300 Data, +} + +// ---------------------------------------------------------------- +// Indices for MovieActor +// ---------------------------------------------------------------- + +Index movie_actor +{ + actor_id "" ACTORID, +} + +// ---------------------------------------------------------------- +// Table MovieActors +// ---------------------------------------------------------------- + +Table movie_actors +{ + MOVIEID "" movie_id UInt 0 Primary, + ACTORID "" actor_id UInt 0 Primary, + ROLE "" actor_role Ascii 300 Data, +} + +// ---------------------------------------------------------------- +// Indices for MovieActors +// ---------------------------------------------------------------- + +Index movie_actors +{ + movie_id "" MOVIEID, + actor_id "" ACTORID, +} + +// ---------------------------------------------------------------- +// Table MovieMedia +// ---------------------------------------------------------------- + +Table movie_media +{ + MOVIEID "" movie_id UInt 0 Primary, + ACTORID "" actor_id UInt 0 Primary, + MEDIATYPE "" media_type UInt 0 Primary, + MEDIAURL "" media_url Ascii 100 Data, + MEDIAWIDTH "" media_width UInt 0 Data, + MEDIAHEIGHT "" media_height UInt 0 Data, + MEDIACONTENT "" media_content Mlob 1000000 Data, +} + +// ---------------------------------------------------------------- +// Indices for MovieMedia +// ---------------------------------------------------------------- + +Index movie_media +{ + movie_id "" MOVIEID, + actor_id "" ACTORID, +} diff --git a/configs/epgsearch/epgsearch.conf b/configs/epgsearch/epgsearch.conf new file mode 100644 index 0000000..bb41591 --- /dev/null +++ b/configs/epgsearch/epgsearch.conf @@ -0,0 +1,4 @@ +8:CSI - Den Tätern auf der Spur:0:::1:S19.2E-1-1019-10301|S19.2E-133-15-59:0:1:1:1:0:0:::1:0:0:1:%Series%:50:99:4:6:0:0:0::1:0:1:1:0:0:0:0:0:1:0:0::1:0:0:0:0:0:0:0:0:0:90::0 +22:Dr. House:0:::1:S19.2E-1-1019-10301|S19.2E-133-15-59:0:0:1:1:0:0:::1:0:0:1:%Series%:50:99:4:6:0:0:0::1:0:1:1:0:0:0:0:0:1:0:0::1:0:0:0:0:0:0:0:0:0:90::0 +24:Grey's Anatomy:0:::1:S19.2E-1-1019-10301|S19.2E-133-15-59:0:0:1:1:0:0:::1:0:0:1:%Series%:50:99:4:6:0:0:0::1:0:1:1:0:0:0:0:0:1:0:0::1:0:0:0:0:0:0:0:0:0:90::0 +41:The Closer:0:::1:S19.2E-1-1019-10301|S19.2E-133-15-59:0:4:1:1:0:0:::1:0:0:1:%Series%:50:99:4:6:0:0:0::1:0:1:1:0:0:0:0:0:1:0:0::1:0:0:0:0:0:0:0:0:0:90::0 diff --git a/configs/epgsearch/epgsearchcats.conf b/configs/epgsearch/epgsearchcats.conf new file mode 100644 index 0000000..a0d5680 --- /dev/null +++ b/configs/epgsearch/epgsearchcats.conf @@ -0,0 +1,42 @@ +# ----------------------------------------------------------------------------- +# This is just a sample - all items deactivated. Please edit! +# Perhaps a category or its value list should be removed. Also the +# 'name in menu' should be adjusted to your language. +# The order of items determines the order listed in epgsearch. It does not +# depend on the ID, which is used by epgsearch. +# Format: +# ID|category name|name in menu|values separated by ',' (option)|searchmode (option) +# - 'ID' should be a unique positive integer +# (changing the id later on will force you to reedit your search timers!) +# - 'category name' is the name in your epg.data +# - 'name in menu' is the name displayed in epgsearch. +# - 'values' is an optional list of possible values +# - 'searchmode' is an optional parameter specifying the mode of search: +# 0 - the whole term must appear as substring +# 1 - all single words (delimiters are ',', ';', '|' or '~') +# must exist as substrings. This is the default search mode. +# 2 - at least one word (delimiters are ',', ';', '|' or '~') +# must exist as substring. +# 3 - matches exactly +# 4 - regular expression +# ----------------------------------------------------------------------------- + +#1|Category|Kategorie|Information,Kinder,Musik,Serie,Show,Spielfilm,Sport|3 +#2|Genre|Genre|Abenteuer,Action,Boxen,Comedy,Dokumentarfilm,Drama,Erotik,Familien-Show,Fantasy,Fussball,Geschichte,Gesellschaft,Gesundheit,Gymnastik,Handball,Heimat,Humor,Jazz,Kinderfilme,Kindernachrichten,Kinderserien,Klassik,Krankenhaus,Krimi,Kultur,Kurzfilm,Motor+Verkehr,Motorsport,Musik,Mystery,Nachrichten,Natur,Politik,Radsport,Ratgeber,Reise,Rock,Romantik/Liebe,Science Fiction,Soap,Spielshows,Talkshows,Tennis,Thriller,Verschiedenes,Volksmusik,Wassersport,Western,Wintersport,Wirtschaft,Wissen,Zeichentrick|3 +#3|Format|Video-Format|16:9,4:3|3 +#4|Audio|Audio|Dolby Surround,Stereo|3 +#5|Year|Jahr||0 +#6|Cast|Besetzung||2 +#7|Director|Regisseur||2 +#8|Moderation|Moderation||2 +#9|Rating|Bewertung|Tagestip,Tip|3 +#10|FSK|FSK|6,12,16,18|3 + +1|Serie|Serie||2 +2|Kurzname|Kurzname||2 +3|Episode|Episode||2 +4|Staffel,%02i|Staffel||2 +5|Staffelfolge,%02i|Staffelfolge||2 +6|Folge,%03i|Folge||2 +7|Kategorie|Kategorie||2 +8|Jahr|Jahr||13 diff --git a/configs/epgsearch/epgsearchuservars.conf b/configs/epgsearch/epgsearchuservars.conf new file mode 100644 index 0000000..cb88a78 --- /dev/null +++ b/configs/epgsearch/epgsearchuservars.conf @@ -0,0 +1,12 @@ +%Langname%=%Serie% ? %Serie% : %Title% +%Name%=%Kurzname% ? %Kurzname% : %Langname% + +%Epg%=%Name%~%Staffel%x%Staffelfolge% - %Folge%. %Subtitle% +%Dummy%=%Name%~?x? - ?. %Subtitle% +%Season%=%Staffel% ? %Epg% : %Dummy% + +%DateVar%=%time_w% %date% %time% +%SerieSD%=%Subtitle% ? %Subtitle% : %DateVar% +%SerieVar1%=Serie~%Name%~%SerieSD% + +%Series%=%Subtitle% ? %Season% : %SerieVar1% diff --git a/contrib/epg2vdr.ignore b/contrib/epg2vdr.ignore new file mode 100644 index 0000000..8fe05d6 --- /dev/null +++ b/contrib/epg2vdr.ignore @@ -0,0 +1,25 @@ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Auto check master role +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Change handler state to.*active +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Cleanup finished, removed.*images and.*symlinks +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: DEBUG: mysql_library_end +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: DVB event found -1 -> for +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Finished|Start reading +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Got.*new images from database +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Handle insert|update of event +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Init handler instance for thread +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Load images from database +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Processed tvm|vdr channel +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Remove old symlinks +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Removed file +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: SQL client character now +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Scheduled next auto update in +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Set locale to +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Starting cleanup of images in +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Statement.*insert|select +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Statement.*update|delete +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Store image.*/var/vdr/epgimages/images/.* +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Update EPG, loading changes since +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Update thread started|ended +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: Updated.*channels and.*events.*in.*seconds +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: detected UTF-8 +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ vdr: EPG2VDR: duration.*was diff --git a/epg2vdr.c b/epg2vdr.c new file mode 100644 index 0000000..1002c65 --- /dev/null +++ b/epg2vdr.c @@ -0,0 +1,991 @@ +/* + * epg2vdr.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <vdr/menu.h> +#include <vdr/tools.h> + +#include "plgconfig.h" +#include "update.h" +#include "menu.h" +#include "handler.h" + +#if defined (APIVERSNUM) && (APIVERSNUM < 20200) +# error VDR API versions < 2.2.0 are not supported ! +#endif + +cUpdate* oUpdate = 0; +const char* logPrefix = LOG_PREFIX; + +//*************************************************************************** +// Static global handler Instance +//*************************************************************************** + +cEpg2VdrEpgHandler* cEpg2VdrEpgHandler::singleton = cEpg2VdrEpgHandler::getSingleton(); + +//*************************************************************************** +// Menu Edit List Item +//*************************************************************************** + +class cMenuEditStrListItem : public cMenuEditIntItem +{ + public: + + cMenuEditStrListItem(const char* Name, int* Value, cStringList* List); + + protected: + + virtual void Set(); + cStringList* list; +}; + +cMenuEditStrListItem::cMenuEditStrListItem(const char* Name, int* Value, cStringList* List) + : cMenuEditIntItem(Name, Value, 0, List->Size()-1) +{ + list = List; + Set(); +} + +void cMenuEditStrListItem::Set() +{ + char* v = (*list)[*value]; + SetValue(v); +} + +//*************************************************************************** +// Plugin Main Menu +//*************************************************************************** + +class cEpgPluginMenu : public cOsdMenu +{ + public: + + cEpgPluginMenu(const char* title, cPluginEPG2VDR* aPlugin); + virtual ~cEpgPluginMenu() { }; + + virtual eOSState ProcessKey(eKeys key); + + protected: + + cPluginEPG2VDR* plugin; +}; + +enum EpgMenuState +{ + emsTimer = os_User, + emsSearchtimer, + emsDones, + emsProgram, + emsUpdate, + emsReload +}; + +cEpgPluginMenu::cEpgPluginMenu(const char* title, cPluginEPG2VDR* aPlugin) + : cOsdMenu(title) +{ + plugin = aPlugin; + + SetMenuCategory(mcMain); + SetHasHotkeys(yes); + Clear(); + + if (Epg2VdrConfig.shareInWeb) + { + cOsdMenu::Add(new cOsdItem(hk(tr("Timer")), (eOSState)emsTimer)); + cOsdMenu::Add(new cOsdItem(hk(tr("Search Timer")), (eOSState)emsSearchtimer)); + cOsdMenu::Add(new cOsdItem(hk(tr("Timer journal")), (eOSState)emsDones)); + } + + cOsdMenu::Add(new cOsdItem(hk(tr("Program")), (eOSState)emsProgram)); + cOsdMenu::Add(new cOsdItem(hk(tr("Update")), (eOSState)emsUpdate)); + cOsdMenu::Add(new cOsdItem(hk(tr("Reload")), (eOSState)emsReload)); + + SetHelp(0, 0, 0, 0); + + Display(); +} + +eOSState cEpgPluginMenu::ProcessKey(eKeys key) +{ + eOSState state = cOsdMenu::ProcessKey(key); + + switch (state) + { + case emsTimer: + { + state = AddSubMenu(new cMenuEpgTimers()); + break; + } + + case emsSearchtimer: + { + state = AddSubMenu(new cMenuEpgSearchTimers()); + break; + } + + case emsDones: + { + state = AddSubMenu(new cEpgMenuDones()); + break; + } + + case emsProgram: + { + state = AddSubMenu(new cMenuEpgWhatsOn()); + break; + } + + case emsUpdate: + { + Skins.Message(mtInfo, tr("Update EPG")); + oUpdate->triggerEpgUpdate(); + return osEnd; + } + + case emsReload: + { + Skins.Message(mtInfo, tr("Reload EPG")); + oUpdate->triggerEpgUpdate(true); + return osEnd; + } + + default: ; + } + + return state; +} + +//*************************************************************************** +// Plugin Setup Menu +//*************************************************************************** + +class cMenuSetupEPG2VDR : public cMenuSetupPage +{ + public: + + cMenuSetupEPG2VDR(); + ~cMenuSetupEPG2VDR() ; + + protected: + + virtual eOSState ProcessKey(eKeys Key); + virtual void Store(); + virtual void Setup(); + + private: + + cEpg2VdrConfig data; + cMenuDb* menuDb; + long int webLoginEnabled; + char** userList; + int userCount; + cStringList interfaceList; + int interfaceIndex; +}; + +cMenuSetupEPG2VDR::cMenuSetupEPG2VDR() +{ + data = Epg2VdrConfig; + menuDb = new cMenuDb; + webLoginEnabled = no; + userList = 0; + userCount = 0; + interfaceIndex = 0; + + Setup(); +} + +cMenuSetupEPG2VDR::~cMenuSetupEPG2VDR() +{ + if (userList) + { + for (int i = 0; i < userCount; i++) + free(userList[i]); + + delete userList; + } + + delete menuDb; +} + +void cMenuSetupEPG2VDR::Setup() +{ + int current = Current(); + + // + // DVB Event update mode + + static const char* masterModes[4]; + + masterModes[0] = tr("auto"); + masterModes[1] = tr("yes"); + masterModes[2] = tr("no"); + masterModes[3] = 0; + + // + // List of WEB users + + menuDb->initUserList(userList, userCount, webLoginEnabled); + + if (webLoginEnabled && userCount && !isEmpty(Epg2VdrConfig.user)) + { + // set userIndex to the configured user + + for (int i = 0; i < userCount; i++) + { + if (strcmp(userList[i], data.user) == 0) + { + data.userIndex = i; + break; + } + } + } + + // + // Network Interfaces + + int i = 0; + char* interfaces; + + interfaces = strdup(getInterfaces()); + interfaceList.Clear(); + + for (char* p = strtok(interfaces, " "); p; p = strtok(0, " ")) + { + interfaceList.Append(strdup(p)); + + if (!isEmpty(data.netDevice) && strncmp(p, data.netDevice, strlen(data.netDevice)) == 0) + { + tell(0, "Index of '%s' is %d", p, i); + interfaceIndex = i; + } + + i++; + } + + free(interfaces); + + // ... + + Clear(); + + Add(new cOsdItem(cString::sprintf("-------------------- %s ----------------------------------", tr("EPG Update")))); + cList<cOsdItem>::Last()->SetSelectable(false); + + cOsdMenu::Add(new cMenuEditStraItem(tr("Update DVB EPG Database"), (int*)&data.masterMode, cUpdate::mmCount, masterModes)); + Add(new cMenuEditBoolItem(tr("Load Images"), &data.getepgimages)); + Add(new cMenuEditBoolItem(tr("Prohibit Shutdown On Busy 'epgd'"), &data.activeOnEpgd)); + Add(new cMenuEditBoolItem(tr("Schedule Boot For Update"), &data.scheduleBoot)); + Add(new cMenuEditBoolItem(tr("Blacklist not configured Channels"), &data.blacklist)); + + Add(new cOsdItem(cString::sprintf("--------------------- %s ---------------------------------", tr("Menu")))); + cList<cOsdItem>::Last()->SetSelectable(false); + Add(new cMenuEditBoolItem(tr("Show In Main Menu"), &data.mainmenuVisible)); + Add(new cMenuEditBoolItem(tr("Replace Program Menu"), &data.replaceScheduleMenu)); + Add(new cMenuEditBoolItem(tr("Replace Timer Menu"), &data.replaceTimerMenu)); + Add(new cMenuEditBoolItem(tr("XChange Key Ok/Blue"), &data.xchgOkBlue)); + Add(new cMenuEditBoolItem(tr("Show Channels without EPG"), &data.showEmptyChannels)); + + Add(new cOsdItem(cString::sprintf("--------------------- %s ---------------------------------", tr("Web")))); + cList<cOsdItem>::Last()->SetSelectable(false); + Add(new cMenuEditBoolItem(tr("Share in Web"), &data.shareInWeb)); + Add(new cMenuEditBoolItem(tr("Create Timer Local"), &data.createTimerLocal)); + Add(new cMenuEditBoolItem(tr("Have Common Recordings Folder (NAS)"), &data.useCommonRecFolder)); + Add(new cMenuEditStrListItem(tr("SVDRP Interface"), &interfaceIndex, &interfaceList)); + if (userCount && webLoginEnabled) + Add(new cMenuEditStraItem(tr("User"), (int*)&data.userIndex, userCount, userList)); + + Add(new cOsdItem(cString::sprintf("--------------------- %s ---------------------------------", tr("MySQL")))); + cList<cOsdItem>::Last()->SetSelectable(false); + Add(new cMenuEditStrItem(tr("Host"), data.dbHost, sizeof(data.dbHost), tr(FileNameChars))); + Add(new cMenuEditIntItem(tr("Port"), &data.dbPort, 1, 99999)); + Add(new cMenuEditStrItem(tr("Database Name"), data.dbName, sizeof(data.dbName), tr(FileNameChars))); + Add(new cMenuEditStrItem(tr("User"), data.dbUser, sizeof(data.dbUser), tr(FileNameChars))); + Add(new cMenuEditStrItem(tr("Password"), data.dbPass, sizeof(data.dbPass), tr(FileNameChars))); + + Add(new cOsdItem(cString::sprintf("--------------------- %s ---------------------------------", tr("Technical Stuff")))); + cList<cOsdItem>::Last()->SetSelectable(false); + Add(new cMenuEditIntItem(tr("Log level"), &data.loglevel, 0, 4)); + + SetCurrent(Get(current)); + Display(); +} + +eOSState cMenuSetupEPG2VDR::ProcessKey(eKeys Key) +{ + eOSState state = cMenuSetupPage::ProcessKey(Key); + + switch (state) + { + case osContinue: + { + if (NORMALKEY(Key) == kUp || NORMALKEY(Key) == kDown) + { + cOsdItem* item = Get(Current()); + + if (item) + item->ProcessKey(kNone); + } + + break; + } + + default: break; + } + + return state; +} + +void cMenuSetupEPG2VDR::Store() +{ + int useCommonRecFolderOptionChanged = no; + + if (Epg2VdrConfig.useCommonRecFolder != data.useCommonRecFolder) + useCommonRecFolderOptionChanged = yes; + + if (data.hasDbLoginChanged(&Epg2VdrConfig)) + oUpdate->triggerDbReconnect(); + + Epg2VdrConfig = data; + + SetupStore("LogLevel", Epg2VdrConfig.loglevel); + SetupStore("ShowInMainMenu", Epg2VdrConfig.mainmenuVisible); + SetupStore("Blacklist", Epg2VdrConfig.blacklist); + SetupStore("DbHost", Epg2VdrConfig.dbHost); + SetupStore("DbPort", Epg2VdrConfig.dbPort); + SetupStore("DbName", Epg2VdrConfig.dbName); + SetupStore("DbUser", Epg2VdrConfig.dbUser); + SetupStore("DbPass", Epg2VdrConfig.dbPass); + SetupStore("MasterMode", Epg2VdrConfig.masterMode); + SetupStore("LoadImages", Epg2VdrConfig.getepgimages); + SetupStore("ActiveOnEpgd", Epg2VdrConfig.activeOnEpgd); + SetupStore("ScheduleBoot", Epg2VdrConfig.scheduleBoot); + SetupStore("ShareInWeb", Epg2VdrConfig.shareInWeb); + SetupStore("CreateTimerLocal", Epg2VdrConfig.createTimerLocal); + SetupStore("XChgKeyOkBlue", Epg2VdrConfig.xchgOkBlue); + SetupStore("ShowEmptyChannels", Epg2VdrConfig.showEmptyChannels); + SetupStore("UseCommonRecFolder", Epg2VdrConfig.useCommonRecFolder); + SetupStore("ReplaceScheduleMenu", Epg2VdrConfig.replaceScheduleMenu); + SetupStore("ReplaceTimerMenu", Epg2VdrConfig.replaceTimerMenu); + + if (userCount && Epg2VdrConfig.userIndex >= 0) + { + sstrcpy(Epg2VdrConfig.user, userList[Epg2VdrConfig.userIndex], sizeof(Epg2VdrConfig.user)); + SetupStore("User", Epg2VdrConfig.user); + } + + char* device = strdup(interfaceList[interfaceIndex]); + char* ip = strchr(device, ':'); + + if (!isEmpty(ip)) + { + *(ip++) = 0; + + if (strcmp(Epg2VdrConfig.netDevice, device) != 0) + { + sstrcpy(Epg2VdrConfig.netDevice, device, sizeof(Epg2VdrConfig.netDevice)); + + menuDb->vdrDb->clear(); + menuDb->vdrDb->setValue("UUID", Epg2VdrConfig.uuid); + menuDb->vdrDb->find(); + menuDb->vdrDb->setValue("IP", ip); + menuDb->vdrDb->store(); + } + } + + free(device); + + SetupStore("NetDevice", Epg2VdrConfig.netDevice); + + if (useCommonRecFolderOptionChanged) + oUpdate->commonRecFolderOptionChanged(); +} + +//*************************************************************************** +// Plugin - EPG2VDR +//*************************************************************************** + +cPluginEPG2VDR::cPluginEPG2VDR() +{ + oUpdate = 0; + connection = 0; + timerDb = 0; + vdrDb = 0; +} + +cPluginEPG2VDR::~cPluginEPG2VDR() +{ + exitDb(); + delete oUpdate; +} + +int cPluginEPG2VDR::initDb() +{ + int status = success; + + exitDb(); + + connection = new cDbConnection(); + + timerDb = new cDbTable(connection, "timers"); + if (timerDb->open() != success) return fail; + + vdrDb = new cDbTable(connection, "vdrs"); + if (vdrDb->open() != success) return fail; + + // ---------- + // select + // t.*, + // v.name, v.state + // from timers t, vdrs v + // where + // (t.state in ('P','R') or t.state is null) + // and t.vdruuid = v.uuid + + selectTimers = new cDbStatement(timerDb); + + selectTimers->build("select "); + selectTimers->setBindPrefix("t."); + selectTimers->bindAllOut(); + selectTimers->setBindPrefix("v."); + selectTimers->bind(vdrDb, "NAME", cDBS::bndOut, ", "); + selectTimers->bind(vdrDb, "UUID", cDBS::bndOut, ", "); + selectTimers->bind(vdrDb, "STATE", cDBS::bndOut, ", "); + selectTimers->clrBindPrefix(); + selectTimers->build(" from %s t, %s v where (t.%s in ('P','R') or t.%s is null)", + timerDb->TableName(), vdrDb->TableName(), + timerDb->getField("STATE")->getDbName(), + timerDb->getField("STATE")->getDbName()); + selectTimers->build(" and t.%s = v.%s", + timerDb->getField("VDRUUID")->getDbName(), + vdrDb->getField("UUID")->getDbName()); + + status += selectTimers->prepare(); + + return status; +} + +int cPluginEPG2VDR::exitDb() +{ + if (connection) + { + delete timerDb; timerDb = 0; + delete vdrDb; vdrDb = 0; + delete connection; connection = 0; + } + + return done; +} + +void cPluginEPG2VDR::DisplayMessage(const char *s) +{ + tell(0, "%s", s); + + Skins.Message(mtInfo, tr(s)); + sleep(Setup.OSDMessageTime); +} + +const char *cPluginEPG2VDR::CommandLineHelp() +{ + return 0; +} + +const char **cPluginEPG2VDR::SVDRPHelpPages() +{ + static const char *HelpPages[] = + { + "UPDATE\n" + " Load new/changed events database.", + "RELOAD\n" + " Reload all events from database to EPG", + "STATE <state>\n" + " For internal usage", + "TIMERJOB\n" + " Check timer jobs", + "UPDREC\n" + " Trigger update of recordings", + "STOREIFO\n" + " Trigger store of recordin info files", + 0 + }; + + return HelpPages; +} + +cString cPluginEPG2VDR::SVDRPCommand(const char* cmd, const char* Option, int &ReplyCode) +{ + // ------------------------------------ + // update epg from database + + if (strcasecmp(cmd, "UPDATE") == 0) + { + oUpdate->triggerEpgUpdate(); + return "EPG2VDR update started."; + } + + // ------------------------------------ + // reload epg from database + + else if (strcasecmp(cmd, "RELOAD") == 0) + { + oUpdate->triggerEpgUpdate(true); + return "EPG2VDR full reload of events from database."; + } + + // ------------------------------------ + // update recording table + + else if (strcasecmp(cmd, "UPDREC") == 0) + { + oUpdate->triggerRecUpdate(); + return "EPG2VDR update of recordings triggert."; + } + + // ------------------------------------ + // store recording info files + + else if (strcasecmp(cmd, "STOREIFO") == 0) + { + oUpdate->triggerStoreInfoFiles(); + return "EPG2VDR store of info files triggert."; + } + + // ------------------------------------ + // inform about epgd's state change + + else if (strcasecmp(cmd, "STATE") == 0) + { + if (isEmpty(Option)) + { + ReplyCode = 901; + return "Error: Missing option"; + } + + oUpdate->epgdStateChange(Option); + return "EPG2VDR epgd state change accepted"; + } + + // ------------------------------------ + // trigger processing timer job + + else if (strcasecmp(cmd, "TIMERJOB") == 0) + { + oUpdate->triggerTimerJobs(); + return "EPG2VDR check of timer jobs triggered"; + } + + // ------------------------------------ + // delete recording + + else if (strcasecmp(cmd, "DELREC") == 0) + { + if (isEmpty(Option)) + { + ReplyCode = 901; + return "Error: Missing option"; + } + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + LOCK_RECORDINGS_WRITE; + cRecordings* recordings = Recordings; +#else + cRecordings* recordings = &Recordings; +#endif + + if (cRecording* rec = recordings->GetByName(Option)) + { + if (rec->IsInUse()) + { + ReplyCode = 554; + tell(0, "Can't modify, recoring is in use"); + return "Can't delete, recoring is in use"; + } + + if (!rec->Delete()) + { + ReplyCode = 554; + tell(0, "Error: Delete of recording '%s' failed", Option); + return "Error: Delete of recording failed"; + } + + recordings->DelByName(rec->FileName()); +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + recordings->SetModified(); +#endif + } + else + { + ReplyCode = 550; + tell(0, "Error: Delete failed, can't find recording '%s'", Option); + return "Error: Delete failed, can't find recording"; + } + + oUpdate->triggerRecUpdate(); + tell(0, "Deleted recording '%s'", Option); + return "Deleted recording"; + } + + // ------------------------------------ + // play recording + + else if (strcasecmp(cmd, "PLAYREC") == 0) + { + if (isEmpty(Option)) + { + ReplyCode = 901; + return "Error: Missing option"; + } + + char* opt = strdup(Option); + char* name = skipspace(opt); + char* position = skipspace(opt); + + while (*position && !isspace(*position)) + position++; + + if (*position) + { + *position = 0; + position++; + } + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + LOCK_RECORDINGS_READ; + const cRecording* recording = Recordings->GetByName(name); +#else + const cRecording* recording = Recordings.GetByName(name); +#endif + + if (!recording) + { + free(opt); + ReplyCode = 901; + return "Recording not found"; + } + + cReplayControl::SetRecording(0); + cControl::Shutdown(); + + if (!isEmpty(position)) + { + int pos = 0; + + if (strcasecmp(position, "BEGIN") != 0) + pos = HMSFToIndex(position, recording->FramesPerSecond()); + + cResumeFile Resume(recording->FileName(), recording->IsPesRecording()); + + if (pos <= 0) + Resume.Delete(); + else + Resume.Save(pos); + } + + cReplayControl::SetRecording(recording->FileName()); + cControl::Launch(new cReplayControl); + cControl::Attach(); + free(opt); + tell(0, "Playing recording '%s'", name); + return "Playing recording"; + } + + // ------------------------------------ + // rename recording + + else if (strcasecmp(cmd, "RENREC") == 0) + { + char* alt = 0; + char* neu = 0; + char* p; + + if (!isEmpty(Option)) + { + alt = strdup(Option); + p = alt; + + if ((p = strchr(p, ' '))) + { + *p = 0; + neu = p+1; + } + } + + if (isEmpty(neu) || isEmpty(alt)) + { + free(alt); + ReplyCode = 901; + return "Error: Missing option"; + } + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + LOCK_RECORDINGS_WRITE; + cRecordings* recordings = Recordings; + recordings->SetExplicitModify(); +#else + cRecordings* recordings = &Recordings; +#endif + + if (cRecording* rec = recordings->GetByName(alt)) + { + if (rec->IsInUse()) + { + ReplyCode = 554; + tell(0, "Can't modify, recoring is in use"); + return "Can't modify, recoring is in use"; + } + if (!rec->ChangeName(neu)) + { + ReplyCode = 554; + tell(0, "Error: Modify of recordings '%s' failed", alt); + free(alt); + return "Error: Modify of recording failed"; + } + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + recordings->SetModified(); +#endif + recordings->TouchUpdate(); + } + else + { + ReplyCode = 550; + tell(0, "Error: Modify failed, can't find recording '%s'", alt); + free(alt); + return "Error: Modify failed, can't find recording"; + } + + oUpdate->triggerRecUpdate(); + tell(0, "Modified recording '%s' to '%s'", alt, neu); + free(alt); + return "Modified recording"; + } + + return 0; +} + +//*************************************************************************** +// Service +//*************************************************************************** + +bool cPluginEPG2VDR::Service(const char* id, void* data) +{ + if (!data) + return fail; + + tell(4, "Service called with '%s', %d/%d", id, + Epg2VdrConfig.replaceScheduleMenu, Epg2VdrConfig.replaceTimerMenu); + + if (strcmp(id, EPG2VDR_UUID_SERVICE) == 0) + { + Epg2vdr_UUID_v1_0* req = (Epg2vdr_UUID_v1_0*)data; + + req->uuid = Epg2VdrConfig.uuid; + + return true; + } + + else if (strcmp(id, MYSQL_INIT_EXIT) == 0) + { + Mysql_Init_Exit_v1_0* ref = (Mysql_Init_Exit_v1_0*)data; + + if (ref->action == mieaInit) + cDbConnection::init(); + else if (ref->action == mieaExit) + cDbConnection::exit(); + else + tell(0, "Warning: Got unexpected action %d in '%s' call", + ref->action, MYSQL_INIT_EXIT); + + return true; + } + + else if (strcmp(id, "MainMenuHooksPatch-v1.0::osSchedule") == 0 && Epg2VdrConfig.replaceScheduleMenu) + { + cOsdMenu** menu = (cOsdMenu**)data; + + if (menu) + *menu = new cMenuEpgWhatsOn(); + + return true; + } + + else if (strcmp(id, "MainMenuHooksPatch-v1.0::osTimers") == 0 && Epg2VdrConfig.replaceTimerMenu) + { + cOsdMenu** menu = (cOsdMenu**)data; + + if (menu) + *menu = new cMenuEpgTimers(); + + return true; + } + + else if (strcmp(id, EPG2VDR_TIMER_SERVICE) == 0) + { + cEpgTimer_Service_V1* ts = (cEpgTimer_Service_V1*)data; + + if (ts) + return timerService(ts); + } + + return false; +} + +//*************************************************************************** +// Timer Service +//*************************************************************************** + +int cPluginEPG2VDR::timerService(cEpgTimer_Service_V1* ts) +{ + cMutexLock lock(&mutexTimerService); + uint64_t start = cTimeMs::Now(); + + if (initDb() == success) + { + timerDb->clear(); + vdrDb->clear(); + + ts->epgTimers.clear(); + + for (int f = selectTimers->find(); f && connection->check() == success; f = selectTimers->fetch()) + { + cEpgTimer* epgTimer = newTimerObjectFromRow(timerDb->getRow(), vdrDb->getRow()); + + if (Epg2VdrConfig.shareInWeb || epgTimer->isLocal()) + ts->epgTimers.push_back(epgTimer); + else + delete epgTimer; + } + + tell(1, "Answer '%s' call with %lu timers, duration was (%s)", + EPG2VDR_TIMER_SERVICE, + ts->epgTimers.size(), + ms2Dur(cTimeMs::Now()-start).c_str()); + } + + exitDb(); + + return true; +} + +//*************************************************************************** +// Initialize +//*************************************************************************** + +bool cPluginEPG2VDR::Initialize() +{ + return true; +} + +//*************************************************************************** +// Start +//*************************************************************************** + +bool cPluginEPG2VDR::Start() +{ + Mysql_Init_Exit_v1_0 req; + + req.action = mieaInit; + Service(MYSQL_INIT_EXIT, &req); + + oUpdate = new cUpdate(this); + + if (oUpdate->init() == success) + oUpdate->Start(); // start plugin thread + else + tell(0, "Initialization failed, start of plugin aborted!"); + + return true; +} + +cString cPluginEPG2VDR::Active() +{ + if (Epg2VdrConfig.activeOnEpgd && oUpdate->isEpgdBusy()) + return tr("EPG2VDR Waiting on epgd"); + +// time_t timeoutAt = time(0) + 10; + +// while (oUpdate->isUpdateActive()) +// { +// tell(0, "EPG2VDR EPG update running, wating up to 10 seconds .."); + +// if (time(0) > timeoutAt) +// { +// tell(0, "EPG2VDR EPG update running, shutdown timed out, aborting"); +// return 0; +// } + +// usleep(500000); +// } + + return 0; +} + +time_t cPluginEPG2VDR::WakeupTime() +{ + // return custom wakeup time for shutdown script + + if (Epg2VdrConfig.scheduleBoot && oUpdate->getNextEpgdUpdateAt()) + return oUpdate->getNextEpgdUpdateAt(); + + return 0; +} + +cOsdObject* cPluginEPG2VDR::MainMenuAction() +{ + return new cEpgPluginMenu(MAINMENUENTRY, this); +} + +cMenuSetupPage* cPluginEPG2VDR::SetupMenu() +{ + return new cMenuSetupEPG2VDR; +} + +bool cPluginEPG2VDR::SetupParse(const char *Name, const char *Value) +{ + // Parse your own setup parameters and store their values. + + if (!strcasecmp(Name, "LogLevel")) Epg2VdrConfig.loglevel = atoi(Value); + else if (!strcasecmp(Name, "ShowInMainMenu")) Epg2VdrConfig.mainmenuVisible = atoi(Value); + else if (!strcasecmp(Name, "Blacklist")) Epg2VdrConfig.blacklist = atoi(Value); + else if (!strcasecmp(Name, "DbHost")) sstrcpy(Epg2VdrConfig.dbHost, Value, sizeof(Epg2VdrConfig.dbHost)); + else if (!strcasecmp(Name, "DbPort")) Epg2VdrConfig.dbPort = atoi(Value); + else if (!strcasecmp(Name, "DbName")) sstrcpy(Epg2VdrConfig.dbName, Value, sizeof(Epg2VdrConfig.dbName)); + else if (!strcasecmp(Name, "DbUser")) sstrcpy(Epg2VdrConfig.dbUser, Value, sizeof(Epg2VdrConfig.dbUser)); + else if (!strcasecmp(Name, "DbPass")) sstrcpy(Epg2VdrConfig.dbPass, Value, sizeof(Epg2VdrConfig.dbPass)); + else if (!strcasecmp(Name, "MasterMode")) Epg2VdrConfig.masterMode = atoi(Value); + else if (!strcasecmp(Name, "LoadImages")) Epg2VdrConfig.getepgimages = atoi(Value); + else if (!strcasecmp(Name, "ActiveOnEpgd")) Epg2VdrConfig.activeOnEpgd = atoi(Value); + else if (!strcasecmp(Name, "ScheduleBoot")) Epg2VdrConfig.scheduleBoot = atoi(Value); + else if (!strcasecmp(Name, "UseCommonRecFolder")) Epg2VdrConfig.useCommonRecFolder = atoi(Value); + else if (!strcasecmp(Name, "ShareInWeb")) Epg2VdrConfig.shareInWeb = atoi(Value); + else if (!strcasecmp(Name, "CreateTimerLocal")) Epg2VdrConfig.createTimerLocal = atoi(Value); + else if (!strcasecmp(Name, "XChgKeyOkBlue")) Epg2VdrConfig.xchgOkBlue = atoi(Value); + else if (!strcasecmp(Name, "ShowEmptyChannels")) Epg2VdrConfig.showEmptyChannels = atoi(Value); + else if (!strcasecmp(Name, "Uuid")) sstrcpy(Epg2VdrConfig.uuid, Value, sizeof(Epg2VdrConfig.uuid)); + else if (!strcasecmp(Name, "NetDevice")) sstrcpy(Epg2VdrConfig.netDevice, Value, sizeof(Epg2VdrConfig.netDevice)); + else if (!strcasecmp(Name, "ReplaceScheduleMenu")) Epg2VdrConfig.replaceScheduleMenu = atoi(Value); + else if (!strcasecmp(Name, "ReplaceTimerMenu")) Epg2VdrConfig.replaceTimerMenu = atoi(Value); + else if (!strcasecmp(Name, "User")) sstrcpy(Epg2VdrConfig.user, Value, sizeof(Epg2VdrConfig.user)); + + else + return false; + + return true; +} + +void cPluginEPG2VDR::Stop() +{ + oUpdate->Stop(); + + Mysql_Init_Exit_v1_0 req; + + req.action = mieaExit; + Service(MYSQL_INIT_EXIT, &req); +} + +//*************************************************************************** + +VDRPLUGINCREATOR(cPluginEPG2VDR) diff --git a/epg2vdr.h b/epg2vdr.h new file mode 100644 index 0000000..d2f05a2 --- /dev/null +++ b/epg2vdr.h @@ -0,0 +1,80 @@ +/* + * epg2vdr.h: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __EPG2VDR_H +#define __EPG2VDR_H + +#include <list> + +#include <vdr/plugin.h> +#include "plgconfig.h" +#include "service.h" +#include "HISTORY.h" + +#include "lib/db.h" + +class cUpdate; +class cEpg2VdrEpgHandler; +extern cUpdate* oUpdate; + +//*************************************************************************** +// Constants +//*************************************************************************** + +static const char* DESCRIPTION = trNOOP("epg2vdr plugin"); +static const char* MAINMENUENTRY = tr("EPG and Timer Service"); + +//*************************************************************************** +// cPluginEPG2VDR +//*************************************************************************** + +cOsdMenu* newWathsOn(); + +class cPluginEPG2VDR : public cPlugin +{ + public: + + cPluginEPG2VDR(void); + virtual ~cPluginEPG2VDR(); + + virtual const char* Version(void) { return VERSION; } + virtual const char* VersionDate(void) { return VERSION_DATE; } + virtual const char* Description(void) { return tr(DESCRIPTION); } + virtual const char* CommandLineHelp(void); + virtual bool Service(const char* id, void* data); + virtual const char** SVDRPHelpPages(void); + virtual cString SVDRPCommand(const char* Cmd, const char* Option, int& ReplyCode); + virtual bool Initialize(void); + virtual bool Start(void); + virtual cString Active(void); + virtual const char* MainMenuEntry(void) + { return Epg2VdrConfig.mainmenuVisible ? MAINMENUENTRY : 0; } + virtual cOsdObject* MainMenuAction(void); + virtual cMenuSetupPage* SetupMenu(void); + virtual bool SetupParse(const char* Name, const char* Value); + virtual void Stop(); + virtual void DisplayMessage(const char* s); + virtual time_t WakeupTime(void); + + protected: + + int initDb(); + int exitDb(); + + int timerService(cEpgTimer_Service_V1* ts); + + private: + + cDbConnection* connection; + cDbTable* timerDb; + cDbTable* vdrDb; + cDbStatement* selectTimers; + cMutex mutexTimerService; +}; + +//*************************************************************************** +#endif // EPG2VDR_H diff --git a/handler.h b/handler.h new file mode 100644 index 0000000..f341d94 --- /dev/null +++ b/handler.h @@ -0,0 +1,1141 @@ +/* + * handler.h: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __HANDLER_H +#define __HANDLER_H + +#include "update.h" + +#define CHANNELMARKOBSOLETE "OBSOLETE" + +//*************************************************************************** +// Define tEventID again, to create a compiler error case it was defines different +//*************************************************************************** + +typedef u_int32_t tEventID; // on error vdr >= 2.3.1 and patch is not applied!! + +//*************************************************************************** +// Mutex Try +//*************************************************************************** + +class cMutexTry +{ + public: + + cMutexTry() + { + locked = 0; + + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); + pthread_mutex_init(&mutex, &attr); + } + + ~cMutexTry() + { + pthread_mutex_destroy(&mutex); + } + + int tryLock() + { + if (pthread_mutex_trylock(&mutex) == 0) + { + locked++; + // tell(0, "[%d] got lock (%d) [%p]", cThread::ThreadId(), locked, this); + + return yes; + } + + // tell(0, "[%d] don't got lock (%d) [%p]", cThread::ThreadId(), locked, this); + + return no; + } + + void lock() + { + pthread_mutex_lock(&mutex); + locked++; + } + + void unlock() + { + if (locked) + { + // tell(0, "[%d] unlock (%d) [%p]", cThread::ThreadId(), locked, this); + locked--; + pthread_mutex_unlock(&mutex); + } + } + + private: + + pthread_mutex_t mutex; + int locked; +}; + +//*************************************************************************** +// EPG Handler +//*************************************************************************** + +class cEpgHandlerInstance +{ + public: + + cEpgHandlerInstance() + { + initialized = no; + connection = 0; + eventsDb = 0; + compDb = 0; + mapDb = 0; + vdrDb = 0; + endTime = 0; + updateDelFlg = 0; + selectDelFlg = 0; + delCompOf = 0; + selectMergeSp = 0; + selectEventByStarttime = 0; + + tell(0, "Handler: Init handler instance for thread %d", cThread::ThreadId()); + } + + virtual ~cEpgHandlerInstance() { exitDb(); } + + virtual int dbConnected(int force = no) + { + return initialized + && connection + && (!force || connection->check() == success); + } + + int checkConnection() + { + static int retry = 0; + + // check connection + + if (!dbConnected(yes)) + { + // try to connect + + tell(0, "Handler: Trying to re-connect to database!"); + retry++; + + if (initDb() != success) + { + exitDb(); + + tell(0, "Handler: Database re-connect failed!"); + return fail; + } + + retry = 0; + tell(0, "Handler: Connection established successfull!"); + } + + return success; + } + + int isEpgdBusy() + { + int busy = no; + + if (!dbConnected()) + return true; + + vdrDb->clear(); + vdrDb->setValue("Uuid", EPGDNAME); + + if (vdrDb->find()) + { + if (strcasecmp(vdrDb->getStrValue("State"), "busy (events)") == 0) + busy = yes; + } + + vdrDb->reset(); + + return busy; + } + + int initDb() + { + int status = success; + + exitDb(); + + connection = new cDbConnection(); + + vdrDb = new cDbTable(connection, "vdrs"); + if (vdrDb->open() != success) return fail; + + mapDb = new cDbTable(connection, "channelmap"); + if (mapDb->open() != success) return fail; + + eventsDb = new cDbTable(connection, "events"); + if (eventsDb->open() != success) return fail; + + compDb = new cDbTable(connection, "components"); + if (compDb->open() != success) return fail; + + // select + // * from events + // where + // source = 'vdr' + // and starttime = ? + // and channelid = ? + + selectEventByStarttime = new cDbStatement(eventsDb); + + selectEventByStarttime->build("select "); + selectEventByStarttime->bindAllOut(0, cDBS::ftAll); + selectEventByStarttime->build(" from %s where source = 'vdr'", eventsDb->TableName()); + selectEventByStarttime->bind("STARTTIME", cDBS::bndIn | cDBS::bndSet, " and "); + selectEventByStarttime->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet, " and "); + + status += selectEventByStarttime->prepare(); + + // prepare statement to get mapsp + // select + // mergesp from channelmap + // where + // source != 'vdr' + // and channelid = ? limit 1 + + selectMergeSp = new cDbStatement(mapDb); + + selectMergeSp->build("select "); + selectMergeSp->bind("MergeSp", cDBS::bndOut); + selectMergeSp->build(" from %s where source != 'vdr'", mapDb->TableName()); + selectMergeSp->bind("ChannelId", cDBS::bndIn | cDBS::bndSet, " and "); + selectMergeSp->build(" limit 1"); + + status += selectMergeSp->prepare(); + + // prepare statement to mark wasted DVB events + + // update events set delflg = ?, updsp = ? + // where channelid = ? and source = ? + // and starttime+duration > ? + // and starttime < ? + // and (tableid > ? or (tableid = ? and version <> ?)) + + endTime = new cDbValue("starttime+duration", cDBS::ffInt, 10); + updateDelFlg = new cDbStatement(eventsDb); + + updateDelFlg->build("update %s set ", eventsDb->TableName()); + updateDelFlg->bind("DelFlg", cDBS::bndIn | cDBS::bndSet); + updateDelFlg->bind("UpdFlg", cDBS::bndIn | cDBS::bndSet, ", "); + updateDelFlg->bind("UpdSp", cDBS::bndIn | cDBS::bndSet, ", "); + updateDelFlg->build(" where "); + updateDelFlg->bind("ChannelId", cDBS::bndIn | cDBS::bndSet); + updateDelFlg->bind("Source", cDBS::bndIn | cDBS::bndSet, " and "); + updateDelFlg->bindCmp(0, endTime, ">" , " and "); + updateDelFlg->bindCmp(0, "StartTime", 0, "<" , " and "); + updateDelFlg->bindCmp(0, "TableId", 0, ">" , " and ("); + updateDelFlg->bindCmp(0, "TableId", 0, "=" , " or ("); + updateDelFlg->bindCmp(0, "Version", 0, "<>" , " and "); + updateDelFlg->build("));"); + + status += updateDelFlg->prepare(); + + // we need the same as select :( + + selectDelFlg = new cDbStatement(eventsDb); + + selectDelFlg->build("select "); + selectDelFlg->bind("EventId", cDBS::bndOut); + selectDelFlg->build(" from %s where ", eventsDb->TableName()); + selectDelFlg->bind("ChannelId", cDBS::bndIn | cDBS::bndSet); + selectDelFlg->bind("Source", cDBS::bndIn | cDBS::bndSet, " and "); + selectDelFlg->bindCmp(0, endTime, ">" , " and "); + selectDelFlg->bindCmp(0, "StartTime", 0, "<" , " and "); + selectDelFlg->bindCmp(0, "TableId", 0, ">" , " and ("); + selectDelFlg->bindCmp(0, "TableId", 0, "=" , " or ("); + selectDelFlg->bindCmp(0, "Version", 0, "<>" , " and "); + selectDelFlg->build("));"); + + status += selectDelFlg->prepare(); + + // ----------------- + // delete from components where eventid = ?; + + delCompOf = new cDbStatement(compDb); + + delCompOf->build("delete from %s where ", compDb->TableName()); + delCompOf->bind("EventId", cDBS::bndIn | cDBS::bndSet); + delCompOf->build(";"); + + status += delCompOf->prepare(); + + if (status == success) + { + status += updateMemList(); + status += updateExternalIdsMap(); + } + + initialized = yes; + return status; + } + + int exitDb() + { + initialized = no; + + if (connection) + { + delete endTime; endTime = 0; + delete updateDelFlg; updateDelFlg = 0; + delete selectDelFlg; selectDelFlg = 0; + delete delCompOf; delCompOf = 0; + + delete vdrDb; vdrDb = 0; + delete eventsDb; eventsDb = 0; + delete compDb; compDb = 0; + delete mapDb; mapDb = 0; + delete selectMergeSp; selectMergeSp = 0; + delete selectEventByStarttime; selectEventByStarttime = 0; + + delete connection; connection = 0; + } + + return done; + } + + int updateMemList() + { + time_t start = time(0); + + evtMemList.clear(); + + // select eventid, channelid, version, tableid, delflg + // from events where source = 'vdr' + + cDbStatement* selectAllVdrEvents = new cDbStatement(eventsDb); + + selectAllVdrEvents->build("select "); + selectAllVdrEvents->bind("EventId", cDBS::bndOut); + selectAllVdrEvents->bind("ChannelId", cDBS::bndOut, ", "); + selectAllVdrEvents->bind("Version", cDBS::bndOut, ", "); + selectAllVdrEvents->bind("TableId", cDBS::bndOut, ", "); + selectAllVdrEvents->bind("DelFlg", cDBS::bndOut, ", "); + selectAllVdrEvents->build(" from %s where source = 'vdr'", eventsDb->TableName()); + + if (selectAllVdrEvents->prepare() != success) + { + tell(0, "Handler: Aborted reading hashes from db due to prepare error"); + delete selectAllVdrEvents; + return fail; + } + + tell(1, "Handler: Start reading hashes from db"); + + eventsDb->clear(); + + for (int f = selectAllVdrEvents->find(); f; f = selectAllVdrEvents->fetch()) + { + char evtKey[100]; + + if (eventsDb->hasValue("DelFlg", "Y")) + continue; + + sprintf(evtKey, "%ld:%s", + eventsDb->getIntValue("STARTTIME"), + eventsDb->getStrValue("CHANNELID")); + + evtMemList[evtKey].version = eventsDb->getIntValue("Version"); + evtMemList[evtKey].tableid = eventsDb->getIntValue("TableId"); + + tell(4, "Handler: cInsert: '%s' with %d/%d", evtKey, evtMemList[evtKey].tableid, evtMemList[evtKey].version); + } + + selectAllVdrEvents->freeResult(); + delete selectAllVdrEvents; + + tell(1, "Handler: Finished reading hashes from db, got %d hashes (in %ld seconds)", + (int)evtMemList.size(), time(0)-start); + + return success; + } + + //*************************************************************************** + // Ignore Channel + //*************************************************************************** + + virtual bool IgnoreChannel(const cChannel* Channel) + { + static time_t nextRetryAt = time(0); + LogDuration l("IgnoreChannel", 5); + + // if this method is called the channel is + // for the db an we have the active role! + // (already checked by cEpg2VdrEpgHandler) + + // only check the DB connection here!! + + if (!dbConnected() && time(0) < nextRetryAt) + return true; + + if (checkConnection() != success || externIdMap.size() < 1) + { + nextRetryAt = time(0) + 60; + return true; + } + + return false; + } + + //*************************************************************************** + // Transaction Stuff + //*************************************************************************** + + virtual bool BeginSegmentTransfer(const cChannel* Channel, bool dummy) + { + // inital die channelid setzen + + channelId = Channel->GetChannelID(); + + return false; + } + + //*************************************************************************** + // Handled Externally + // hier wird festgelegt ob das Event via VDR im EPG landen soll + //*************************************************************************** + + virtual bool HandledExternally(const cChannel* Channel) + { + static int reportFirst = yes; + + LogDuration l("HandledExternally", 5); + + if (!channelId.Valid()) + { + // if 'Channel' valid and 'channelId' not valid assume SegmentTransfer patch is missing + // -> report this .. + + if (Channel->GetChannelID().Valid() && reportFirst) + { + reportFirst = no; + tell(0, "Handler: WARNING: Assume 'SegmentTransfer' patch is missing, " + "VDR < 2.1.0 have to be patched! Or disable the handler in the plugin setup"); + } + + return false; + } + + // dbConnected() here okay -> if not connected we return false an the vdr will handle the event?!? + + if (dbConnected() && getExternalIdOfChannel(&channelId) != "") + return true; + + return false; + } + + virtual bool EndSegmentTransfer(bool Modified, bool dummy) + { + if (dummy || !dbConnected() || !connection->inTransaction()) + return false; + + if (Modified) + connection->commit(); + else + connection->rollback(); + + if (Epg2VdrConfig.loglevel > 2) + connection->showStat("handler"); + + return false; + } + + //*************************************************************************** + // Is Update + //*************************************************************************** + + virtual bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) + { + char evtKey[100]; + LogDuration l("IsUpdate", 5); + + if (!dbConnected()) + return false; + + if (!isZero(getExternalIdOfChannel(&channelId).c_str()) && StartTime > time(0) + 4 * tmeSecondsPerDay) + return false; + + sprintf(evtKey, "%ld:%s", StartTime, (const char*)channelId.ToString()); + + if (evtMemList.find(evtKey) == evtMemList.end()) + { + tell(4, "Handler: Handle insert (or starttime update) of event '%s' (%d) for channel '%s'", + evtKey, EventID, (const char*)channelId.ToString()); + + if (!connection->inTransaction()) + connection->startTransaction(); + + return true; + } + + uchar currentTableId = ::max(uchar(evtMemList[evtKey].tableid), uchar(0x4E)); + + // skip bigger ids as current + + if (TableID > currentTableId) + { + tell(4, "Handler: Ignoring update with older tableid (%d) for event '%s' (%d)(has tableid %d)", + TableID, evtKey, EventID, evtMemList[evtKey].tableid); + return false; + } + + // skip if version an tid identical + + if (currentTableId == TableID && evtMemList[evtKey].version == Version) + { + tell(4, "Handler: Ignoring 'non' update for event '%s' (%d), version still (%d)", + evtKey, EventID, Version); + return false; + } + + if (Epg2VdrConfig.loglevel > 3) + tell(4, "Handler: Handle update of event '%s' (%d) %d/%d - %d/%d", evtKey, EventID, + Version, TableID, + evtMemList[evtKey].version, evtMemList[evtKey].tableid); + + if (!connection->inTransaction()) + connection->startTransaction(); + + return true; + } + + //*************************************************************************** + // Handle Event + //*************************************************************************** + + virtual bool HandleEvent(cEvent* event) + { + int oldStartTime = 0; + + if (!dbConnected() || !event || !channelId.Valid()) + return false; + + LogDuration l("HandleEvent", 5); + + // External-ID: + // na -> Kanal nicht konfiguriert -> wird ignoriert + // = 0 -> wird vom Sender genommen -> und in der DB abgelegt + // > 0 -> echte externe ID -> wird von extern ins epg übertragen, + // das Sender EPG wird nur (zusätzlich) in der DB gehalten + + // Events der Kanäle welche nicht in der map zu finden sind ignorieren + + if (getExternalIdOfChannel(&channelId) == "") + return false; + + if (!connection->inTransaction()) + { + tell(0, "Handler: Error missing tact in HandleEvent"); + return false; + } + + std::string comp; + + // lookup the event .. + // first try by starttime + + eventsDb->clear(); + eventsDb->setValue("CHANNELID", channelId.ToString()); + eventsDb->setValue("STARTTIME", event->StartTime()); + + int insert = !selectEventByStarttime->find(); + + if (insert) + { + // try lookup by eventid + + eventsDb->setValue("CHANNELID", channelId.ToString()); + eventsDb->setBigintValue("EVENTID", (long)event->EventID()); + + if (eventsDb->find()) + { + // fount => NOT a insert, just a update of the starttime + + insert = no; + oldStartTime = eventsDb->getIntValue("STARTTIME"); + eventsDb->setValue("STARTTIME", event->StartTime()); + } + } + + // reinstate ?? + + if (eventsDb->hasValue("DELFLG", "Y")) + { + char updFlg = Us::usPassthrough; + + mapDb->clear(); + mapDb->setValue("CHANNELID", channelId.ToString()); + + if (selectMergeSp->find()) + { + time_t mergesp = mapDb->getIntValue("MERGESP"); + long masterid = eventsDb->getIntValue("MASTERID"); + long useid = eventsDb->getIntValue("USEID"); + + if (event->StartTime() > mergesp) + updFlg = Us::usRemove; + else if (event->StartTime() <= mergesp && masterid == useid) + updFlg = Us::usActive; + else if (event->StartTime() <= mergesp && masterid != useid) + updFlg = Us::usLink; + } + + eventsDb->setCharValue("UPDFLG", updFlg); + eventsDb->getValue("DELFLG")->setNull(); + } + + if (!insert && abs(event->StartTime() - eventsDb->getIntValue("StartTime")) > 6*tmeSecondsPerHour) + { + tell(3, "Handler: Info: Start time of %d/%s - '%s' moved %ld hours from %s to %s - '%s'", + event->EventID(), (const char*)channelId.ToString(), + eventsDb->getStrValue("Title"), + (event->StartTime() - eventsDb->getIntValue("StartTime")) / tmeSecondsPerHour, + l2pTime(eventsDb->getIntValue("StartTime")).c_str(), + l2pTime(event->StartTime()).c_str(), + event->Title()); + } + + time_t end = event->StartTime() + event->Duration(); + + if (!insert + && end < time(0) - 2*tmeSecondsPerHour + && eventsDb->getIntValue("StartTime") > time(0) + && event->StartTime() < eventsDb->getIntValue("StartTime")) + { + tell(1, "Handler: Info: Got update of %d/%s with startime more than 2h in past " + "(%s/%d) before (%s), ignoring update, set delflg instead", + event->EventID(), (const char*)channelId.ToString(), + l2pTime(event->StartTime()).c_str(), event->Duration(), + l2pTime(eventsDb->getIntValue("StartTime")).c_str()); + + eventsDb->setValue("DelFlg", "Y"); + eventsDb->setCharValue("UpdFlg", Us::usDelete); + } + else + { + eventsDb->setValue("StartTime", event->StartTime()); + } + + if (!insert && eventsDb->getIntValue("VPS") != event->Vps()) + tell(1, "Handler: Toggle vps flag for '%s' at '%s' from %s to %s", + event->Title(), (const char*)channelId.ToString(), + l2pTime(eventsDb->getIntValue("VPS")).c_str(), l2pTime(event->Vps()).c_str()); + + eventsDb->setValue("Source", "vdr"); + eventsDb->setValue("TableId", event->TableID()); + eventsDb->setValue("Version", event->Version()); + eventsDb->setValue("Title", event->Title()); + eventsDb->setValue("LongDescription", event->Description()); + eventsDb->setValue("Duration", event->Duration()); + eventsDb->setValue("ParentalRating", event->ParentalRating()); + eventsDb->setValue("Vps", event->Vps()); + + if (!isEmpty(event->ShortText())) + eventsDb->setValue("ShortText", event->ShortText()); + + // contents + + char contents[MaxEventContents * 10] = ""; + + for (int i = 0; i < MaxEventContents; i++) + if (event->Contents(i) > 0) + sprintf(eos(contents), "0x%x,", event->Contents(i)); + + if (!isEmpty(contents)) + eventsDb->setValue("CONTENTS", contents); + + // components .. + + compDb->clear(); + compDb->setBigintValue("EVENTID", eventsDb->getBigintValue("EVENTID")); + delCompOf->execute(); + + if (event->Components()) + { + for (int i = 0; i < event->Components()->NumComponents(); i++) + { + tComponent* p = event->Components()->Component(i); + + compDb->clear(); + compDb->setBigintValue("EventId", eventsDb->getBigintValue("EVENTID")); + compDb->setValue("ChannelId", channelId.ToString()); + compDb->setValue("Stream", p->stream); + compDb->setValue("Type", p->type); + compDb->setValue("Lang", p->language); + compDb->setValue("Description", p->description ? p->description : ""); + compDb->store(); + } + } + + // compressed .. + + if (event->Title()) + { + comp = event->Title(); + prepareCompressed(comp); + eventsDb->setValue("COMPTITLE", comp.c_str()); + } + + if (!isEmpty(event->ShortText())) + { + comp = event->ShortText(); + prepareCompressed(comp); + eventsDb->setValue("COMPSHORTTEXT", comp.c_str()); + } + + if (!isEmpty(event->Description())) + { + comp = event->Description(); + prepareCompressed(comp); + eventsDb->setValue("COMPLONGDESCRIPTION", comp.c_str()); + } + + if (insert) + { + eventsDb->setValue("UseId", 0L); + eventsDb->setCharValue("UpdFlg", Us::usPassthrough); // default (vdr:000 events) + + mapDb->clear(); + mapDb->setValue("ChannelId", channelId.ToString()); + + if (selectMergeSp->find()) + { + // vdr event for merge with external event + + time_t mergesp = mapDb->getIntValue("MergeSp"); + eventsDb->setCharValue("UpdFlg", event->StartTime() > mergesp ? Us::usInactive : Us::usActive); + } + + eventsDb->insert(); + } + else + { + eventsDb->update(); + } + + if (!insert && oldStartTime) + { + char evtKey[100]; + sprintf(evtKey, "%ld:%s", (long)oldStartTime, (const char*)channelId.ToString()); + evtMemList.erase(evtKey); + tell(4, "Handler: cRemove: '%s' (due to starttime update)", evtKey); + } + + // update hash map + + char evtKey[100]; + + sprintf(evtKey, "%ld:%s", (long)event->StartTime(), (const char*)channelId.ToString()); + + evtMemList[evtKey].version = event->Version(); + evtMemList[evtKey].tableid = event->TableID(); + + tell(4, "Handler: cUpdate/cInsert: '%s' to %d/%d", evtKey, evtMemList[evtKey].tableid, evtMemList[evtKey].version); + + selectEventByStarttime->freeResult(); + + return true; + } + + //*************************************************************************** + // Drop Outdated + //*************************************************************************** + + virtual bool DropOutdated(cSchedule* Schedule, time_t SegmentStart, + time_t SegmentEnd, uchar TableID, uchar Version) + { + // we handle only vdr events here (provided by DVB) + + if (!dbConnected() || getExternalIdOfChannel(&channelId) == "") + return false; + + if (!connection->inTransaction()) + { + tell(0, "Handler: Error missing tact DropOutdated"); + return false; + } + + if (SegmentStart <= 0 || SegmentEnd <= 0) + return false; + + LogDuration l("DropOutdated", 5); + + eventsDb->clear(); + eventsDb->setValue("ChannelId", Schedule->ChannelID().ToString()); + eventsDb->setValue("Source", "vdr"); + eventsDb->setValue("UpdSp", time(0)); + eventsDb->setValue("StartTime", SegmentEnd); + eventsDb->setValue("TableId", TableID); + eventsDb->setValue("Version", Version); + endTime->setValue(SegmentStart); + + // remove segment from cache + + for (int f = selectDelFlg->find(); f; f = selectDelFlg->fetch()) + { + char evtKey[100]; + + sprintf(evtKey, "%ld:%s", + eventsDb->getIntValue("STARTTIME"), + eventsDb->getStrValue("CHANNELID")); + + evtMemList.erase(evtKey); + tell(4, "Handler: cRemove: '%s'", evtKey); + } + + selectDelFlg->freeResult(); + + eventsDb->setValue("DELFLG", "Y"); + eventsDb->setCharValue("UPDFLG", Us::usDelete); + + // mark segment as deleted + + updateDelFlg->execute(); + + return true; + } + + private: + + std::string getExternalIdOfChannel(tChannelID* channelId) + { + char* id = 0; + std::string extid = ""; + + if (externIdMap.size() < 1) + return ""; + + id = strdup(channelId->ToString()); + + if (externIdMap.find(id) != externIdMap.end()) + extid = externIdMap[id]; + + free(id); + + return extid; + } + + int updateExternalIdsMap() + { + externIdMap.clear(); + tell(1, "Handler: Start reading external ids from db"); + mapDb->clear(); + + // select extid, channelid + // from channelmap + + cDbStatement* selectAll = new cDbStatement(mapDb); + + selectAll->build("select "); + selectAll->bind("ExternalId", cDBS::bndOut); + selectAll->bind("ChannelId", cDBS::bndOut, ", "); + selectAll->build(" from %s", mapDb->TableName()); + + if (selectAll->prepare() != success) + { + tell(0, "Handler: Reading external id's from db aborted due to prepare error"); + delete selectAll; + return fail; + } + + for (int f = selectAll->find(); f; f = selectAll->fetch()) + { + const char* extid = mapDb->getStrValue("ExternalId"); + const char* chan = mapDb->getStrValue("ChannelId"); + + externIdMap[chan] = extid; + } + + tell(1, "Handler: Finished reading external id's from db, got %d id's", + (int)externIdMap.size()); + + selectAll->freeResult(); + delete selectAll; + + return success; + } + + struct MemMap + { + int version; + int tableid; + }; + + std::map<std::string,MemMap> evtMemList; + std::map<std::string,std::string> externIdMap; + tChannelID channelId; + + int initialized; + cDbConnection* connection; + + cDbTable* eventsDb; + cDbTable* mapDb; + cDbTable* vdrDb; + cDbTable* compDb; + + cDbValue* endTime; + cDbStatement* updateDelFlg; + cDbStatement* selectDelFlg; + cDbStatement* delCompOf; + cDbStatement* selectMergeSp; + cDbStatement* selectEventByStarttime; + + cUpdate* update; +}; + +//*************************************************************************** +// cEpg2VdrEpgHandler +//*************************************************************************** + +class cEpg2VdrEpgHandler : public cEpgHandler +{ + public: + + cEpg2VdrEpgHandler() : cEpgHandler() { active = no; } + + ~cEpg2VdrEpgHandler() + { + handlerMutex.lock(); + + std::map<tThreadId,cEpgHandlerInstance*>::iterator it; + + for (it = handler.begin(); it != handler.end(); ++it) + { + delete handler[it->first]; + handler[it->first] = 0; + } + + handlerMutex.unlock(); + } + + int getActive() { return active; } + void setActive(int state) { active = state; } + + //*************************************************************************** + // Ignore Channel + // - includes the NOEPG feature - so we don't need the noepg plugin + //*************************************************************************** + + virtual bool IgnoreChannel(const cChannel* Channel) + { + // IgnoreChannel ist der erste Anlaufpunkt des EIT handlers, + // wird hier ignoriert bricht die Verarbeitung des Kanals direkt ab + + tChannelID tmp = Channel->GetChannelID(); + + if (externIdMap.size() < 1) + return true; + + // Kanäle welche nicht in der map konfiguriert sind (na) werden nicht in der DB verwaltet + // abhängig von blacklist durchgelassen oder ignoriert + + if (getExternalIdOfChannel(&tmp) == "") + return Epg2VdrConfig.blacklist; + + // ingnore - wenn nicht aktiv + + if (!active) + return true; + + // Handler check - only to check if the DB connection is fine + + if (getHandler()->IgnoreChannel(Channel)) + return true; + + return false; + } + + virtual bool HandledExternally(const cChannel* Channel) + { + return getHandler()->HandledExternally(Channel); + } + + virtual bool BeginSegmentTransfer(const cChannel *Channel, bool dummy) + { + // solange die Datenbank mit einem anderen handler thread + // beschäftigt ist (ergo wir den lock nicht bekommen) erst mal ignorieren + + if (!handlerMutex.tryLock()) + return false; + + getHandler()->BeginSegmentTransfer(Channel, dummy); + + return true; + } + + virtual bool EndSegmentTransfer(bool Modified, bool dummy) + { + handlerMutex.unlock(); + getHandler()->EndSegmentTransfer(Modified, dummy); + return false; + } + + virtual bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) + { + + cEpgHandlerInstance* h = getHandler(); + return h->IsUpdate(EventID, StartTime, TableID, Version); + } + + virtual bool HandleEvent(cEvent* event) + { + cEpgHandlerInstance* h = getHandler(); + return h->HandleEvent(event); + } + + virtual bool DropOutdated(cSchedule* Schedule, time_t SegmentStart, + time_t SegmentEnd, uchar TableID, uchar Version) + { + cEpgHandlerInstance* h = getHandler(); + return h->DropOutdated(Schedule, SegmentStart, SegmentEnd, TableID, Version); + } + + int updateExternalIdsMap(cDbTable* mapDb) + { + cMutexLock lock(&mapMutex); + + externIdMap.clear(); + tell(1, "Handler: Start reading external ids from db"); + mapDb->clear(); + + // select extid, channelid + // from channelmap + + cDbStatement* selectAll = new cDbStatement(mapDb); + + selectAll->build("select "); + selectAll->bind("EXTERNALID", cDBS::bndOut); + selectAll->bind("CHANNELID", cDBS::bndOut, ", "); + selectAll->bind("SOURCE", cDBS::bndOut, ", "); + selectAll->bind("CHANNELNAME", cDBS::bndOut, ", "); + selectAll->bind("FORMAT", cDBS::bndOut, ", "); + selectAll->bind("MERGE", cDBS::bndOut, ", "); + selectAll->build(" from %s", mapDb->TableName()); + + if (selectAll->prepare() != success) + { + tell(0, "Handler: Reading external id's from db aborted due to prepare error"); + delete selectAll; + return fail; + } + + // channel lock scope + { +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; +#else + cChannels* channels = &Channels; +#endif + + for (int f = selectAll->find(); f; f = selectAll->fetch()) + { + std::string extid = mapDb->getStrValue("ExternalId"); + + const char* strChannelId = mapDb->getStrValue("ChannelId"); + const cChannel* channel = channels->GetByChannelID(tChannelID::FromString(strChannelId)); + + // update channelname in channelmap + + if (channel && !isEmpty(channel->Name()) && + (!mapDb->hasValue("CHANNELNAME", channel->Name()) || mapDb->getValue("FORMAT")->isNull())) + { + mapDb->find(); // get all fields from table (needed for update)! + + mapDb->setValue("CHANNELNAME", channel->Name()); + + if (strstr(channel->Name(), CHANNELMARKOBSOLETE)) + mapDb->setValue("UNKNOWNATVDR", yes); + + if (strstr(channel->Name(), "HD")) + mapDb->setValue("FORMAT", "HD"); + else if (strstr(channel->Name(), "3D")) + mapDb->setValue("FORMAT", "3D"); + else + mapDb->setValue("FORMAT", "SD"); + + mapDb->update(); + mapDb->reset(); + } + + // we should get the merge > 1 channels already via a merge 1 entry of the channelmap! + + if (mapDb->getIntValue("Merge") > 1) + continue; + + // insert into map + + externIdMap[strChannelId] = extid; + } + } + + tell(1, "Handler: Finished reading external id's from db, got %d id's", + (int)externIdMap.size()); + + selectAll->freeResult(); + delete selectAll; + + return success; + } + + static cEpg2VdrEpgHandler* getSingleton() + { + if (!singleton) + singleton = new cEpg2VdrEpgHandler(); + + return singleton; + } + + private: + + std::string getExternalIdOfChannel(tChannelID* channelId) + { + char* id = 0; + std::string extid = ""; + + cMutexLock lock(&mapMutex); + + if (externIdMap.size() < 1) + return ""; + + id = strdup(channelId->ToString()); + + if (externIdMap.find(id) != externIdMap.end()) + extid = externIdMap[id]; + + free(id); + + return extid; + } + + cEpgHandlerInstance* getHandler() + { + if (handler.find(cThread::ThreadId()) == handler.end()) + handler[cThread::ThreadId()] = new cEpgHandlerInstance(); + + return handler[cThread::ThreadId()]; + } + + std::map<std::string,std::string> externIdMap; + std::map<tThreadId,cEpgHandlerInstance*> handler; + int active; + cMutex mapMutex; + cMutexTry handlerMutex; + + static cEpg2VdrEpgHandler* singleton; +}; + +//*************************************************************************** +#endif // __HANDLER_H diff --git a/lib/Makefile b/lib/Makefile new file mode 100644 index 0000000..0fe3774 --- /dev/null +++ b/lib/Makefile @@ -0,0 +1,108 @@ +# +# Makefile +# +# See the README file for copyright information and how to reach the author. +# + +include ../Make.config + +LIBTARGET = libhorchi +HLIB = -L. -lhorchi + +DEMO = demo +TEST = tst + +LIBOBJS = common.o config.o db.o epgservice.o dbdict.o json.o + +ifdef USEJPEG + LIBOBJS += imgtools.o +endif + +ifdef USECURL + LIBOBJS += curl.o configuration.o thread.o +endif + +BASELIBS = -lrt -lz -luuid +BASELIBS += $(shell mysql_config --libs_r) + +ifdef USECURL + BASELIBS += -lcurl +endif + +ifdef USEEPGS + LIBOBJS += searchtimer.o +endif + +ifdef USEPYTHON + BASELIBS += $(shell python-config --libs) + LIBOBJS += python.o +endif + +ifdef USELIBXML + BASELIBS += $(shell xml2-config --libs) $(shell xslt-config --libs) +endif + +ifdef SYSD_NOTIFY + BASELIBS += $(shell pkg-config --libs libsystemd-daemon) + CFLAGS += $(shell pkg-config --cflags libsystemd-daemon) +endif + +ifdef DEBUG + CFLAGS += -ggdb -O0 +endif + +CFLAGS += $(shell mysql_config --include) +DEFINES += $(USES) + +ifdef USEPYTHON + CFLAGS += $(shell python-config --includes) +endif + +all: lib $(TEST) $(DEMO) +lib: $(LIBTARGET).a + +$(LIBTARGET).a : $(LIBOBJS) + @echo Building Lib ... + $(doLib) $@ $(LIBOBJS) + +tst: test.o lib + $(doLink) test.o $(HLIB) -larchive -lcrypto $(BASELIBS) -o $@ + +demo: demo.o lib + $(doLink) demo.o $(HLIB) -larchive -lcrypto $(BASELIBS) -o $@ + +pytst: pytst.c python.c python.h hlib + $(CC) $(CFLAGS) pytst.c python.c -L./lib -lhorchi $(DLIBS) -o pytst + +clean: + rm -f *.o *~ core $(TEST) $(DEMO) $(LIBTARGET).a + +cppchk: + cppcheck --template="{file}:{line}:{severity}:{message}" --quiet --force *.c *.h + +%.o: %.c + @echo Compile "$(*F)" ... + $(doCompile) $(*F).c -o $@ + +#-------------------------------------------------------- +# dependencies +#-------------------------------------------------------- + +HEADER = db.h common.h config.h dbdict.h + +common.o : common.c $(HEADER) common.h +configuration.o : configuration.c $(HEADER) configuration.h +thread.o : thread.c $(HEADER) thread.h +curl.o : curl.c $(HEADER) curl.h +imgtools.o : imgtools.c $(HEADER) imgtools.h +config.o : config.c $(HEADER) config.h +db.o : db.c $(HEADER) db.h +epgservice.o : epgservice.c $(HEADER) epgservice.h +dbdict.o : dbdict.c $(HEADER) dbdict.h +json.o : json.c $(HEADER) json.h +python.o : python.c $(HEADER) python.h +searchtimer.o : searchtimer.c $(HEADER) searchtimer.h + +demo.o : demo.c $(HEADER) +test.o : test.c $(HEADER) + diff --git a/lib/common.c b/lib/common.c new file mode 100644 index 0000000..8a0e9ac --- /dev/null +++ b/lib/common.c @@ -0,0 +1,1921 @@ +/* + * common.c: + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <sys/stat.h> +#include <sys/time.h> + +#include <sys/ioctl.h> +#include <net/if.h> +#include <arpa/inet.h> + +#ifdef USEUUID +# include <uuid/uuid.h> +#endif + +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> +#include <errno.h> +#include <regex.h> +#include <limits.h> + +#ifdef USELIBARCHIVE +# include <archive.h> +# include <archive_entry.h> +#endif + +#include "common.h" +#include "config.h" + +cMyMutex logMutex; + +//*************************************************************************** +// Tell +//*************************************************************************** + +const char* getLogPrefix() +{ + return logPrefix; +} + +void tell(int eloquence, const char* format, ...) +{ + if (cEpgConfig::loglevel < eloquence) + return ; + + logMutex.Lock(); + +#ifndef VDR_PLUGIN + static int init = no; + + if (!init) + { + init = yes; + openlog(cEpgConfig::logName, LOG_CONS, cEpgConfig::logFacility); + } +#endif + + const int sizeBuffer = 100000; + char t[sizeBuffer+100]; *t = 0; + va_list ap; + + va_start(ap, format); + + if (getLogPrefix()) + snprintf(t, sizeBuffer, "%s", getLogPrefix()); + + vsnprintf(t+strlen(t), sizeBuffer-strlen(t), format, ap); + + strReplace(t, '\n', '$'); + + if (cEpgConfig::logstdout) + { + char buf[50+TB]; + timeval tp; + + gettimeofday(&tp, 0); + + tm* tm = localtime(&tp.tv_sec); + + sprintf(buf,"%2.2d:%2.2d:%2.2d,%3.3ld ", + tm->tm_hour, tm->tm_min, tm->tm_sec, + tp.tv_usec / 1000); + + printf("%s %s\n", buf, t); + } + else + syslog(LOG_ERR, "%s", t); + + logMutex.Unlock(); + + va_end(ap); +} + +//*************************************************************************** +// Syslog Facilities +//*************************************************************************** + +const Syslog::Facilities Syslog::facilities[] = +{ + { "auth", LOG_AUTH }, + { "cron", LOG_CRON }, + { "daemon", LOG_DAEMON }, + { "kern", LOG_KERN }, + { "lpr", LOG_LPR }, + { "mail", LOG_MAIL }, + { "news", LOG_NEWS }, + { "security", LOG_AUTH }, + { "syslog", LOG_SYSLOG }, + { "user", LOG_USER }, + { "uucp", LOG_UUCP }, + { "local0", LOG_LOCAL0 }, + { "local1", LOG_LOCAL1 }, + { "local2", LOG_LOCAL2 }, + { "local3", LOG_LOCAL3 }, + { "local4", LOG_LOCAL4 }, + { "local5", LOG_LOCAL5 }, + { "local6", LOG_LOCAL6 }, + { "local7", LOG_LOCAL7 }, + { 0, 0 } +}; + +//*************************************************************************** +// To Name +//*************************************************************************** + +char* Syslog::toName(int code) +{ + for (int i = 0; facilities[i].name; i++) + if (facilities[i].code == code) + return (char*) facilities[i].name; // #83 + + return 0; +} + +//*************************************************************************** +// To Code +//*************************************************************************** + +int Syslog::toCode(const char* name) +{ + for (int i = 0; facilities[i].name; i++) + if (strcmp(facilities[i].name, name) == 0) + return facilities[i].code; + + return na; +} + +//*************************************************************************** +// Save Realloc +//*************************************************************************** + +char* srealloc(void* ptr, size_t size) +{ + void* n = realloc(ptr, size); + + if (!n) + { + free(ptr); + ptr = 0; + } + + return (char*)n; +} + +//*************************************************************************** +// us now +//*************************************************************************** + +double usNow() +{ + struct timeval tp; + + gettimeofday(&tp, 0); + + return tp.tv_sec * 1000000.0 + tp.tv_usec; +} + +//*************************************************************************** +// Host ID +//*************************************************************************** + +unsigned int getHostId() +{ + static unsigned int id = gethostid() & 0xFFFFFFFF; + return id; +} + +//*************************************************************************** +// String Operations +//*************************************************************************** + +void toUpper(std::string& str) +{ + const char* s = str.c_str(); + int lenSrc = str.length(); + + char* dest = (char*)malloc(lenSrc+TB); *dest = 0; + char* d = dest; + + int csSrc; // size of character + + for (int ps = 0; ps < lenSrc; ps += csSrc) + { + csSrc = std::max(mblen(&s[ps], lenSrc-ps), 1); + + if (csSrc == 1) + *d++ = toupper(s[ps]); + else if (csSrc == 2 && s[ps] == (char)0xc3 && s[ps+1] >= (char)0xa0) + { + *d++ = s[ps]; + *d++ = s[ps+1] - 32; + } + else + { + for (int i = 0; i < csSrc; i++) + *d++ = s[ps+i]; + } + } + + *d = 0; + + str = dest; + free(dest); +} + +//*************************************************************************** +// To Case (UTF-8 save) +//*************************************************************************** + +const char* toCase(Case cs, char* str) +{ + char* s = str; + int lenSrc = strlen(str); + + int csSrc; // size of character + + for (int ps = 0; ps < lenSrc; ps += csSrc) + { + csSrc = std::max(mblen(&s[ps], lenSrc-ps), 1); + + if (csSrc == 1) + s[ps] = cs == cUpper ? toupper(s[ps]) : tolower(s[ps]); + else if (csSrc == 2 && s[ps] == (char)0xc3 && s[ps+1] >= (char)0xa0) + { + s[ps] = s[ps]; + s[ps+1] = cs == cUpper ? toupper(s[ps+1]) : tolower(s[ps+1]); + } + else + { + for (int i = 0; i < csSrc; i++) + s[ps+i] = s[ps+i]; + } + } + + return str; +} + +void removeChars(std::string& str, const char* ignore) +{ + const char* s = str.c_str(); + int lenSrc = str.length(); + int lenIgn = strlen(ignore); + + char* dest = (char*)malloc(lenSrc+TB); *dest = 0; + char* d = dest; + + int csSrc; // size of character + int csIgn; // + + for (int ps = 0; ps < lenSrc; ps += csSrc) + { + int skip = no; + + csSrc = std::max(mblen(&s[ps], lenSrc-ps), 1); + + for (int pi = 0; pi < lenIgn; pi += csIgn) + { + csIgn = std::max(mblen(&ignore[pi], lenIgn-pi), 1); + + if (csSrc == csIgn && strncmp(&s[ps], &ignore[pi], csSrc) == 0) + { + skip = yes; + break; + } + } + + if (!skip) + { + for (int i = 0; i < csSrc; i++) + *d++ = s[ps+i]; + } + } + + *d = 0; + + str = dest; + free(dest); +} + +void removeCharsExcept(std::string& str, const char* except) +{ + const char* s = str.c_str(); + int lenSrc = str.length(); + int lenIgn = strlen(except); + + char* dest = (char*)malloc(lenSrc+TB); *dest = 0; + char* d = dest; + + int csSrc; // size of character + int csIgn; // + + for (int ps = 0; ps < lenSrc; ps += csSrc) + { + int skip = yes; + + mblen(0,0); + csSrc = std::max(mblen(&s[ps], lenSrc-ps), 1); + + for (int pi = 0; pi < lenIgn; pi += csIgn) + { + csIgn = std::max(mblen(&except[pi], lenIgn-pi), 1); + + if (csSrc == csIgn && strncmp(&s[ps], &except[pi], csSrc) == 0) + { + skip = no; + break; + } + } + + if (!skip) + { + for (int i = 0; i < csSrc; i++) + *d++ = s[ps+i]; + } + } + + *d = 0; + + str = dest; + free(dest); +} + +void removeWord(std::string& pattern, std::string word) +{ + size_t pos; + + if ((pos = pattern.find(word)) != std::string::npos) + pattern.swap(pattern.erase(pos, word.length())); +} + +//*************************************************************************** +// String Manipulation +//*************************************************************************** + +void prepareCompressed(std::string& pattern) +{ + // const char* ignore = " (),.;:-_+*!#?=&%$<>§/'`´@~\"[]{}"; + const char* notignore = "ABCDEFGHIJKLMNOPQRSTUVWXYZßÖÄÜöäü0123456789"; + + toUpper(pattern); + removeWord(pattern, " TEIL "); + removeWord(pattern, " FOLGE "); + removeCharsExcept(pattern, notignore); +} + +//*************************************************************************** +// Get Range Parts of String like '33-123' or '-123' or '21-' +//*************************************************************************** + +int rangeFrom(const char* s) +{ + return atoi(s); +} + +int rangeTo(const char* s) +{ + int to = INT_MAX; + + const char* p = strchr(s, '-'); + + if (p && *(p+1)) + to = atoi(p+1); + + return to; +} + +//*************************************************************************** +// Left Trim +//*************************************************************************** + +char* lTrim(char* buf) +{ + if (buf) + { + char *tp = buf; + + while (*tp && strchr("\n\r\t ",*tp)) + tp++; + + memmove(buf, tp, strlen(tp) +1); + } + + return buf; +} + +//************************************************************************* +// Right Trim +//************************************************************************* + +char* rTrim(char* buf) +{ + if (buf) + { + char *tp = buf + strlen(buf); + + while (tp >= buf && strchr("\n\r\t ",*tp)) + tp--; + + *(tp+1) = 0; + } + + return buf; +} + +//************************************************************************* +// All Trim +//************************************************************************* + +char* allTrim(char* buf) +{ + return lTrim(rTrim(buf)); +} + +std::string strReplace(const std::string& what, const std::string& with, const std::string& subject) +{ + std::string str = subject; + size_t pos = 0; + + while ((pos = str.find(what, pos)) != std::string::npos) + { + str.replace(pos, what.length(), with); + pos += with.length(); + } + + return str; +} + +std::string strReplace(const std::string& what, long with, const std::string& subject) +{ + char swith[100]; + + sprintf(swith, "%ld", with); + + return strReplace(what, swith, subject); +} + +std::string strReplace(const std::string& what, double with, const std::string& subject) +{ + char swith[100]; + + sprintf(swith, "%.2f", with); + + return strReplace(what, swith, subject); +} + +char* strReplace(char* buffer, char from, char to) +{ + char* p = buffer; + + while (*p) + { + if (*p == from) + *p = to; + + p++; + } + + return buffer; +} + +//*************************************************************************** +// Number to String +//*************************************************************************** + +std::string num2Str(int num) +{ + char txt[16]; + + snprintf(txt, sizeof(txt), "%d", num); + + return std::string(txt); +} + +//*************************************************************************** +// Long to Pretty Time +//*************************************************************************** + +std::string l2pTime(time_t t, const char* format) +{ + char txt[100]; + tm* tmp = localtime(&t); + + strftime(txt, sizeof(txt), format, tmp); + + return std::string(txt); +} + +std::string l2pDate(time_t t) +{ + char txt[30]; + tm* tmp = localtime(&t); + + strftime(txt, sizeof(txt), "%d.%m.%Y", tmp); + + return std::string(txt); +} + +//*************************************************************************** +// To HTTP Header Date Format +//*************************************************************************** + +std::string l2HttpTime(time_t t) +{ + char date[128+TB]; + tm now; + + static const char *const days[] = + { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + + static const char *const mons[] = + { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + + gmtime_r(&t, &now); + + sprintf(date, "%3s, %02u %3s %04u %02u:%02u:%02u GMT", + days[now.tm_wday % 7], + (unsigned int)now.tm_mday, + mons[now.tm_mon % 12], + (unsigned int)(1900 + now.tm_year), + (unsigned int)now.tm_hour, + (unsigned int)now.tm_min, + (unsigned int)now.tm_sec); + + return std::string(date); +} + +//*************************************************************************** +// Is Daylight Saving Time +//*************************************************************************** + +int isDST(time_t t) +{ + struct tm tm; + + if (!t) + t = time(0); + + localtime_r(&t, &tm); + tm.tm_isdst = -1; // force DST auto detect + mktime(&tm); + + return tm.tm_isdst; +} + +//*************************************************************************** +// Time of +//*************************************************************************** + +time_t timeOf(time_t t) +{ + struct tm tm; + + localtime_r(&t, &tm); + + tm.tm_year = 0; + tm.tm_mon = 0; + tm.tm_mday = 0; + tm.tm_wday = 0; + tm.tm_yday = 0; + tm.tm_isdst = -1; // force DST auto detect + + return mktime(&tm); +} + +//*************************************************************************** +// Weekday Of +//*************************************************************************** + +int weekdayOf(time_t t) +{ + struct tm tm_r; + int weekday = localtime_r(&t, &tm_r)->tm_wday; + + return weekday == 0 ? 6 : weekday - 1; // Monday is 0 +} + +//*************************************************************************** +// To Weekday Name +//*************************************************************************** + +const char* toWeekdayName(uint day) +{ + const char* dayNames[] = + { + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + 0 + }; + + if (day > 6) + return "<unknown>"; + + return dayNames[day]; +} + +//*************************************************************************** +// hhmm of +//*************************************************************************** + +time_t hhmmOf(time_t t) +{ + struct tm tm; + + localtime_r(&t, &tm); + + tm.tm_year = 0; + tm.tm_mon = 0; + tm.tm_mday = 0; + tm.tm_wday = 0; + tm.tm_yday = 0; + tm.tm_sec = 0; + tm.tm_isdst = -1; // force DST auto detect + + return mktime(&tm); +} + +//*************************************************************************** +// time_t to hhmm like '2015' +//*************************************************************************** + +int l2hhmm(time_t t) +{ + struct tm tm; + + localtime_r(&t, &tm); + + return tm.tm_hour * 100 + tm.tm_min; +} + +//*************************************************************************** +// HHMM to Pretty Time +//*************************************************************************** + +std::string hhmm2pTime(int hhmm) +{ + char txt[100]; + + sprintf(txt, "%02d:%02d", hhmm / 100, hhmm % 100); + + return std::string(txt); +} + +//*************************************************************************** +// Midnight of +//*************************************************************************** + +time_t midnightOf(time_t t) +{ + struct tm tm; + + localtime_r(&t, &tm); + + tm.tm_hour = 0; + tm.tm_min = 0; + tm.tm_sec = 0; + tm.tm_isdst = -1; // force DST auto detect + + return mktime(&tm); +} + +//*************************************************************************** +// MS to Duration +//*************************************************************************** + +std::string ms2Dur(uint64_t t) +{ + char txt[30]; + + int s = t / 1000; + int ms = t % 1000; + + if (s != 0) + snprintf(txt, sizeof(txt), "%d.%03d seconds", s, ms); + else + snprintf(txt, sizeof(txt), "%d ms", ms); + + return std::string(txt); +} + +//*************************************************************************** +// Char to Char-String +//*************************************************************************** + +const char* c2s(char c, char* buf) +{ + sprintf(buf, "%c", c); + + return buf; +} + +//*************************************************************************** +// End Of String +//*************************************************************************** + +char* eos(char* s) +{ + if (!s) + return 0; + + return s + strlen(s); +} + +//*************************************************************************** +// Store To File +//*************************************************************************** + +int storeToFile(const char* filename, const char* data, int size) +{ + FILE* fout; + + if ((fout = fopen(filename, "w+"))) + { + fwrite(data, sizeof(char), size, fout); + fclose(fout); + } + else + { + tell(0, "Error, can't store '%s' to filesystem '%s'", filename, strerror(errno)); + return fail; + } + + return success; +} + +//*************************************************************************** +// Load From File +//*************************************************************************** + +int loadFromFile(const char* infile, MemoryStruct* data) +{ + FILE* fin; + struct stat sb; + + data->clear(); + + if (!fileExists(infile)) + { + tell(0, "File '%s' not found'", infile); + return fail; + } + + if (stat(infile, &sb) < 0) + { + tell(0, "Can't get info of '%s', error was '%s'", infile, strerror(errno)); + return fail; + } + + if ((fin = fopen(infile, "r"))) + { + const char* sfx = suffixOf(infile); + + data->size = sb.st_size; + data->modTime = sb.st_mtime; + data->memory = (char*)malloc(data->size); + fread(data->memory, sizeof(char), data->size, fin); + fclose(fin); + sprintf(data->tag, "%ld", (long int)data->size); + + if (strcmp(sfx, "gz") == 0) + sprintf(data->contentEncoding, "gzip"); + + if (strcmp(sfx, "js") == 0) + sprintf(data->contentType, "application/javascript"); + + else if (strcmp(sfx, "png") == 0 || strcmp(sfx, "jpg") == 0 || strcmp(sfx, "gif") == 0) + sprintf(data->contentType, "image/%s", sfx); + else if (strcmp(sfx, "svg") == 0) + sprintf(data->contentType, "image/%s+xml", sfx); + else if (strcmp(sfx, "ico") == 0) + strcpy(data->contentType, "image/x-icon"); + + else + sprintf(data->contentType, "text/%s", sfx); + } + else + { + tell(0, "Error, can't open '%s' for reading, error was '%s'", infile, strerror(errno)); + return fail; + } + + return success; +} + +//*************************************************************************** +// TOOLS +//*************************************************************************** + +int isEmpty(const char* str) +{ + return !str || !*str; +} + +const char* notNull(const char* str, const char* def) +{ + if (!str) + return def; + + return str; +} + +int isZero(const char* str) +{ + const char* p = str; + + while (p && *p) + { + if (*p != '0') + return no; + + p++; + } + + return yes; +} + +//*************************************************************************** +// Is Member +//*************************************************************************** + +int isMember(const char** list, const char* item) +{ + const char** p; + int i; + + for (i = 0, p = list; *p; i++, p++) + if (strcmp(*p, item) == 0) + return i; + + return na; +} + +//*************************************************************************** +// +//*************************************************************************** + +char* sstrcpy(char* dest, const char* src, int max) +{ + if (!dest || !src) + return 0; + + strncpy(dest, src, max); + dest[max-1] = 0; + + return dest; +} + +int isLink(const char* path) +{ + struct stat sb; + + if (lstat(path, &sb) == 0) + return S_ISLNK(sb.st_mode); + + tell(0, "Error: Detecting state for '%s' failed, error was '%s'", path, strerror(errno)); + + return false; +} + +const char* suffixOf(const char* path) +{ + const char* p; + + if (path && (p = strrchr(path, '.'))) + return p+1; + + return ""; +} + +int fileSize(const char* path) +{ + struct stat sb; + + if (lstat(path, &sb) == 0) + return sb.st_size; + + tell(0, "Error: Detecting state for '%s' failed, error was '%s'", path, strerror(errno)); + + return 0; +} + +time_t fileModTime(const char* path) +{ + struct stat sb; + + if (lstat(path, &sb) == 0) + return sb.st_mtime; + + tell(0, "Error: Detecting state for '%s' failed, error was '%s'", path, strerror(errno)); + + return 0; +} + +int folderExists(const char* path) +{ + struct stat fs; + + return stat(path, &fs) == 0 && S_ISDIR(fs.st_mode); +} + +int fileExists(const char* path) +{ + return access(path, F_OK) == 0; +} + +int createLink(const char* link, const char* dest, int force) +{ + if (!fileExists(link) || force) + { + // may be the link exists and point to a wrong or already deleted destination ... + // .. therefore we delete the link at first + + unlink(link); + + if (symlink(dest, link) != 0) + { + tell(0, "Failed to create symlink '%s', error was '%s'", link, strerror(errno)); + return fail; + } + } + + return success; +} + +//*************************************************************************** +// Remove File +//*************************************************************************** + +int removeFile(const char* filename) +{ + int lnk = isLink(filename); + + if (unlink(filename) != 0) + { + tell(0, "Can't remove file '%s', '%s'", filename, strerror(errno)); + + return 1; + } + + tell(3, "Removed %s '%s'", lnk ? "link" : "file", filename); + + return 0; +} + +//*************************************************************************** +// Check Dir +//*************************************************************************** + +int chkDir(const char* path) +{ + struct stat fs; + + if (stat(path, &fs) != 0 || !S_ISDIR(fs.st_mode)) + { + tell(0, "Creating directory '%s'", path); + + if (mkdir(path, ACCESSPERMS) == -1) + { + tell(0, "Can't create directory '%s'", strerror(errno)); + return fail; + } + } + + return success; +} + +#ifdef USELIBXML + +//*************************************************************************** +// Load XSLT +//*************************************************************************** + +xsltStylesheetPtr loadXSLT(const char* name, const char* path, int utf8) +{ + xsltStylesheetPtr stylesheet; + char* xsltfile; + + asprintf(&xsltfile, "%s/%s-%s.xsl", path, name, utf8 ? "utf-8" : "iso-8859-1"); + + if ((stylesheet = xsltParseStylesheetFile((const xmlChar*)xsltfile)) == 0) + tell(0, "Error: Can't load xsltfile %s", xsltfile); + else + tell(0, "Info: Stylesheet '%s' loaded", xsltfile); + + free(xsltfile); + return stylesheet; +} +#endif + +#ifdef USEGUNZIP + +//*************************************************************************** +// Gnu Unzip +//*************************************************************************** + +int gunzip(MemoryStruct* zippedData, MemoryStruct* unzippedData) +{ + const int growthStep = 1024; + + z_stream stream = {0,0,0,0,0,0,0,0,0,0,0,Z_NULL,Z_NULL,Z_NULL}; + unsigned int resultSize = 0; + int res = 0; + + unzippedData->clear(); + + // determining the size in this way is taken from the sources of the gzip utility. + + memcpy(&unzippedData->size, zippedData->memory + zippedData->size -4, 4); + unzippedData->memory = (char*)malloc(unzippedData->size); + + // zlib initialisation + + stream.avail_in = zippedData->size; + stream.next_in = (Bytef*)zippedData->memory; + stream.avail_out = unzippedData->size; + stream.next_out = (Bytef*)unzippedData->memory; + + // The '+ 32' tells zlib to process zlib&gzlib headers + + res = inflateInit2(&stream, MAX_WBITS + 32); + + if (res != Z_OK) + { + tellZipError(res, " during zlib initialisation", stream.msg); + inflateEnd(&stream); + return fail; + } + + // skip the header + + res = inflate(&stream, Z_BLOCK); + + if (res != Z_OK) + { + tellZipError(res, " while skipping the header", stream.msg); + inflateEnd(&stream); + return fail; + } + + while (res == Z_OK) + { + if (stream.avail_out == 0) + { + unzippedData->size += growthStep; + unzippedData->memory = (char*)realloc(unzippedData->memory, unzippedData->size); + + // Set the stream pointers to the potentially changed buffer! + + stream.avail_out = resultSize - stream.total_out; + stream.next_out = (Bytef*)(unzippedData + stream.total_out); + } + + res = inflate(&stream, Z_SYNC_FLUSH); + resultSize = stream.total_out; + } + + if (res != Z_STREAM_END) + { + tellZipError(res, " during inflating", stream.msg); + inflateEnd(&stream); + return fail; + } + + unzippedData->size = resultSize; + inflateEnd(&stream); + + return success; +} + +//*************************************************************************** +// gzip +//*************************************************************************** + +int gzip(Bytef* dest, uLongf* destLen, const Bytef* source, uLong sourceLen) +{ + z_stream stream; + int res; + + stream.next_in = (Bytef *)source; + stream.avail_in = (uInt)sourceLen; + stream.next_out = dest; + stream.avail_out = (uInt)*destLen; + if ((uLong)stream.avail_out != *destLen) return Z_BUF_ERROR; + + stream.zalloc = (alloc_func)0; + stream.zfree = (free_func)0; + stream.opaque = (voidpf)0; + + res = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); + + if (res == Z_OK) + { + res = deflate(&stream, Z_FINISH); + + if (res != Z_STREAM_END) + { + deflateEnd(&stream); + res = res == Z_OK ? Z_BUF_ERROR : res; + } + } + + if (res == Z_STREAM_END) + { + *destLen = stream.total_out; + res = deflateEnd(&stream); + } + + if (res != Z_OK) + tellZipError(res, " during compression", ""); + + return res == Z_OK ? success : fail; +} + +ulong gzipBound(ulong size) +{ + return compressBound(size); +} + +//************************************************************************* +// tellZipError +//************************************************************************* + +void tellZipError(int errorCode, const char* op, const char* msg) +{ + if (!op) op = ""; + if (!msg) msg = "None"; + + switch (errorCode) + { + case Z_OK: return; + case Z_STREAM_END: return; + case Z_MEM_ERROR: tell(0, "Error: Not enough memory to zip/unzip file%s!\n", op); return; + case Z_BUF_ERROR: tell(0, "Error: Couldn't zip/unzip data due to output buffer size problem%s!\n", op); return; + case Z_DATA_ERROR: tell(0, "Error: Zipped input data corrupted%s! Details: %s\n", op, msg); return; + case Z_STREAM_ERROR: tell(0, "Error: Invalid stream structure%s. Details: %s\n", op, msg); return; + default: tell(0, "Error: Couldn't zip/unzip data for unknown reason (%6d)%s!\n", errorCode, op); return; + } +} + +#endif // USEGUNZIP + +//************************************************************************* +// Host Data +//************************************************************************* + +#include <sys/utsname.h> +#include <netdb.h> +#include <ifaddrs.h> + +static struct utsname info; + +const char* getHostName() +{ + // get info from kernel + + if (uname(&info) == -1) + return ""; + + return info.nodename; +} + +const char* getFirstIp(int skipLo) +{ + struct ifaddrs *ifaddr, *ifa; + static char host[NI_MAXHOST] = ""; + static char netMask[INET6_ADDRSTRLEN] = ""; + + if (getifaddrs(&ifaddr) == -1) + { + tell(0, "Error: getifaddrs() failed"); + return ""; + } + + // walk through linked interface list + + for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) + { + if (!ifa->ifa_addr) + continue; + + // For an AF_INET interfaces + + if (ifa->ifa_addr->sa_family == AF_INET) // || ifa->ifa_addr->sa_family == AF_INET6) + { + int res = getnameinfo(ifa->ifa_addr, + (ifa->ifa_addr->sa_family == AF_INET) ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6), + host, NI_MAXHOST, 0, 0, NI_NUMERICHOST); + + if (res) + { + tell(0, "Error: getnameinfo() for '%s' failed: %s", gai_strerror(res), ifa->ifa_name); + continue; + } + + // skip loopback interface + + if (skipLo && strcmp(host, "127.0.0.1") == 0) + continue; + + if (ifa->ifa_netmask && ifa->ifa_netmask->sa_family == AF_INET) + { + void* p = &((struct sockaddr_in*)ifa->ifa_netmask)->sin_addr; + inet_ntop(ifa->ifa_netmask->sa_family, p, netMask, sizeof(netMask)); + } + + tell(1, "%-8s %-15s %s; netmask '%s'", ifa->ifa_name, host, + ifa->ifa_addr->sa_family == AF_INET ? " (AF_INET)" : + ifa->ifa_addr->sa_family == AF_INET6 ? " (AF_INET6)" : "", + netMask); + } + } + + freeifaddrs(ifaddr); + + return host; +} + +//*************************************************************************** +// Get Interfaces +//*************************************************************************** + +const char* getInterfaces() +{ + static char buffer[1000+TB] = ""; + static char host[NI_MAXHOST] = ""; + struct ifaddrs *ifaddr, *ifa; + + *buffer = 0; + + if (getifaddrs(&ifaddr) == -1) + { + tell(0, "Error: getifaddrs() failed"); + return ""; + } + + // walk through linked interface list + + for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) + { + if (!ifa->ifa_addr) + continue; + + // For an AF_INET interfaces + + if (ifa->ifa_addr->sa_family == AF_INET) // || ifa->ifa_addr->sa_family == AF_INET6) + { + int res = getnameinfo(ifa->ifa_addr, + (ifa->ifa_addr->sa_family == AF_INET) ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6), + host, NI_MAXHOST, 0, 0, NI_NUMERICHOST); + + if (res) + { + tell(0, "Error: getnameinfo() failed: %s, skipping interface '%s'", gai_strerror(res), ifa->ifa_name); + continue; + } + + sprintf(eos(buffer), "%s:%s ", ifa->ifa_name, host); + } + } + + freeifaddrs(ifaddr); + + return buffer; +} + +//*************************************************************************** +// Get First Interface +//*************************************************************************** + +const char* getFirstInterface() +{ + static char buffer[1000+TB] = ""; + static char host[NI_MAXHOST] = ""; + struct ifaddrs *ifaddr, *ifa; + + *buffer = 0; + + if (getifaddrs(&ifaddr) == -1) + { + tell(0, "Error: getifaddrs() failed"); + return ""; + } + + // walk through linked interface list + + for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) + { + if (!ifa->ifa_addr) + continue; + + // For an AF_INET interfaces + + if (ifa->ifa_addr->sa_family == AF_INET) // || ifa->ifa_addr->sa_family == AF_INET6) + { + int res = getnameinfo(ifa->ifa_addr, + (ifa->ifa_addr->sa_family == AF_INET) ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6), + host, NI_MAXHOST, 0, 0, NI_NUMERICHOST); + + if (res) + { + tell(0, "Error: getnameinfo() failed: %s, skipping interface '%s'", gai_strerror(res), ifa->ifa_name); + continue; + } + + if (strcasecmp(ifa->ifa_name, "lo") != 0) + sprintf(buffer, "%s", ifa->ifa_name); + } + } + + freeifaddrs(ifaddr); + + return buffer; +} + +//*************************************************************************** +// Get IP Of +//*************************************************************************** + +const char* getIpOf(const char* device) +{ + struct ifreq ifr; + int fd; + + if (isEmpty(device)) + return getFirstIp(); + + fd = socket(AF_INET, SOCK_DGRAM, 0); + + ifr.ifr_addr.sa_family = AF_INET; + strncpy(ifr.ifr_name, device, IFNAMSIZ-1); + + ioctl(fd, SIOCGIFADDR, &ifr); + close(fd); + + // caller has to copy the result string 'before' calling again! + + return inet_ntoa(((struct sockaddr_in*)&ifr.ifr_addr)->sin_addr); +} + +//*************************************************************************** +// Get Mask Of +//*************************************************************************** + +const char* getMaskOf(const char* device) +{ + struct ifreq ifr; + static char netMask[INET6_ADDRSTRLEN] = ""; + int fd; + + if (isEmpty(device)) + device = getFirstInterface(); + + fd = socket(AF_INET, SOCK_DGRAM, 0); + + ifr.ifr_addr.sa_family = AF_INET; + strncpy(ifr.ifr_name, device, IFNAMSIZ-1); + ioctl(fd, SIOCGIFNETMASK, &ifr); + close(fd); + + if (ifr.ifr_netmask.sa_family == AF_INET) + { + void* p = &((struct sockaddr_in*)(&ifr.ifr_netmask))->sin_addr; + inet_ntop(ifr.ifr_netmask.sa_family, p, netMask, sizeof(netMask)); + + tell(5, "netmask for device '%s' is '%s'", device, netMask); + } + + return netMask; +} + +//*************************************************************************** +// Broadcast Address Of +//*************************************************************************** + +const char* bcastAddressOf(const char* ipStr, const char* maskStr) +{ + struct in_addr host, mask, broadcast; + static char bcastAddress[INET_ADDRSTRLEN] = ""; + + if (isEmpty(maskStr)) + maskStr = "255.255.255.0"; + + if (inet_pton(AF_INET, ipStr, &host) == 1 && inet_pton(AF_INET, maskStr, &mask) == 1) + { + broadcast.s_addr = host.s_addr | ~mask.s_addr; + + if (inet_ntop(AF_INET, &broadcast, bcastAddress, INET_ADDRSTRLEN)) + tell(5, "Bcast for '%s' is '%s'", ipStr, bcastAddress); + else + tell(0, "Error: Failed converting number to string"); + } + else + { + tell(0, "Error: Failed converting strings to numbers"); + } + + return bcastAddress; +} + +//*************************************************************************** +// MAC Address Of +//*************************************************************************** + +const char* getMacOf(const char* device) +{ + enum { macTuppel = 6 }; + + static char mac[20+TB]; + struct ifreq ifr; + int s; + + s = socket(AF_INET, SOCK_DGRAM, 0); + strcpy(ifr.ifr_name, "eth0"); + ioctl(s, SIOCGIFHWADDR, &ifr); + + for (int i = 0; i < macTuppel; i++) + sprintf(&mac[i*3],"%02x:",((unsigned char*)ifr.ifr_hwaddr.sa_data)[i]); + + mac[17] = 0; + + close(s); + + return mac; +} + +#ifdef USELIBARCHIVE + +//*************************************************************************** +// unzip <file> and get data of first content which name matches <filter> +//*************************************************************************** + +int unzip(const char* file, const char* filter, char*& buffer, int& size, char* entryName) +{ + const int step = 1024*10; + + int bufSize = 0; + int r; + int res; + + struct archive_entry* entry; + struct archive* a = archive_read_new(); + + *entryName = 0; + buffer = 0; + size = 0; + + archive_read_support_filter_all(a); + archive_read_support_format_all(a); + + r = archive_read_open_filename(a, file, 10204); + + if (r != ARCHIVE_OK) + { + tell(0, "Error: Open '%s' failed - %s", file, strerror(errno)); + return 1; + } + + while (archive_read_next_header(a, &entry) == ARCHIVE_OK) + { + strcpy(entryName, archive_entry_pathname(entry)); + + if (strstr(entryName, filter)) + { + bufSize = step; + buffer = (char*)malloc(bufSize+1); + + while ((res = archive_read_data(a, buffer+size, step)) > 0) + { + size += res; + bufSize += step; + + buffer = (char*)realloc(buffer, bufSize+1); + } + + buffer[size] = 0; + + break; + } + } + + r = archive_read_free(a); + + if (r != ARCHIVE_OK) + { + size = 0; + free(buffer); + return fail; + } + + return size > 0 ? success : fail; +} + +#endif + +//*************************************************************************** +// cMyMutex +//*************************************************************************** + +cMyMutex::cMyMutex (void) +{ + locked = 0; + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK_NP); + pthread_mutex_init(&mutex, &attr); +} + +cMyMutex::~cMyMutex() +{ + pthread_mutex_destroy(&mutex); +} + +void cMyMutex::Lock(void) +{ + pthread_mutex_lock(&mutex); + locked++; +} + +void cMyMutex::Unlock(void) +{ + if (!--locked) + pthread_mutex_unlock(&mutex); +} + +//*************************************************************************** +// cMyTimeMs +//*************************************************************************** + +cMyTimeMs::cMyTimeMs(int Ms) +{ + if (Ms >= 0) + Set(Ms); + else + begin = 0; +} + +uint64_t cMyTimeMs::Now(void) +{ +#define MIN_RESOLUTION 5 // ms + static bool initialized = false; + static bool monotonic = false; + struct timespec tp; + if (!initialized) { + // check if monotonic timer is available and provides enough accurate resolution: + if (clock_getres(CLOCK_MONOTONIC, &tp) == 0) { + // long Resolution = tp.tv_nsec; + // require a minimum resolution: + if (tp.tv_sec == 0 && tp.tv_nsec <= MIN_RESOLUTION * 1000000) { + if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0) { + monotonic = true; + } + else + tell(0, "Error: cMyTimeMs: clock_gettime(CLOCK_MONOTONIC) failed"); + } + else + tell(0, "Info: cMyTimeMs: not using monotonic clock - resolution is too bad (%ld s %ld ns)", tp.tv_sec, tp.tv_nsec); + } + else + tell(0, "Error: cMyTimeMs: clock_getres(CLOCK_MONOTONIC) failed"); + initialized = true; + } + if (monotonic) { + if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0) + return (uint64_t(tp.tv_sec)) * 1000 + tp.tv_nsec / 1000000; + tell(0, "Error: cMyTimeMs: clock_gettime(CLOCK_MONOTONIC) failed"); + monotonic = false; + // fall back to gettimeofday() + } + struct timeval t; + if (gettimeofday(&t, NULL) == 0) + return (uint64_t(t.tv_sec)) * 1000 + t.tv_usec / 1000; + return 0; +} + +void cMyTimeMs::Set(int Ms) +{ + begin = Now() + Ms; +} + +bool cMyTimeMs::TimedOut(void) +{ + return Now() >= begin; +} + +uint64_t cMyTimeMs::Elapsed(void) +{ + return Now() - begin; +} + +//************************************************************************** +// Regular Expression Searching +//************************************************************************** + +int rep(const char* string, const char* expression, Option options) +{ + const char* tmpA; + const char* tmpB; + + return rep(string, expression, tmpA, tmpB, options); +} + +int rep(const char* string, const char* expression, const char*& s_location, + Option options) +{ + const char* tmpA; + + return rep(string, expression, s_location, tmpA, options); +} + + +int rep(const char* string, const char* expression, const char*& s_location, + const char*& e_location, Option options) +{ + regex_t reg; + regmatch_t rm; + int status; + int opt = 0; + + // Vorbereiten von reg fuer die Expressionsuche mit regexec + // Flags: REG_EXTENDED = Use Extended Regular Expressions + // REG_ICASE = Ignore case in match. + + reg.re_nsub = 0; + + // Options umwandeln + if (options & repUseRegularExpression) + opt = opt | REG_EXTENDED; + if (options & repIgnoreCase) + opt = opt | REG_ICASE; + + if (regcomp( ®, expression, opt) != 0) + return fail; + + // Suchen des ersten Vorkommens von reg in string + + status = regexec(®, string, 1, &rm, 0); + regfree(®); + + if (status != 0) + return fail; + + // Suche erfolgreich => + // Setzen der ermittelten Start- und Endpositionen + + s_location = (char*)(string + rm.rm_so); + e_location = (char*)(string + rm.rm_eo); + + return success; +} + +//*************************************************************************** +// Class LogDuration +//*************************************************************************** + +LogDuration::LogDuration(const char* aMessage, int aLogLevel) +{ + logLevel = aLogLevel; + strcpy(message, aMessage); + + // at last ! + + durationStart = cMyTimeMs::Now(); +} + +LogDuration::~LogDuration() +{ + tell(logLevel, "duration '%s' was (%ldms)", + message, (long)(cMyTimeMs::Now() - durationStart)); +} + +void LogDuration::show(const char* label) +{ + tell(logLevel, "elapsed '%s' at '%s' was (%ldms)", + message, label, (long)(cMyTimeMs::Now() - durationStart)); +} + +//*************************************************************************** +// Get Unique ID +//*************************************************************************** + +#ifdef USEUUID +const char* getUniqueId() +{ + static char uuid[sizeUuid+TB] = ""; + + uuid_t id; + uuid_generate(id); + uuid_unparse_upper(id, uuid); + + return uuid; +} +#endif // USEUUID + +//*************************************************************************** +// Create MD5 +//*************************************************************************** + +#ifdef USEMD5 + +int createMd5(const char* buf, md5* md5) +{ + MD5_CTX c; + unsigned char out[MD5_DIGEST_LENGTH]; + + MD5_Init(&c); + MD5_Update(&c, buf, strlen(buf)); + MD5_Final(out, &c); + + for (int n = 0; n < MD5_DIGEST_LENGTH; n++) + sprintf(md5+2*n, "%02x", out[n]); + + md5[sizeMd5] = 0; + + return done; +} + +int createMd5OfFile(const char* path, const char* name, md5* md5) +{ + FILE* f; + char buffer[1000]; + int nread = 0; + MD5_CTX c; + unsigned char out[MD5_DIGEST_LENGTH]; + char* file = 0; + + asprintf(&file, "%s/%s", path, name); + + if (!(f = fopen(file, "r"))) + { + tell(0, "Fatal: Cannot build MD5 of '%s'; Error was '%s'", file, strerror(errno)); + free(file); + return fail; + } + + free(file); + + MD5_Init(&c); + + while ((nread = fread(buffer, 1, 1000, f)) > 0) + MD5_Update(&c, buffer, nread); + + fclose(f); + + MD5_Final(out, &c); + + for (int n = 0; n < MD5_DIGEST_LENGTH; n++) + sprintf(md5+2*n, "%02x", out[n]); + + md5[sizeMd5] = 0; + + return success; +} + +#endif // USEMD5 + +//*************************************************************************** +// Url Unescape +//*************************************************************************** +/* + * The buffer pointed to by @dst must be at least strlen(@src) bytes. + * Decoding stops at the first character from @src that decodes to null. + + * Path normalization will remove redundant slashes and slash+dot sequences, + * as well as removing path components when slash+dot+dot is found. It will + * keep the root slash (if one was present) and will stop normalization + * at the first questionmark found (so query parameters won't be normalized). + * + * @param dst destination buffer + * @param src source buffer + * @param normalize perform path normalization if nonzero + * @return number of valid characters in @dst + */ + +int urlUnescape(char* dst, const char* src, int normalize) +{ +// CURL* curl; +// int resultSize; + +// if (curl_global_init(CURL_GLOBAL_ALL) != 0) +// { +// tell(0, "Error, something went wrong with curl_global_init()"); + +// return fail; +// } + +// curl = curl_easy_init(); + +// if (!curl) +// { +// tell(0, "Error, unable to get handle from curl_easy_init()"); + +// return fail; +// } + +// dst = curl_easy_unescape(curl, src, strlen(src), &resultSize); + +// tell(0, " [%.40s]", src); + +// tell(0, "res size %d [%.40s]", resultSize, dst); +// return resultSize; + + char* org_dst = dst; + int slash_dot_dot = 0; + char ch, a, b; + + a = 0; + + do { + ch = *src++; + + if (ch == '%' && isxdigit(a = src[0]) && isxdigit(b = src[1])) + { + if (a < 'A') + a -= '0'; + else if + (a < 'a') a -= 'A' - 10; + else + a -= 'a' - 10; + + if (b < 'A') + b -= '0'; + else if (b < 'a') + b -= 'A' - 10; + else + b -= 'a' - 10; + + ch = 16 * a + b; + src += 2; + } + + if (normalize) + { + switch (ch) + { + case '/': // compress consecutive slashes and remove slash-dot + if (slash_dot_dot < 3) + { + + dst -= slash_dot_dot; + slash_dot_dot = 1; + break; + } + // fall-through + + case '?': // at start of query, stop normalizing + if (ch == '?') + normalize = 0; + + // fall-through + + case '\0': // remove trailing slash-dot-(dot) + if (slash_dot_dot > 1) + { + dst -= slash_dot_dot; + + // remove parent directory if it was two dots + + if (slash_dot_dot == 3) + while (dst > org_dst && *--dst != '/') + ; // empty body + slash_dot_dot = (ch == '/') ? 1 : 0; + + // keep the root slash if any + + if (!slash_dot_dot && dst == org_dst && *dst == '/') + ++dst; + + } + break; + + case '.': + if (slash_dot_dot == 1 || slash_dot_dot == 2) + { + ++slash_dot_dot; + break; + } + // fall-through + + default: + slash_dot_dot = 0; + } + } + + *dst++ = ch; + } while(ch); + + return (dst - org_dst) - 1; +} diff --git a/lib/common.h b/lib/common.h new file mode 100644 index 0000000..df1c236 --- /dev/null +++ b/lib/common.h @@ -0,0 +1,560 @@ +/* + * common.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __COMMON_H +#define __COMMON_H + +#include <stdint.h> // uint_64_t +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <zlib.h> +#include <errno.h> +#include <string> +#include <map> + +#ifdef USESYSD +# include <systemd/sd-daemon.h> +#endif + +#ifdef USEMD5 +# include <openssl/md5.h> // MD5_* +#endif + +#ifdef USELIBXML +# include <libxslt/transform.h> +# include <libxslt/xsltutils.h> +# include <libexslt/exslt.h> +#endif + +#ifdef VDR_PLUGIN +# include <vdr/tools.h> +#endif + +class MemoryStruct; + +//*************************************************************************** +// Misc +//*************************************************************************** + +#ifndef VDR_PLUGIN + inline long min(long a, long b) { return a < b ? a : b; } + inline long max(long a, long b) { return a > b ? a : b; } +#else +# define __STL_CONFIG_H +# include <vdr/tools.h> +#endif + +enum Misc +{ + success = 0, + done = success, + fail = -1, + na = -1, + ignore = -2, + all = -3, + abrt = -4, + yes = 1, + on = 1, + off = 0, + no = 0, + TB = 1, + +#ifdef USEMD5 + sizeMd5 = 2 * MD5_DIGEST_LENGTH, +#endif + sizeUuid = 36, + + tmeSecondsPerMinute = 60, + tmeSecondsPerHour = tmeSecondsPerMinute * 60, + tmeSecondsPerDay = 24 * tmeSecondsPerHour, + tmeUsecondsPerSecond = 1000 * 1000 +}; + +enum Case +{ + cUpper, + cLower +}; + +const char* toCase(Case cs, char* str); + +//*************************************************************************** +// Tell +//*************************************************************************** + +extern const char* logPrefix; + +const char* getLogPrefix(); +void __attribute__ ((format(printf, 2, 3))) tell(int eloquence, const char* format, ...); + +//*************************************************************************** +// Syslog +//*************************************************************************** + +class Syslog +{ + public: + + struct Facilities + { + const char* name; // #83 + int code; + }; + + static const Facilities facilities[]; + + static char* toName(int code); + static int toCode(const char* name); + + static int syslogFacility; +}; + +//*************************************************************************** +// +//*************************************************************************** + +char* srealloc(void* ptr, size_t size); + +//*************************************************************************** +// Gun-Zip +//*************************************************************************** + +ulong gzipBound(ulong size); +int gzip(Bytef* dest, uLongf* destLen, const Bytef* source, uLong sourceLen); +void tellZipError(int errorCode, const char* op, const char* msg); +int gunzip(MemoryStruct* zippedData, MemoryStruct* unzippedData); + +//*************************************************************************** +// MemoryStruct +//*************************************************************************** + +struct MemoryStruct +{ + public: + + MemoryStruct() { expireAt = 0; memory = 0; zmemory = 0; clear(); } + MemoryStruct(const MemoryStruct* o) + { + size = o->size; + memory = (char*)malloc(size); + memcpy(memory, o->memory, size); + + zsize = o->zsize; + zmemory = (char*)malloc(zsize); + memcpy(zmemory, o->zmemory, zsize); + + copyAttributes(o); + } + + ~MemoryStruct() { clear(); } + + int isEmpty() { return memory == 0; } + int isZipped() { return zmemory != 0 && zsize > 0; } + + int append(const char* buf, int len) + { + memory = srealloc(memory, size+len); + memcpy(memory+size, buf, len); + size += len; + + return success; + } + + void copyAttributes(const MemoryStruct* o) + { + strcpy(tag, o->tag); + strcpy(name, o->name); + strcpy(contentType, o->contentType); + strcpy(contentEncoding, o->contentEncoding); + strcpy(mimeType, o->mimeType); + headerOnly = o->headerOnly; + modTime = o->modTime; + expireAt = o->expireAt; + } + + int toGzip() + { + free(zmemory); + zsize = 0; + + if (isEmpty()) + return fail; + + zsize = gzipBound(size) + 512; // the maximum calculated by the lib, will adusted at gzip() call + zmemory = (char*)malloc(zsize); + + if (gzip((Bytef*)zmemory, &zsize, (Bytef*)memory, size) != success) + { + free(zmemory); + zsize = 0; + tell(0, "Error gzip failed!"); + + return fail; + } + + sprintf(contentEncoding, "gzip"); + + return success; + } + + void clear() + { + free(memory); + memory = 0; + size = 0; + free(zmemory); + zmemory = 0; + zsize = 0; + *tag = 0; + *name = 0; + *contentType = 0; + *contentEncoding = 0; + *mimeType = 0; + modTime = time(0); + headerOnly = no; + headers.clear(); + statusCode = 0; + // expireAt = time(0); -> don't reset 'expireAt' here !!!! + } + + // data + + char* memory; + long unsigned int size; + + char* zmemory; + long unsigned int zsize; + + // tag attribute + + char tag[100+TB]; // the tag to be compared + char name[100+TB]; // content name (filename) + char contentType[100+TB]; // e.g. text/html + char mimeType[100+TB]; // + char contentEncoding[100+TB]; // + int headerOnly; + long statusCode; + time_t modTime; + time_t expireAt; + std::map<std::string, std::string> headers; +}; + +//*************************************************************************** +// Tools +//*************************************************************************** + +double usNow(); +unsigned int getHostId(); +const char* getHostName(); +const char* getFirstInterface(); +const char* getInterfaces(); +const char* getFirstIp(int skipLo = yes); +const char* getIpOf(const char* device); +const char* getMacOf(const char* device); +const char* getMaskOf(const char* device); +const char* bcastAddressOf(const char* ipStr, const char* maskStr = 0); + +//*************************************************************************** + +#ifdef USEUUID + const char* getUniqueId(); +#endif + +void removeChars(std::string& str, const char* ignore); +void removeCharsExcept(std::string& str, const char* except); +void removeWord(std::string& pattern, std::string word); +void prepareCompressed(std::string& pattern); +std::string strReplace(const std::string& what, const std::string& with, const std::string& subject); +std::string strReplace(const std::string& what, long with, const std::string& subject); +std::string strReplace(const std::string& what, double with, const std::string& subject); +char* strReplace(char* buffer, char from, char to); + +int rangeFrom(const char* s); +int rangeTo(const char* s); + +char* rTrim(char* buf); +char* lTrim(char* buf); +char* allTrim(char* buf); + +int isMember(const char** list, const char* item); +char* sstrcpy(char* dest, const char* src, int max); +std::string num2Str(int num); +int isDST(time_t t = 0); +time_t timeOf(time_t t); +int weekdayOf(time_t t); +const char* toWeekdayName(uint day); +time_t hhmmOf(time_t t); +int l2hhmm(time_t t); +std::string hhmm2pTime(int hhmm); +time_t midnightOf(time_t t); +std::string l2pTime(time_t t, const char* format = "%d.%m.%Y %T"); +std::string l2pDate(time_t t); +std::string l2HttpTime(time_t t); +std::string ms2Dur(uint64_t t); +const char* c2s(char c, char* buf); +char* eos(char* s); +int urlUnescape(char* dst, const char* src, int normalize = yes); + +int storeToFile(const char* filename, const char* data, int size); +int loadFromFile(const char* infile, MemoryStruct* data); + +int folderExists(const char* path); +int fileExists(const char* path); +int fileSize(const char* path); +time_t fileModTime(const char* path); +int createLink(const char* link, const char* dest, int force); +int isLink(const char* path); +const char* suffixOf(const char* path); +int isEmpty(const char* str); +const char* notNull(const char* str, const char* def = "<null>"); +int isZero(const char* str); +int removeFile(const char* filename); +int chkDir(const char* path); + +#ifdef USELIBXML + xsltStylesheetPtr loadXSLT(const char* name, const char* path, int utf8); +#endif + +#ifdef USEMD5 + typedef char md5Buf[sizeMd5+TB]; + typedef char md5; + int createMd5(const char* buf, md5* md5); + int createMd5OfFile(const char* path, const char* name, md5* md5); +#endif + +//*************************************************************************** +// Zip +//*************************************************************************** + +#ifdef USELIBARCHIVE +int unzip(const char* file, const char* filter, char*& buffer, + int& size, char* entryName); +#endif + +//*************************************************************************** +// cMyMutex +//*************************************************************************** + +class cMyMutex +{ + friend class cCondVar; + + public: + + cMyMutex(void); + ~cMyMutex(); + void Lock(void); + void Unlock(void); + + private: + + pthread_mutex_t mutex; + int locked; +}; + +//*************************************************************************** +// cMyTimeMs +//*************************************************************************** + +class cMyTimeMs +{ + private: + + uint64_t begin; + + public: + + cMyTimeMs(int Ms = 0); + static uint64_t Now(void); + void Set(int Ms = 0); + bool TimedOut(void); + uint64_t Elapsed(void); +}; + +//*************************************************************************** +// Wrapper for Regual Expression Library +//*************************************************************************** + +enum Option +{ + repUseRegularExpression = 1, + repIgnoreCase = 2 +}; + +int rep(const char* string, const char* expression, Option options = repUseRegularExpression); + +int rep(const char* string, const char* expression, + const char*& s_location, Option options = repUseRegularExpression); + +int rep(const char* string, const char* expression, const char*& s_location, + const char*& e_location, Option options = repUseRegularExpression); + +//*************************************************************************** +// Log Duration +//*************************************************************************** + +class LogDuration +{ + public: + + LogDuration(const char* aMessage, int aLogLevel = 2); + ~LogDuration(); + + void show(const char* label = ""); + + protected: + + char message[1000]; + uint64_t durationStart; + int logLevel; +}; + +//*************************************************************************** +// Semaphore +//*************************************************************************** + +#include <sys/sem.h> + +class Sem +{ + public: + + Sem(key_t aKey) + { + locked = no; + key = aKey; + + if ((id = semget(key, 1, 0666 | IPC_CREAT)) == -1) + tell(0, "Error: Can't get semaphore, errno (%d) '%s'", + errno, strerror(errno)); + } + + ~Sem() + { + if (locked) + v(); + } + + // ---------------------- + // get lock + + int p() + { + sembuf sops[2]; + + sops[0].sem_num = 0; + sops[0].sem_op = 0; // wait for lock + sops[0].sem_flg = SEM_UNDO; + + sops[1].sem_num = 0; + sops[1].sem_op = 1; // increment + sops[1].sem_flg = SEM_UNDO | IPC_NOWAIT; + + if (semop(id, sops, 2) == -1) + { + tell(0, "Error: Can't lock semaphore, errno (%d) '%s'", + errno, strerror(errno)); + + return fail; + } + + locked = yes; + + return success; + } + + // ---------------------- + // increment + + int inc() + { + sembuf sops[1]; + + sops[0].sem_num = 0; + sops[0].sem_op = 1; // increment + sops[0].sem_flg = SEM_UNDO | IPC_NOWAIT; + + if (semop(id, sops, 1) == -1) + { + if (errno != EAGAIN) + tell(0, "Error: Can't lock semaphore, errno was (%d) '%s'", + errno, strerror(errno)); + + return fail; + } + + locked = yes; + + return success; + } + + // ---------------------- + // decrement + + int dec() + { + return v(); + } + + // ---------------------- + // check + + int check() + { + sembuf sops[1]; + + sops[0].sem_num = 0; + sops[0].sem_op = 0; + sops[0].sem_flg = SEM_UNDO | IPC_NOWAIT; + + if (semop(id, sops, 1) == -1) + { + if (errno != EAGAIN) + tell(0, "Error: Can't lock semaphore, errno was (%d) '%s'", + errno, strerror(errno)); + + return fail; + } + + return success; + } + + // ---------------------- + // release lock + + int v() + { + sembuf sops; + + sops.sem_num = 0; + sops.sem_op = -1; // release control + sops.sem_flg = SEM_UNDO | IPC_NOWAIT; + + if (semop(id, &sops, 1) == -1) + { + if (errno != EAGAIN) + tell(0, "Error: Can't unlock semaphore, errno (%d) '%s'", + errno, strerror(errno)); + + return fail; + } + + locked = no; + + return success; + } + + private: + + key_t key; + int id; + int locked; +}; + +//*************************************************************************** +#endif //___COMMON_H diff --git a/lib/config.c b/lib/config.c new file mode 100644 index 0000000..f7d3fba --- /dev/null +++ b/lib/config.c @@ -0,0 +1,59 @@ +/* + * config.c: + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <syslog.h> + +#include "config.h" + +//*************************************************************************** +// Statics +//*************************************************************************** + +int cEpgConfig::logstdout = no; +int cEpgConfig::loglevel = 1; +int cEpgConfig::argLoglevel = na; +int cEpgConfig::logFacility = LOG_USER; +const char* cEpgConfig::logName = "unknown"; + +//*************************************************************************** +// Common EPG Service Configuration +//*************************************************************************** + +cEpgConfig::cEpgConfig() +{ + // database connection + + sstrcpy(dbHost, "localhost", sizeof(dbHost)); + dbPort = 3306; + sstrcpy(dbName, "epg2vdr", sizeof(dbName)); + sstrcpy(dbUser, "epg2vdr", sizeof(dbUser)); + sstrcpy(dbPass, "epg", sizeof(dbPass)); + + sstrcpy(netDevice, getFirstInterface(), sizeof(netDevice)); + + uuid[0] = 0; + + getepgimages = yes; +} + +//*************************************************************************** +// Has DB Login Changed +//*************************************************************************** + +int cEpgConfig::hasDbLoginChanged(cEpgConfig* old) +{ + if (old->dbPort != dbPort || + strcmp(old->dbHost, dbHost) != 0 || + strcmp(old->dbName, dbName) != 0 || + strcmp(old->dbUser, dbUser) != 0 || + strcmp(old->dbPass, dbPass) != 0) + { + return yes; + } + + return no; +} diff --git a/lib/config.h b/lib/config.h new file mode 100644 index 0000000..a233bc3 --- /dev/null +++ b/lib/config.h @@ -0,0 +1,47 @@ +/* + * config.h: + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __EPG_CONFIG_H +#define __EPG_CONFIG_H + +#include "common.h" + +//*************************************************************************** +// Config +//*************************************************************************** + +struct cEpgConfig +{ + public: + + cEpgConfig(); + + // database connection + + int hasDbLoginChanged(cEpgConfig* old); + + char dbHost[100+TB]; + int dbPort; + char dbName[100+TB]; + char dbUser[100+TB]; + char dbPass[100+TB]; + + char netDevice[20+TB]; + char uuid[sizeUuid+TB]; + + int getepgimages; + + // static stuff + + static int logstdout; + static int loglevel; + static int argLoglevel; + static int logFacility; + static const char* logName; +}; + +#endif // __EPG_CONFIG_H diff --git a/lib/configuration.c b/lib/configuration.c new file mode 100644 index 0000000..1f89930 --- /dev/null +++ b/lib/configuration.c @@ -0,0 +1,193 @@ +/* + * configuration.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "configuration.h" + +time_t cSystemNotification::lastWatchdogAt = time(0); +const char* cSystemNotification::pidfile = ""; + +//*************************************************************************** +// Class cSystemNotification +//*************************************************************************** + +cSystemNotification::cSystemNotification() + : cThread("SystemNotification", yes) +{ + interval = 0; + stop = no; +} + +//*************************************************************************** +// System Watchdog Check / notify +//*************************************************************************** + +void cSystemNotification::check(int force) +{ + if (interval && (force || lastWatchdogAt <= time(0) - interval)) + { + // notify all 'n' seconds + + notify(evKeepalive); + lastWatchdogAt = time(0); + } +} + +//*************************************************************************** +// Start / Stop notification thread +//*************************************************************************** + +int cSystemNotification::startNotifyThread(int timeout) +{ + threadTimeout = timeout; + Start(yes); + return success; +} + +int cSystemNotification::stopNotifyThread() +{ + stop = yes; + waitCondition.Broadcast(); + Cancel(3); + return success; +} + +//*************************************************************************** +// action +//*************************************************************************** + +void cSystemNotification::action() +{ + cMyMutex mutex; + time_t timeoutAt = time(0) + threadTimeout; + + stop = no; + mutex.Lock(); + + while (time(0) < timeoutAt && Running() && !stop) + { + // tell(0, "loop ..."); + + waitCondition.TimedWait(mutex, interval*1000); + + if (!stop) + check(); + } + + if (time(0) >= timeoutAt) + tell(0, "Warning: Ending notification thread, timeout reached!"); +} + +//*************************************************************************** +// Notify System Deamon :o :( +//*************************************************************************** + +int cSystemNotification::notify(int event, const char* format, ...) +{ +#ifdef USESYSD + char* message; + char* tmp; + va_list ap; + + if (isEmpty(format)) + format = ""; + + va_start(ap, format); + vasprintf(&tmp, format, ap); + + switch (event) + { + case evStatus: asprintf(&message, "%s", tmp); break; + case evStopping: asprintf(&message, "STOPPING=1\n%s", tmp); break; + case evReady: asprintf(&message, "READY=1\nSTATUS=Ready\nMAINPID=%d\n%s", getpid(), tmp); break; + case evKeepalive: asprintf(&message, "WATCHDOG=1\n%s", tmp); break; + } + + tell(event == evKeepalive ? 2 : 1, "Calling sd_notify(%s)", message); + sd_notify(0, message); + + free(tmp); + free(message); +#endif + + // handle pidfile at evReady / evStopping + + if (!isEmpty(pidfile)) + { + if (event == evReady) + { + FILE* f = fopen(pidfile, "w"); + + if (f) + { + pid_t pid = getpid(); + tell(1, "Creating pidfile '%s'; pid %d", pidfile, pid); + fprintf(f, "%d\n", pid); + fclose(f); + } + else + tell(0, "Error: Can't create pid file '%s' error was (%d) '%s'", + pidfile, errno, strerror(errno)); + } + else if (event == evStopping) + { + if (fileExists(pidfile)) + { + tell(1, "Removing pidfile '%s'", pidfile); + removeFile(pidfile); + } + } + } + + return done; +} + +//*************************************************************************** +// +//*************************************************************************** + +int cSystemNotification::getWatchdogState(int minInterval) +{ + interval = 0; + +#ifdef USESYSD + +# ifdef SYSDWDIFO + uint64_t us = 0; + int sec; + + if (sd_watchdog_enabled(0, &us) > 0 && us > 0) + { + sec = us / tmeUsecondsPerSecond; + + if (sec < minInterval*2) + { + tell(0, "Warning: Systemd watchdog configured to (%ds) but min %ds are required," + " ignoring watchdog", sec, minInterval * 2); + return no; + } + + interval = sec / 2; + + tell(0, "Info: Systemd watchdog request interval" + " of at least(%ds), using (%ds) now!", sec, interval); + + return yes; + } + + tell(0, "Info: Systemd watchdog not configured, epgd won't be sending keep-alive messages!"); + +# else + interval = defaultInterval; + return yes; +# endif + +#else + tell(0, "Info: Systemd support not enabled, epgd won't be sending notifications!"); +#endif + + return no; +} diff --git a/lib/configuration.h b/lib/configuration.h new file mode 100644 index 0000000..bd6ba96 --- /dev/null +++ b/lib/configuration.h @@ -0,0 +1,140 @@ +/* + * configuration.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __CONFIGURATION_H +#define __CONFIGURATION_H + +#include "thread.h" +#include "common.h" + +extern const char* confDir; + +//*************************************************************************** +// Configuration +//*************************************************************************** + +class Configuration +{ + public: + + Configuration() {}; + virtual ~Configuration() {}; + + virtual int atConfigItem(const char* Name, const char* Value) = 0; + + virtual int readConfig(const char* file = 0) + { + int count = 0; + FILE* f; + char* line = 0; + size_t size = 0; + char* value; + char* name; + char* fileName; + + if (!isEmpty(file)) + asprintf(&fileName, "%s", file); + else + asprintf(&fileName, "%s/epgd.conf", confDir); + + if (access(fileName, F_OK) != 0) + { + fprintf(stderr, "Cannot access configuration file '%s'\n", fileName); + free(fileName); + return fail; + } + + f = fopen(fileName, "r"); + + while (getline(&line, &size, f) > 0) + { + char* p = strchr(line, '#'); + if (p) *p = 0; + + allTrim(line); + + if (isEmpty(line)) + continue; + + if (!(value = strchr(line, '='))) + continue; + + *value = 0; + value++; + lTrim(value); + name = line; + allTrim(name); + + if (atConfigItem(name, value) != success) + { + fprintf(stderr, "Found unexpected parameter '%s', aborting\n", name); + free(fileName); + return fail; + } + + count++; + } + + free(line); + fclose(f); + + tell(0, "Read %d option from %s", count , fileName); + + free(fileName); + + return success; + } +}; + +//*************************************************************************** +// System Notification Interface (systemd, watchdog, pidfile, ...) +//*************************************************************************** + +class cSystemNotification : public cThread +{ + public: + + enum SystemEvent + { + evReady, + evStatus, + evKeepalive, + evStopping + }; + + enum Misc + { + defaultInterval = 60 + }; + + cSystemNotification(); + + int __attribute__ ((format(printf, 3, 4))) notify(int event, const char* format = 0, ...); + int getWatchdogState(int minInterval); + void check(int force = no); + + int startNotifyThread(int timeout); + int stopNotifyThread(); + + static void setPidFile(const char* file) { pidfile = file; } + + protected: + + virtual void action(); + + int interval; + int threadTimeout; + cCondVar waitCondition; + int stop; + + static time_t lastWatchdogAt; + static const char* pidfile; +}; + +//*************************************************************************** + +#endif // __CONFIGURATION_H diff --git a/lib/curl.c b/lib/curl.c new file mode 100644 index 0000000..ab5cf82 --- /dev/null +++ b/lib/curl.c @@ -0,0 +1,454 @@ +/* + * curlfuncs.cpp + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "curl.h" + +//*************************************************************************** +// Singelton +//*************************************************************************** + +cCurl curl; + +std::string cCurl::sBuf = ""; +int cCurl::curlInitialized = no; +cSystemNotification* cCurl::sysNotification = 0; + +//*************************************************************************** +// Callbacks +//*************************************************************************** + +size_t collect_data(void *ptr, size_t size, size_t nmemb, void* stream) +{ + std::string sTmp; + register size_t actualsize = size * nmemb; + + if ((FILE *)stream == NULL) + { + sTmp.assign((char *)ptr, actualsize); + cCurl::sBuf += sTmp; + } + else + { + fwrite(ptr, size, nmemb, (FILE *)stream); + } + + return actualsize; +} + +//*************************************************************************** +// Object +//*************************************************************************** + +cCurl::cCurl() +{ + handle = 0; +} + +cCurl::~cCurl() +{ + exit(); +} + +//*************************************************************************** +// Create / Destroy +//*************************************************************************** + +int cCurl::create() +{ + if (!curlInitialized) + { + // call only once per process and *before* any thread is started! + + if (curl_global_init(CURL_GLOBAL_NOTHING /*CURL_GLOBAL_ALL*/) != 0) + { + tell(0, "Error, something went wrong with curl_global_init()"); + return fail; + } + + curlInitialized = yes; + } + + return done; +} + +int cCurl::destroy() +{ + if (curlInitialized) + curl_global_cleanup(); + + curlInitialized = no; + + return done; +} + +//*************************************************************************** +// Init / Exit +//*************************************************************************** + +int cCurl::init(const char* httpproxy) +{ + if (!handle) + { + if (!(handle = curl_easy_init())) + { + tell(0, "Could not create new curl instance"); + return fail; + } + } + + // Reset Options + + if (!isEmpty(httpproxy)) + { + curl_easy_setopt(handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + curl_easy_setopt(handle, CURLOPT_PROXY, httpproxy); // Specify HTTP proxy + } + + curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, collect_data); + curl_easy_setopt(handle, CURLOPT_WRITEDATA, 0); // Set option to write to string + curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, yes); + curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, 0); // Send header to this function + curl_easy_setopt(handle, CURLOPT_WRITEHEADER, 0); // Pass some header details to this struct + curl_easy_setopt(handle, CURLOPT_MAXFILESIZE, 100*1024*1024); // Set maximum file size to get (bytes) + curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 1); // No progress meter + curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1); // No signaling + curl_easy_setopt(handle, CURLOPT_TIMEOUT, 30); // Set timeout + curl_easy_setopt(handle, CURLOPT_NOBODY, 0); // + curl_easy_setopt(handle, CURLOPT_USERAGENT, CURL_USERAGENT); // Some servers don't like requests + + return success; +} + +int cCurl::exit() +{ + if (handle) + curl_easy_cleanup(handle); + + handle = 0; + + return done; +} + +//*************************************************************************** +// Get Url +//*************************************************************************** + +int cCurl::GetUrl(const char *url, std::string *sOutput, const std::string &sReferer) +{ + CURLcode res; + + init(); + + curl_easy_setopt(handle, CURLOPT_URL, url); // Set the URL to get + + if (sReferer != "") + curl_easy_setopt(handle, CURLOPT_REFERER, sReferer.c_str()); + + curl_easy_setopt(handle, CURLOPT_HTTPGET, yes); + curl_easy_setopt(handle, CURLOPT_FAILONERROR, yes); + curl_easy_setopt(handle, CURLOPT_WRITEDATA, 0); // Set option to write to string + sBuf = ""; + + res = curl_easy_perform(handle); + + if (res != CURLE_OK) + { + *sOutput = ""; + return 0; + } + + *sOutput = sBuf; + return 1; +} + +int cCurl::GetUrlFile(const char *url, const char *filename, const std::string &sReferer) +{ + int nRet = 0; + init(); + + // Point the output to a file + + FILE *fp; + if ((fp = fopen(filename, "w")) == NULL) + return 0; + + curl_easy_setopt(handle, CURLOPT_WRITEDATA, fp); // Set option to write to file + curl_easy_setopt(handle, CURLOPT_URL, url); // Set the URL to get + if (sReferer != "") + curl_easy_setopt(handle, CURLOPT_REFERER, sReferer.c_str()); + curl_easy_setopt(handle, CURLOPT_HTTPGET, yes); + if (curl_easy_perform(handle) == 0) + nRet = 1; + else + nRet = 0; + + curl_easy_setopt(handle, CURLOPT_WRITEDATA, NULL); // Set option back to default (string) + fclose(fp); + return nRet; +} + +int cCurl::PostUrl(const char *url, const std::string &sPost, std::string *sOutput, const std::string &sReferer) +{ + init(); + + int retval = 1; + std::string::size_type nStart = 0, nEnd, nPos; + std::string sTmp, sName, sValue; + struct curl_httppost *formpost=NULL; + struct curl_httppost *lastptr=NULL; + struct curl_slist *headerlist=NULL; + + // Add the POST variables here + while ((nEnd = sPost.find("##", nStart)) != std::string::npos) { + sTmp = sPost.substr(nStart, nEnd - nStart); + if ((nPos = sTmp.find("=")) == std::string::npos) + return 0; + sName = sTmp.substr(0, nPos); + sValue = sTmp.substr(nPos+1); + curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, sName.c_str(), CURLFORM_COPYCONTENTS, sValue.c_str(), CURLFORM_END); + nStart = nEnd + 2; + } + sTmp = sPost.substr(nStart); + if ((nPos = sTmp.find("=")) == std::string::npos) + return 0; + sName = sTmp.substr(0, nPos); + sValue = sTmp.substr(nPos+1); + curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, sName.c_str(), CURLFORM_COPYCONTENTS, sValue.c_str(), CURLFORM_END); + + retval = curl.DoPost(url, sOutput, sReferer, formpost, headerlist); + + curl_formfree(formpost); + curl_slist_free_all(headerlist); + return retval; +} + +int cCurl::PostRaw(const char *url, const std::string &sPost, std::string *sOutput, const std::string &sReferer) +{ + init(); + + int retval; + struct curl_httppost *formpost=NULL; + struct curl_slist *headerlist=NULL; + + curl_easy_setopt(handle, CURLOPT_POSTFIELDS, sPost.c_str()); + curl_easy_setopt(handle, CURLOPT_POSTFIELDSIZE, 0); //FIXME: Should this be the size instead, in case this is binary string? + + retval = curl.DoPost(url, sOutput, sReferer, formpost, headerlist); + + curl_formfree(formpost); + curl_slist_free_all(headerlist); + return retval; +} + +int cCurl::DoPost(const char *url, std::string *sOutput, const std::string &sReferer, + struct curl_httppost *formpost, struct curl_slist *headerlist) +{ + headerlist = curl_slist_append(headerlist, "Expect:"); + + // Now do the form post + curl_easy_setopt(handle, CURLOPT_URL, url); + if (sReferer != "") + curl_easy_setopt(handle, CURLOPT_REFERER, sReferer.c_str()); + curl_easy_setopt(handle, CURLOPT_HTTPPOST, formpost); + + curl_easy_setopt(handle, CURLOPT_WRITEDATA, 0); // Set option to write to string + sBuf = ""; + if (curl_easy_perform(handle) == 0) { + *sOutput = sBuf; + return 1; + } + else { + // We have an error here mate! + *sOutput = ""; + return 0; + } +} + +int cCurl::SetCookieFile(char *filename) +{ + init(); + + if (curl_easy_setopt(handle, CURLOPT_COOKIEFILE, filename) != 0) + return 0; + if (curl_easy_setopt(handle, CURLOPT_COOKIEJAR, filename) != 0) + return 0; + return 1; +} + +char* cCurl::EscapeUrl(const char *url) +{ + init(); + return curl_easy_escape(handle, url , strlen(url)); +} + +void cCurl::Free(char* str) +{ + curl_free(str); +} + +//*************************************************************************** +// Callbacks +//*************************************************************************** + +size_t cCurl::WriteMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data) +{ + size_t realsize = size * nmemb; + struct MemoryStruct* mem = (struct MemoryStruct*)data; + + if (sysNotification) + sysNotification->check(); + + if (mem->memory) + mem->memory = (char*)realloc(mem->memory, mem->size + realsize + 1); + else + mem->memory = (char*)malloc(mem->size + realsize + 1); + + if (mem->memory) + { + memcpy (&(mem->memory[mem->size]), ptr, realsize); + mem->size += realsize; + mem->memory[mem->size] = 0; + } + + return realsize; +} + +size_t cCurl::WriteHeaderCallback(char* ptr, size_t size, size_t nmemb, void* data) +{ + size_t realsize = size * nmemb; + struct MemoryStruct* mem = (struct MemoryStruct*)data; + char* p; + + if (sysNotification) + sysNotification->check(); + + if (ptr) + { + // add to Header to map + + std::string header(ptr); + std::size_t pos = header.find(": "); + + if(pos != std::string::npos) + { + std::string name = header.substr(0, pos); + std::string value = header.substr(pos+2, std::string::npos); + mem->headers[name] = value; + } + + // get filename + { + // Content-Disposition: attachment; filename="20140103_20140103_de_qy.zip" + + const char* attribute = "Content-disposition: "; + + if ((p = strcasestr((char*)ptr, attribute))) + { + if ((p = strcasestr(p, "filename="))) + { + p += strlen("filename="); + + tell(4, "found filename at [%s]", p); + + if (*p == '"') + p++; + + sprintf(mem->name, "%s", p); + + if ((p = strchr(mem->name, '\n'))) + *p = 0; + + if ((p = strchr(mem->name, '\r'))) + *p = 0; + + if ((p = strchr(mem->name, '"'))) + *p = 0; + + tell(4, "set name to '%s'", mem->name); + } + } + } + + // since some sources update "ETag" an "Last-Modified:" without changing the contents + // we have to use "Content-Length:" to check for updates :( + { + const char* attribute = "Content-Length: "; + + if ((p = strcasestr((char*)ptr, attribute))) + { + sprintf(mem->tag, "%s", p+strlen(attribute)); + + if ((p = strchr(mem->tag, '\n'))) + *p = 0; + + if ((p = strchr(mem->tag, '\r'))) + *p = 0; + + if ((p = strchr(mem->tag, '"'))) + *p = 0; + } + } + } + + return realsize; +} + +//*************************************************************************** +// Download File +//*************************************************************************** + +int cCurl::downloadFile(const char* url, int& size, MemoryStruct* data, int timeout, + const char* userAgent, struct curl_slist* headerlist) +{ + long code; + CURLcode res = CURLE_OK; + + size = 0; + + init(); + + curl_easy_setopt(handle, CURLOPT_URL, url); // Specify URL to get + curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, yes); + curl_easy_setopt(handle, CURLOPT_UNRESTRICTED_AUTH, yes); // continue to send authentication (user+password) when following locations + curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); // Send all data to this function + curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void*)data); // Pass our 'data' struct to the callback function + curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, WriteHeaderCallback); // Send header to this function + curl_easy_setopt(handle, CURLOPT_WRITEHEADER, (void*)data); // Pass some header details to this struct + curl_easy_setopt(handle, CURLOPT_MAXFILESIZE, 100*1024*1024); // Set maximum file size to get (bytes) + curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 1); // No progress meter + curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1); // No signaling + curl_easy_setopt(handle, CURLOPT_TIMEOUT, timeout); // Set timeout + curl_easy_setopt(handle, CURLOPT_NOBODY, data->headerOnly ? 1 : 0); // + curl_easy_setopt(handle, CURLOPT_USERAGENT, userAgent); // Some servers don't like requests without a user-agent field + curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, "gzip"); // + + if (headerlist) + curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headerlist); + + // perform http-get + + if ((res = curl_easy_perform(handle)) != 0) + { + data->clear(); + tell(1, "Error, download failed; %s (%d)", curl_easy_strerror(res), res); + + return fail; + } + + curl_easy_getinfo(handle, CURLINFO_HTTP_CODE, &code); + data->statusCode = code; + + if (code == 404) + { + data->clear(); + return fail; + } + + size = data->size; + + return success; +} diff --git a/lib/curl.h b/lib/curl.h new file mode 100644 index 0000000..475675c --- /dev/null +++ b/lib/curl.h @@ -0,0 +1,77 @@ +/* + * curlfuncs.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __LIB_CURL__ +#define __LIB_CURL__ + +#include <curl/curl.h> +#include <curl/easy.h> + +#include <string> + +#include "common.h" +#include "config.h" +#include "configuration.h" + +#define CURL_USERAGENT "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; Mayukh's libcurl wrapper http://www.mayukhbose.com/)" + +//*************************************************************************** +// CURL +//*************************************************************************** + +class cCurl +{ + public: + + cCurl(); + ~cCurl(); + + int init(const char* httpproxy = ""); + int exit(); + + static int create(); + static int destroy(); + + int GetUrl(const char *url, std::string *sOutput, const std::string &sReferer=""); + int GetUrlFile(const char *url, const char *filename, const std::string &sReferer=""); + int SetCookieFile(char *filename); + int PostUrl(const char *url, const std::string &sPost, std::string *sOutput, const std::string &sReferer = ""); + int PostRaw(const char *url, const std::string &sPost, std::string *sOutput, const std::string &sReferer = ""); + int DoPost(const char *url, std::string *sOutput, const std::string &sReferer, + struct curl_httppost *formpost, struct curl_slist *headerlist); + + char* EscapeUrl(const char *url); + void Free(char* str); + + int downloadFile(const char* url, int& size, MemoryStruct* data, int timeout = 30, + const char* userAgent = CURL_USERAGENT, struct curl_slist *headerlist = 0); + + // static stuff + + static void setSystemNotification(cSystemNotification* s) { sysNotification = s; } + static std::string sBuf; // dirty + + protected: + + // data + + CURL* handle; + + static cSystemNotification* sysNotification; + + // statics + + static size_t WriteMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data); + static size_t WriteHeaderCallback(char* ptr, size_t size, size_t nmemb, void* data); + + static int curlInitialized; +}; + +extern cCurl curl; + +//*************************************************************************** +#endif // __LIB_CURL__ diff --git a/lib/db.c b/lib/db.c new file mode 100644 index 0000000..271c6ae --- /dev/null +++ b/lib/db.c @@ -0,0 +1,1649 @@ +/* + * db.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <stdio.h> +#include <errmsg.h> + +#include <map> + +#include "db.h" + +//*************************************************************************** +// DB Statement +//*************************************************************************** + +int cDbStatement::explain = no; + +cDbStatement::cDbStatement(cDbTable* aTable) +{ + table = aTable; + connection = table->getConnection(); + stmtTxt = ""; + stmt = 0; + inCount = 0; + outCount = 0; + inBind = 0; + outBind = 0; + affected = 0; + metaResult = 0; + bindPrefix = 0; + firstExec = yes; + buildErrors = 0; + + callsPeriod = 0; + callsTotal = 0; + duration = 0; + + if (connection) + connection->statements.append(this); +} + +cDbStatement::cDbStatement(cDbConnection* aConnection, const char* sText) +{ + table = 0; + connection = aConnection; + stmtTxt = sText; + stmt = 0; + inCount = 0; + outCount = 0; + inBind = 0; + outBind = 0; + affected = 0; + metaResult = 0; + bindPrefix = 0; + firstExec = yes; + + callsPeriod = 0; + callsTotal = 0; + duration = 0; + buildErrors = 0; + + if (connection) + connection->statements.append(this); +} + +cDbStatement::~cDbStatement() +{ + if (connection) + connection->statements.remove(this); + + clear(); +} + +//*************************************************************************** +// Execute +//*************************************************************************** + +int cDbStatement::execute(int noResult) +{ + affected = 0; + + if (!connection || !connection->getMySql()) + return fail; + + if (!stmt) + return connection->errorSql(connection, "execute(missing statement)"); + +// if (explain && firstExec) +// { +// firstExec = no; + +// if (strstr(stmtTxt.c_str(), "select ")) +// { +// MYSQL_RES* result; +// MYSQL_ROW row; +// string q = "explain " + stmtTxt; + +// if (connection->query(q.c_str()) != success) +// connection->errorSql(connection, "explain ", 0); +// else if ((result = mysql_store_result(connection->getMySql()))) +// { +// while ((row = mysql_fetch_row(result))) +// { +// tell(0, "EXPLAIN: %s) %s %s %s %s %s %s %s %s %s", +// row[0], row[1], row[2], row[3], +// row[4], row[5], row[6], row[7], row[8], row[9]); +// } + +// mysql_free_result(result); +// } +// } +// } + + // tell(0, "execute %d [%s]", stmt, stmtTxt.c_str()); + + double start = usNow(); + + if (mysql_stmt_execute(stmt)) + return connection->errorSql(connection, "execute(stmt_execute)", stmt, stmtTxt.c_str()); + + duration += usNow() - start; + callsPeriod++; + callsTotal++; + + // out binding - if needed + + if (outCount && !noResult) + { + if (mysql_stmt_store_result(stmt)) + return connection->errorSql(connection, "execute(store_result)", stmt, stmtTxt.c_str()); + + // fetch the first result - if any + + if (mysql_stmt_affected_rows(stmt) > 0) + mysql_stmt_fetch(stmt); + } + else if (outCount) + { + mysql_stmt_store_result(stmt); + } + + // result was stored (above) only if output (outCound) is expected, + // therefore we don't need to call freeResult() after insert() or update() + + affected = mysql_stmt_affected_rows(stmt); + + return success; +} + +//*************************************************************************** +// +//*************************************************************************** + +int cDbStatement::getLastInsertId() +{ + MYSQL_RES* result = 0; + int insertId = na; + + if ((result = mysql_store_result(connection->getMySql())) == 0 && + mysql_field_count(connection->getMySql()) == 0 && + mysql_insert_id(connection->getMySql()) != 0) + { + insertId = mysql_insert_id(connection->getMySql()); + } + + mysql_free_result(result); + + return insertId; +} + +int cDbStatement::getResultCount() +{ + mysql_stmt_store_result(stmt); + + return mysql_stmt_affected_rows(stmt); +} + +int cDbStatement::find() +{ + if (execute() != success) + return fail; + + return getAffected() > 0 ? yes : no; +} + +int cDbStatement::fetch() +{ + if (!mysql_stmt_fetch(stmt)) + return yes; + + return no; +} + +int cDbStatement::freeResult() +{ + if (metaResult) + mysql_free_result(metaResult); + + if (stmt) + mysql_stmt_free_result(stmt); + + return success; +} + +//*************************************************************************** +// Build Statements - new Interface +//*************************************************************************** + +int cDbStatement::build(const char* format, ...) +{ + if (format) + { + char* tmp; + + va_list more; + va_start(more, format); + vasprintf(&tmp, format, more); + + stmtTxt += tmp; + free(tmp); + } + + return success; +} + +int cDbStatement::bind(const char* fname, int mode, const char* delim) +{ + return bind(table->getValue(fname), mode, delim); +} + +int cDbStatement::bind(cDbFieldDef* field, int mode, const char* delim) +{ + return bind(table->getValue(field), mode, delim); +} + +int cDbStatement::bind(cDbTable* aTable, cDbFieldDef* field, int mode, const char* delim) +{ + return bind(aTable->getValue(field), mode, delim); +} + +int cDbStatement::bind(cDbTable* aTable, const char* fname, int mode, const char* delim) +{ + return bind(aTable->getValue(fname), mode, delim); +} + +int cDbStatement::bind(cDbValue* value, int mode, const char* delim) +{ + if (!value || !value->getField()) + { + tell(0, "Error: Missing %s value", !value ? "bind" : "field of bind"); + buildErrors++; + return fail; + } + + if (delim) + stmtTxt += delim; + + if (bindPrefix) + stmtTxt += bindPrefix; + + if (mode & bndIn) + { + if (mode & bndSet) + stmtTxt += value->getDbName() + std::string(" ="); + + stmtTxt += " ?"; + appendBinding(value, bndIn); + } + else if (mode & bndOut) + { + stmtTxt += value->getDbName(); + appendBinding(value, bndOut); + } + + return success; +} + +int cDbStatement::bindAllOut(const char* delim, int incTypes, int excTypes) +{ + int n = 0; + std::map<std::string, cDbFieldDef*>::iterator f; + cDbTableDef* tableDef = table->getTableDef(); + + if (delim) + stmtTxt += delim; + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + { + if (!(incTypes & f->second->getType())) // Include Types + continue; + + if (excTypes & f->second->getType()) // Exclude Types + continue; + + bind(f->second, bndOut, n++ ? ", " : ""); + } + + return success; +} + +int cDbStatement::bindCmp(const char* ctable, cDbValue* value, + const char* comp, const char* delim) +{ + if (delim) build("%s", delim); + if (ctable) build("%s.", ctable); + + build("%s%s %s ?", bindPrefix ? bindPrefix : "", value->getDbName(), comp); + + appendBinding(value, bndIn); + + return success; +} + +int cDbStatement::bindCmp(const char* ctable, cDbFieldDef* field, cDbValue* value, + const char* comp, const char* delim) +{ + cDbValue* vf = table->getRow()->getValue(field); + cDbValue* vv = value ? value : vf; + + if (!vf || !vv) + { + buildErrors++; + return fail; + } + + if (delim) build("%s", delim); + if (ctable) build("%s.", ctable); + + build("%s%s %s ?", bindPrefix ? bindPrefix : "", vf->getDbName(), comp); + + appendBinding(vv, bndIn); + + return success; +} + +int cDbStatement::bindCmp(const char* ctable, const char* fname, cDbValue* value, + const char* comp, const char* delim) +{ + cDbValue* vf = table->getRow()->getValue(fname); + cDbValue* vv = value ? value : vf; + + if (!vf || !vv) + { + buildErrors++; + return fail; + } + + if (delim) build("%s", delim); + if (ctable) build("%s.", ctable); + + build("%s%s %s ?", bindPrefix ? bindPrefix : "", vf->getDbName(), comp); + + appendBinding(vv, bndIn); + + return success; +} + +int cDbStatement::bindText(const char* text, cDbValue* value, + const char* comp, const char* delim) +{ + if (!value) + { + buildErrors++; + return fail; + } + + if (delim) build("%s", delim); + + build("%s %s ?", text, comp); + + appendBinding(value, bndIn); + + return success; +} + +int cDbStatement::bindTextFree(const char* text, cDbValue* value, int mode) +{ + if (!value) + { + buildErrors++; + return fail; + } + + build("%s", text); + + if (mode & bndIn) + appendBinding(value, bndIn); + + else if (mode & bndOut) + appendBinding(value, bndOut); + + return success; +} + +//*************************************************************************** +// Bind In Char - like <field> in ('A','B','C') +// +// expected string in cDbValue is: "A,B,C" +//*************************************************************************** + +int cDbStatement::bindInChar(const char* ctable, const char* fname, + cDbValue* value, const char* delim) +{ + cDbValue* vf = table->getRow()->getValue(fname); + cDbValue* vv = value ? value : vf; + + if (!vf || !vv) + { + buildErrors++; + return fail; + } + + build("%s find_in_set(cast(%s%s%s%s as char),?)", + delim ? delim : "", + bindPrefix ? bindPrefix : "", + ctable ? ctable : "", + ctable ? "." : "", + vf->getDbName()); + + appendBinding(vv, bndIn); + + return success; +} + +//*************************************************************************** +// Clear +//*************************************************************************** + +void cDbStatement::clear() +{ + stmtTxt = ""; + affected = 0; + + if (inCount) + { + free(inBind); + inCount = 0; + inBind = 0; + } + + if (outCount) + { + free(outBind); + outCount = 0; + outBind = 0; + } + + if (stmt) + { + mysql_stmt_free_result(stmt); + mysql_stmt_close(stmt); + stmt = 0; + } +} + +//*************************************************************************** +// Append Binding +//*************************************************************************** + +int cDbStatement::appendBinding(cDbValue* value, BindType bt) +{ + int count = 0; + MYSQL_BIND** bindings = 0; + MYSQL_BIND* newBinding; + + if (bt & bndIn) + { + count = ++inCount; + bindings = &inBind; + } + else if (bt & bndOut) + { + count = ++outCount; + bindings = &outBind; + } + else + return 0; + + if (!*bindings) + *bindings = (MYSQL_BIND*)malloc(count * sizeof(MYSQL_BIND)); + else + *bindings = (MYSQL_BIND*)srealloc(*bindings, count * sizeof(MYSQL_BIND)); + + newBinding = &((*bindings)[count-1]); + + memset(newBinding, 0, sizeof(MYSQL_BIND)); + + if (value->getField()->getFormat() == ffAscii || value->getField()->getFormat() == ffText || value->getField()->getFormat() == ffMText) + { + newBinding->buffer_type = MYSQL_TYPE_STRING; + newBinding->buffer = value->getStrValueRef(); + newBinding->buffer_length = value->getField()->getSize(); + newBinding->length = value->getStrValueSizeRef(); + + newBinding->is_null = value->getNullRef(); + newBinding->error = 0; // #TODO + } + else if (value->getField()->getFormat() == ffMlob) + { + newBinding->buffer_type = MYSQL_TYPE_BLOB; + newBinding->buffer = value->getStrValueRef(); + newBinding->buffer_length = value->getField()->getSize(); + newBinding->length = value->getStrValueSizeRef(); + + newBinding->is_null = value->getNullRef(); + newBinding->error = 0; // #TODO + } + else if (value->getField()->getFormat() == ffFloat) + { + newBinding->buffer_type = MYSQL_TYPE_FLOAT; + newBinding->buffer = value->getFloatValueRef(); + + newBinding->length = 0; // #TODO + newBinding->is_null = value->getNullRef(); + newBinding->error = 0; // #TODO + } + else if (value->getField()->getFormat() == ffDateTime) + { + newBinding->buffer_type = MYSQL_TYPE_DATETIME; + newBinding->buffer = value->getTimeValueRef(); + + newBinding->length = 0; // #TODO + newBinding->is_null = value->getNullRef(); + newBinding->error = 0; // #TODO + } + else if (value->getField()->getFormat() == ffBigInt || value->getField()->getFormat() == ffUBigInt) + { + newBinding->buffer_type = MYSQL_TYPE_LONGLONG; + newBinding->buffer = value->getBigIntValueRef(); + newBinding->is_unsigned = (value->getField()->getFormat() == ffUBigInt); + + newBinding->length = 0; + newBinding->is_null = value->getNullRef(); + newBinding->error = 0; // #TODO + } + else // ffInt, ffUInt + { + newBinding->buffer_type = MYSQL_TYPE_LONG; + newBinding->buffer = value->getIntValueRef(); + newBinding->is_unsigned = (value->getField()->getFormat() == ffUInt); + + newBinding->length = 0; + newBinding->is_null = value->getNullRef(); + newBinding->error = 0; // #TODO + } + + return success; +} + +//*************************************************************************** +// Prepare Statement +//*************************************************************************** + +int cDbStatement::prepare() +{ + if (!connection->getMySql()) + { + tell(0, "Error: Lost connection, can't prepare statement"); + return fail; + } + + if (!stmtTxt.length()) + return fail; + + if (buildErrors) + return fail; + + stmt = mysql_stmt_init(connection->getMySql()); + + // prepare statement + + if (mysql_stmt_prepare(stmt, stmtTxt.c_str(), stmtTxt.length())) + return connection->errorSql(connection, "prepare(stmt_prepare)", stmt, stmtTxt.c_str()); + + if (outBind) + { + if (mysql_stmt_bind_result(stmt, outBind)) + return connection->errorSql(connection, "execute(bind_result)", stmt); + } + + if (inBind) + { + if (mysql_stmt_bind_param(stmt, inBind)) + return connection->errorSql(connection, "buildPrimarySelect(bind_param)", stmt); + } + + tell(2, "Statement '%s' with (%ld) in parameters and (%d) out bindings prepared", + stmtTxt.c_str(), mysql_stmt_param_count(stmt), outCount); + + return success; +} + +//*************************************************************************** +// Show Statistic +//*************************************************************************** + +void cDbStatement::showStat() +{ + if (callsPeriod) + { + tell(0, "calls %4ld in %6.2fms; total %4ld [%s]", + callsPeriod, duration/1000, callsTotal, stmtTxt.c_str()); + + callsPeriod = 0; + duration = 0; + } +} + +//*************************************************************************** +// cDbConnection statics +//*************************************************************************** + +char* cDbConnection::confPath = 0; +char* cDbConnection::encoding = 0; +char* cDbConnection::dbHost = strdup("localhost"); +int cDbConnection::dbPort = 3306; +char* cDbConnection::dbUser = 0; +char* cDbConnection::dbPass = 0; +char* cDbConnection::dbName = 0; +int cDbConnection::initThreads = 0; +cMyMutex cDbConnection::initMutex; + +//*************************************************************************** +// Class cDbTable +//*************************************************************************** + +//*************************************************************************** +// Object +//*************************************************************************** + +cDbTable::cDbTable(cDbConnection* aConnection, const char* name) +{ + connection = aConnection; + holdInMemory = no; + attached = no; + + row = 0; + stmtSelect = 0; + stmtInsert = 0; + stmtUpdate = 0; + lastInsertId = na; + + tableDef = dbDict.getTable(name); + + if (tableDef) + row = new cDbRow(tableDef); + else + tell(0, "Fatal: Table '%s' missing in dictionary '%s'!", name, dbDict.getPath()); +} + +cDbTable::~cDbTable() +{ + close(); + + delete row; +} + +//*************************************************************************** +// Open / Close +//*************************************************************************** + +int cDbTable::open(int allowAlter) +{ + if (!tableDef || !row) + return abrt; + + if (attach() != success) + { + tell(0, "Could not access database '%s:%d' (tried to open %s)", + connection->getHost(), connection->getPort(), TableName()); + + return fail; + } + + return init(allowAlter); +} + +int cDbTable::close() +{ + if (stmtSelect) { delete stmtSelect; stmtSelect = 0; } + if (stmtInsert) { delete stmtInsert; stmtInsert = 0; } + if (stmtUpdate) { delete stmtUpdate; stmtUpdate = 0; } + + detach(); + + return success; +} + +//*************************************************************************** +// Attach / Detach +//*************************************************************************** + +int cDbTable::attach() +{ + if (isAttached()) + return success; + + if (connection->attachConnection() != success) + { + tell(0, "Could not access database '%s:%d'", + connection->getHost(), connection->getPort()); + + return fail; + } + + attached = yes; + + return success; +} + +int cDbTable::detach() +{ + if (isAttached()) + { + connection->detachConnection(); + attached = no; + } + + return success; +} + +//*************************************************************************** +// Init +//*************************************************************************** + +int cDbTable::init(int allowAlter) +{ + std::string str; + std::map<std::string, cDbFieldDef*>::iterator f; + int n = 0; + + if (!isConnected()) + return fail; + + // check/create table ... + + if (exist() && allowAlter) + validateStructure(allowAlter); + + if (createTable() != success) + return fail; + + // check/create indices + + createIndices(); + + // ------------------------------ + // prepare BASIC statements + // ------------------------------ + + // select by primary key ... + + stmtSelect = new cDbStatement(this); + + stmtSelect->build("select "); + + n = 0; + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + stmtSelect->bind(f->second, bndOut, n++ ? ", " : ""); + + stmtSelect->build(" from %s where ", TableName()); + + n = 0; + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + { + if (!(f->second->getType() & ftPrimary)) + continue; + + stmtSelect->bind(f->second, bndIn | bndSet, n++ ? " and " : ""); + } + + stmtSelect->build(";"); + + if (stmtSelect->prepare() != success) + return fail; + + // ----------------------------------------- + // insert + + stmtInsert = new cDbStatement(this); + + stmtInsert->build("insert into %s set ", TableName()); + + n = 0; + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + { + // don't insert autoinc and calculated fields + + if (f->second->getType() & ftAutoinc) + continue; + + stmtInsert->bind(f->second, bndIn | bndSet, n++ ? ", " : ""); + } + + stmtInsert->build(";"); + + if (stmtInsert->prepare() != success) + return fail; + + // ----------------------------------------- + // update via primary key ... + + stmtUpdate = new cDbStatement(this); + + stmtUpdate->build("update %s set ", TableName()); + + n = 0; + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + { + // don't update PKey, autoinc and not used fields + + if (f->second->getType() & ftPrimary || + f->second->getType() & ftAutoinc) + continue; + + if (strcasecmp(f->second->getName(), "inssp") == 0) // don't update the insert stamp + continue; + + stmtUpdate->bind(f->second, bndIn | bndSet, n++ ? ", " : ""); + } + + stmtUpdate->build(" where "); + + n = 0; + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + { + if (!(f->second->getType() & ftPrimary)) + continue; + + stmtUpdate->bind(f->second, bndIn | bndSet, n++ ? " and " : ""); + } + + stmtUpdate->build(";"); + + if (stmtUpdate->prepare() != success) + return fail; + + return success; +} + +//*************************************************************************** +// Check Table +//*************************************************************************** + +int cDbTable::exist(const char* name) +{ + if (isEmpty(name)) + name = TableName(); + + if (!connection || !connection->getMySql()) + return fail; + + MYSQL_RES* result = mysql_list_tables(connection->getMySql(), name); + MYSQL_ROW tabRow = mysql_fetch_row(result); + mysql_free_result(result); + + return tabRow ? yes : no; +} + +//*************************************************************************** +// Validate Structure +//*************************************************************************** + +struct FieldInfo +{ + std::string columnFormat; + std::string description; + std::string def; +}; + +int cDbTable::validateStructure(int allowAlter) +{ + std::map<std::string, FieldInfo, _casecmp_> fields; + MYSQL_RES* result; + MYSQL_ROW row; + std::map<std::string, FieldInfo, _casecmp_>::iterator it; + int needDetach = no; + + if (!allowAlter) + return done; + + const char* select = "select column_name, column_type, column_comment, data_type, is_nullable, " + " character_maximum_length, column_default, numeric_precision " + " from information_schema.columns " + " where table_name = '%s' and table_schema= '%s'"; + + if (!isAttached()) + { + needDetach = yes; + + if (attach() != success) + return fail; + } + + // ------------------------ + // execute query + + if (connection->query(select, TableName(), connection->getName()) != success) + { + connection->errorSql(getConnection(), "validateStructure()", 0); + if (needDetach) detach(); + return fail; + } + + // ------------------------ + // process the result + + if (!(result = mysql_store_result(connection->getMySql()))) + { + connection->errorSql(getConnection(), "validateStructure()"); + if (needDetach) detach(); + return fail; + } + + while ((row = mysql_fetch_row(result))) + { + fields[row[0]].columnFormat = row[1]; + fields[row[0]].description = row[2]; + fields[row[0]].def = row[6] ? row[6] : ""; + } + + mysql_free_result(result); + + // -------------------------------------- + // validate if all fields of dict are in + // table and check their format, ... + + for (int i = 0; i < fieldCount(); i++) + { + char colType[100]; + + tell(4, "Check field '%s'", getField(i)->getName()); + + if (fields.find(getField(i)->getDbName()) == fields.end()) + alterAddField(getField(i)); + + else + { + FieldInfo* fieldInfo = &fields[getField(i)->getDbName()]; + + getField(i)->toColumnFormat(colType); + + if (strcasecmp(fieldInfo->columnFormat.c_str(), colType) != 0 || + strcasecmp(fieldInfo->description.c_str(), getField(i)->getDescription()) != 0 || + (strcasecmp(fieldInfo->def.c_str(), getField(i)->getDefault()) != 0 && !(getField(i)->getType() & ftPrimary))) + { + alterModifyField(getField(i)); + } + } + } + + // -------------------------------------- + // check if table contains unused fields + // and report them + + for (it = fields.begin(); it != fields.end(); it++) + { + if (!getRow()->getFieldByDbName(it->first.c_str())) + { + if (allowAlter == 2) + alterDropField(it->first.c_str()); + else + tell(0, "Info: Field '%s' not used anymore, " + "to remove it call 'ALTER TABLE %s DROP COLUMN %s;' manually", + it->first.c_str(), TableName(), it->first.c_str()); + } + } + + if (needDetach) detach(); + + return success; +} + +//*************************************************************************** +// Alter 'Modify Field' +//*************************************************************************** + +int cDbTable::alterModifyField(cDbFieldDef* def) +{ + char* statement; + char colType[100]; + + tell(0, " Info: Definition of field '%s.%s' modified, try to alter table", + TableName(), def->getName()); + + // alter table events modify column guest varchar(50) + + asprintf(&statement, "alter table %s modify column %s %s comment '%s' %s%s%s", + TableName(), + def->getDbName(), + def->toColumnFormat(colType), + def->getDbDescription(), + !isEmpty(def->getDefault()) ? "default '" : "", + !isEmpty(def->getDefault()) ? def->getDefault() : "", + !isEmpty(def->getDefault()) ? "'" : "" + ); + + tell(1, "Execute [%s]", statement); + + if (connection->query("%s", statement)) + return connection->errorSql(getConnection(), "alterAddField()", + 0, statement); + + free(statement); + + return done; +} + +//*************************************************************************** +// Alter 'Add Field' +//*************************************************************************** + +int cDbTable::alterAddField(cDbFieldDef* def) +{ + std::string statement; + char colType[100]; + + tell(0, "Info: Missing field '%s.%s', try to alter table", + TableName(), def->getName()); + + // alter table channelmap add column ord int(11) [after source] + + statement = std::string("alter table ") + TableName() + std::string(" add column ") + + def->getDbName() + std::string(" ") + def->toColumnFormat(colType); + + if (def->getFormat() != ffMlob) + { + if (def->getType() & ftAutoinc) + statement += " not null auto_increment"; + else if (!isEmpty(def->getDefault())) + statement += " default '" + std::string(def->getDefault()) + "'"; + } + + if (!isEmpty(def->getDbDescription())) + statement += std::string(" comment '") + def->getDbDescription() + std::string("'"); + + if (def->getIndex() > 0) + statement += std::string(" after ") + getField(def->getIndex()-1)->getDbName(); + + tell(1, "Execute [%s]", statement.c_str()); + + if (connection->query("%s", statement.c_str())) + return connection->errorSql(getConnection(), "alterAddField()", + 0, statement.c_str()); + + return done; +} + +//*************************************************************************** +// Alter 'Drop Field' +//*************************************************************************** + +int cDbTable::alterDropField(const char* name) +{ + char* statement; + + tell(0, "Info: Unused field '%s', try to drop it", name); + + // alter table channelmap add column ord int(11) [after source] + + asprintf(&statement, "alter table %s drop column %s", TableName(), name); + + tell(1, "Execute [%s]", statement); + + if (connection->query("%s", statement)) + return connection->errorSql(getConnection(), "alterDropField()", + 0, statement); + + free(statement); + + return done; +} + +//*************************************************************************** +// Create Table +//*************************************************************************** + +int cDbTable::createTable() +{ + std::string statement; + std::string aKey; + int needDetach = no; + + if (!tableDef || !row) + return abrt; + + if (!isAttached()) + { + needDetach = yes; + + if (attach() != success) + return fail; + } + + // table exists -> nothing to do + + if (exist()) + { + if (needDetach) detach(); + return done; + } + + tell(0, "Initialy creating table '%s'", TableName()); + + // build 'create' statement ... + + statement = std::string("create table ") + TableName() + std::string("("); + + for (int i = 0; i < fieldCount(); i++) + { + char colType[100]; + + if (i) statement += std::string(", "); + + statement += std::string(getField(i)->getDbName()) + " " + std::string(getField(i)->toColumnFormat(colType)); + + if (getField(i)->getFormat() != ffMlob) + { + if (getField(i)->getType() & ftAutoinc) + statement += " not null auto_increment"; + else if (!isEmpty(getField(i)->getDefault())) + statement += " default '" + std::string(getField(i)->getDefault()) + "'"; + } + + if (!isEmpty(getField(i)->getDbDescription())) + statement += std::string(" comment '") + getField(i)->getDbDescription() + std::string("'"); + } + + aKey = ""; + + for (int i = 0, n = 0; i < fieldCount(); i++) + { + if (getField(i)->getType() & ftPrimary) + { + if (n++) aKey += std::string(", "); + aKey += std::string(getField(i)->getDbName()) + " DESC"; + } + } + + if (aKey.length()) + { + statement += std::string(", PRIMARY KEY("); + statement += aKey; + statement += ")"; + } + + aKey = ""; + + for (int i = 0, n = 0; i < fieldCount(); i++) + { + if (getField(i)->getType() & ftAutoinc && !(getField(i)->getType() & ftPrimary)) + { + if (n++) aKey += std::string(", "); + aKey += std::string(getField(i)->getDbName()) + " DESC"; + } + } + + if (aKey.length()) + { + statement += std::string(", KEY("); + statement += aKey; + statement += ")"; + } + + statement += std::string(") ENGINE=InnoDB ROW_FORMAT=DYNAMIC;"); + + tell(1, "%s", statement.c_str()); + + if (connection->query("%s", statement.c_str())) + { + if (needDetach) detach(); + return connection->errorSql(getConnection(), "createTable()", + 0, statement.c_str()); + } + + if (needDetach) detach(); + + return success; +} + +//*************************************************************************** +// Create Indices +//*************************************************************************** + +int cDbTable::createIndices() +{ + std::string statement; + + tell(5, "Initialy checking indices for '%s'", TableName()); + + // check/create indexes + + for (int i = 0; i < tableDef->indexCount(); i++) + { + cDbIndexDef* index = tableDef->getIndex(i); + int fCount; + std::string idxName; + + if (!index->fieldCount()) + continue; + + // check + + idxName = "idx" + std::string(index->getName()); + + checkIndex(idxName.c_str(), fCount); + + if (fCount != index->fieldCount()) + { + // create index + + statement = "create index " + idxName; + statement += " on " + std::string(TableName()) + "("; + + int n = 0; + + for (int f = 0; f < index->fieldCount(); f++) + { + cDbFieldDef* fld = index->getField(f); + + if (fld) + { + if (n++) statement += std::string(", "); + statement += fld->getDbName(); + } + } + + if (!n) continue; + + statement += ");"; + tell(1, "%s", statement.c_str()); + + if (connection->query("%s", statement.c_str())) + return connection->errorSql(getConnection(), "createIndices()", + 0, statement.c_str()); + } + } + + return success; +} + +//*************************************************************************** +// Check Index +//*************************************************************************** + +int cDbTable::checkIndex(const char* idxName, int& fieldCount) +{ + enum IndexQueryFields + { + idTable, + idNonUnique, + idKeyName, + idSeqInIndex, + idColumnName, + idCollation, + idCardinality, + idSubPart, + idPacked, + idNull, + idIndexType, + idComment, + idIndexComment, + + idCount + }; + + MYSQL_RES* result; + MYSQL_ROW row; + + fieldCount = 0; + + if (connection->query("show index from %s", TableName()) != success) + { + connection->errorSql(getConnection(), "checkIndex()", 0); + + return fail; + } + + if ((result = mysql_store_result(connection->getMySql()))) + { + while ((row = mysql_fetch_row(result))) + { + tell(5, "%s: %-20s %s %s", + row[idTable], row[idKeyName], + row[idSeqInIndex], row[idColumnName]); + + if (strcasecmp(row[idKeyName], idxName) == 0) + fieldCount++; + } + + mysql_free_result(result); + + return success; + } + + connection->errorSql(getConnection(), "checkIndex()"); + + return fail; +} + +//*************************************************************************** +// Copy Values +//*************************************************************************** + +void cDbTable::copyValues(cDbRow* r, int typesFilter) +{ + std::map<std::string, cDbFieldDef*>::iterator f; + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + { + cDbFieldDef* fld = f->second; + + if (r->isNull(fld)) // skip where source field is NULL + continue; + + if (!(typesFilter & fld->getType())) // Filter + continue; + + switch (fld->getFormat()) + { + case ffAscii: + case ffText: + case ffMText: + case ffMlob: + row->setValue(fld, r->getStrValue(fld)); + break; + + case ffFloat: + row->setValue(fld, r->getFloatValue(fld)); + break; + + case ffDateTime: + row->setValue(fld, r->getTimeValue(fld)); + break; + + case ffBigInt: + case ffUBigInt: + row->setBigintValue(fld, r->getBigintValue(fld)); + break; + + case ffInt: + case ffUInt: + row->setValue(fld, r->getIntValue(fld)); + break; + + default: + tell(0, "Fatal unhandled field type %d", fld->getFormat()); + } + } +} + +//*************************************************************************** +// SQL Error +//*************************************************************************** + +int cDbConnection::errorSql(cDbConnection* connection, const char* prefix, + MYSQL_STMT* stmt, const char* stmtTxt) +{ + if (!connection || !connection->mysql) + { + tell(0, "SQL-Error in '%s'", prefix); + return fail; + } + + int error = mysql_errno(connection->mysql); + char* conErr = 0; + char* stmtErr = 0; + + if (error == CR_SERVER_LOST || + error == CR_SERVER_GONE_ERROR || + error == CR_INVALID_CONN_HANDLE || + error == CR_COMMANDS_OUT_OF_SYNC || + error == CR_SERVER_LOST_EXTENDED || + error == CR_STMT_CLOSED || + error == CR_CONN_UNKNOW_PROTOCOL || + error == CR_UNSUPPORTED_PARAM_TYPE || + error == CR_NO_PREPARE_STMT || + error == CR_SERVER_HANDSHAKE_ERR || + error == CR_WRONG_HOST_INFO || + error == CR_OUT_OF_MEMORY || + error == CR_IPSOCK_ERROR || + error == CR_SOCKET_CREATE_ERROR || + error == CR_CONNECTION_ERROR || + error == CR_TCP_CONNECTION || + error == CR_PARAMS_NOT_BOUND || + error == CR_CONN_HOST_ERROR || + error == CR_SSL_CONNECTION_ERROR + + // to be continued - not all errors should result in a reconnect ... + + ) + { + connectDropped = yes; + } + + if (error) + asprintf(&conErr, "%s (%d) ", mysql_error(connection->mysql), error); + + if (stmt || stmtTxt) + asprintf(&stmtErr, "'%s' [%s]", + stmt ? mysql_stmt_error(stmt) : "", + stmtTxt ? stmtTxt : ""); + + tell(0, "SQL-Error in '%s' - %s%s", prefix, + conErr ? conErr : "", stmtErr ? stmtErr : ""); + + free(conErr); + free(stmtErr); + + if (connectDropped) + tell(0, "Fatal, lost connection to mysql server, aborting pending actions"); + + return fail; +} + +//*************************************************************************** +// Delete Where +//*************************************************************************** + +int cDbTable::deleteWhere(const char* where, ...) +{ + std::string stmt; + char* tmp; + va_list more; + + if (!connection || !connection->getMySql()) + return fail; + + va_start(more, where); + vasprintf(&tmp, where, more); + + stmt = "delete from " + std::string(TableName()) + " where " + std::string(tmp); + + free(tmp); + + if (connection->query("%s", stmt.c_str())) + return connection->errorSql(connection, "deleteWhere()", 0, stmt.c_str()); + + return success; +} + +//*************************************************************************** +// Coiunt Where +//*************************************************************************** + +int cDbTable::countWhere(const char* where, int& count, const char* what) +{ + std::string tmp; + MYSQL_RES* res; + MYSQL_ROW data; + + count = 0; + + if (isEmpty(what)) + what = "count(1)"; + + if (!isEmpty(where)) + tmp = "select " + std::string(what) + " from " + std::string(TableName()) + " where " + std::string(where); + else + tmp = "select " + std::string(what) + " from " + std::string(TableName()); + + if (connection->query("%s", tmp.c_str())) + return connection->errorSql(connection, "countWhere()", 0, tmp.c_str()); + + if ((res = mysql_store_result(connection->getMySql()))) + { + data = mysql_fetch_row(res); + + if (data) + count = atoi(data[0]); + + mysql_free_result(res); + } + + return success; +} + +//*************************************************************************** +// Truncate +//*************************************************************************** + +int cDbTable::truncate() +{ + std::string tmp; + + tmp = "delete from " + std::string(TableName()); + + if (connection->query("%s", tmp.c_str())) + return connection->errorSql(connection, "truncate()", 0, tmp.c_str()); + + return success; +} + + +//*************************************************************************** +// Store +//*************************************************************************** + +int cDbTable::store() +{ + int found; + + // insert or just update ... + + if (stmtSelect->execute(/*noResult =*/ yes) != success) + { + connection->errorSql(connection, "store()"); + return no; + } + + found = stmtSelect->getAffected() == 1; + stmtSelect->freeResult(); + + if (found) + return update(); + else + return insert(); +} + +//*************************************************************************** +// Insert +//*************************************************************************** + +int cDbTable::insert(time_t inssp) +{ + std::map<std::string, cDbFieldDef*>::iterator f; + lastInsertId = na; + + if (!stmtInsert) + { + tell(0, "Fatal missing insert statement\n"); + return fail; + } + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + { + cDbFieldDef* fld = f->second; + + if (strcasecmp(fld->getName(), "updsp") == 0 || strcasecmp(fld->getName(), "inssp") == 0) + setValue(fld, inssp ? inssp : time(0)); + + else if (getValue(fld)->isNull() && !isEmpty(fld->getDefault())) + setValue(fld, fld->getDefault()); + } + + if (stmtInsert->execute()) + return fail; + + lastInsertId = stmtInsert->getLastInsertId(); + + return stmtInsert->getAffected() == 1 ? success : fail; +} + +//*************************************************************************** +// Update +//*************************************************************************** + +int cDbTable::update(time_t updsp) +{ + std::map<std::string, cDbFieldDef*>::iterator f; + + if (!stmtUpdate) + { + tell(0, "Fatal missing update statement\n"); + return fail; + } + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + { + cDbFieldDef* fld = f->second; + + if (strcasecmp(fld->getName(), "updsp") == 0) + setValue(fld, updsp ? updsp : time(0)); + + else if (getValue(fld)->isNull() && !isEmpty(fld->getDefault())) + setValue(fld, fld->getDefault()); + } + + if (stmtUpdate->execute()) + return fail; + + return stmtUpdate->getAffected() == 1 ? success : fail; +} + +//*************************************************************************** +// Find +//*************************************************************************** + +int cDbTable::find() +{ + if (!stmtSelect) + return no; + + if (stmtSelect->execute() != success) + { + connection->errorSql(connection, "find()"); + return no; + } + + return stmtSelect->getAffected() == 1 ? yes : no; +} + +//*************************************************************************** +// Find via Statement +//*************************************************************************** + +int cDbTable::find(cDbStatement* stmt) +{ + if (!stmt) + return no; + + if (stmt->execute() != success) + { + connection->errorSql(connection, "find(stmt)"); + return no; + } + + return stmt->getAffected() > 0 ? yes : no; +} + +//*************************************************************************** +// Fetch +//*************************************************************************** + +int cDbTable::fetch(cDbStatement* stmt) +{ + if (!stmt) + return no; + + return stmt->fetch(); +} + +//*************************************************************************** +// Reset Fetch +//*************************************************************************** + +void cDbTable::reset(cDbStatement* stmt) +{ + if (stmt) + stmt->freeResult(); +} diff --git a/lib/db.h b/lib/db.h new file mode 100644 index 0000000..e16aeb8 --- /dev/null +++ b/lib/db.h @@ -0,0 +1,1370 @@ +/* + * db.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __DB_H +#define __DB_H + +#include <linux/unistd.h> + +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <stdarg.h> +#include <errno.h> + +#include <mysql/mysql.h> + +#include <list> + +#include "common.h" +#include "dbdict.h" + +class cDbTable; +class cDbConnection; + +//*************************************************************************** +// cDbValue +//*************************************************************************** + +class cDbValue : public cDbService +{ + public: + + cDbValue(cDbFieldDef* f = 0) + { + field = 0; + strValue = 0; + ownField = 0; + changed = 0; + + if (f) setField(f); + } + + cDbValue(const char* name, FieldFormat format, int size) + { + strValue = 0; + changed = 0; + ownField = new cDbFieldDef(name, name, format, size, ftData, 0); + + field = ownField; + strValue = (char*)calloc(field->getSize()+TB, sizeof(char)); + + clear(); + } + + virtual ~cDbValue() + { + free(); + } + + void free() + { + clear(); + ::free(strValue); + strValue = 0; + + if (ownField) + { + delete ownField; + ownField = 0; + } + + field = 0; + } + + void clear() + { + if (strValue) + *strValue = 0; + + strValueSize = 0; + numValue = 0; + longlongValue = 0; + floatValue = 0; + memset(&timeValue, 0, sizeof(timeValue)); + + nullValue = 1; + changed = 0; + } + + void clearChanged() + { + changed = 0; + } + + virtual void setField(cDbFieldDef* f) + { + free(); + field = f; + + if (field) + strValue = (char*)calloc(field->getSize()+TB, sizeof(char)); + } + + virtual cDbFieldDef* getField() { return field; } + virtual const char* getName() { return field->getName(); } + virtual const char* getDbName() { return field->getDbName(); } + + void setNull() + { + int c = changed; + int n = nullValue; + + clear(); + changed = c; + + if (!n) + changed++; + } + + void __attribute__ ((format(printf, 2, 3))) sPrintf(const char* format, ...) + { + va_list more; + char* buf = 0; + + if (!format) + return ; + + va_start(more, format); + vasprintf(&buf, format, more); + + setValue(buf); + + ::free(buf); + } + + void setValue(const char* value, int size = 0) + { + int modified = no; + + if (field->getFormat() != ffAscii && field->getFormat() != ffText && + field->getFormat() != ffMText && field->getFormat() != ffMlob) + { + tell(0, "Setting invalid field format for '%s', expected ASCII, TEXT or MLOB", + field->getName()); + return; + } + + if (field->getFormat() == ffMlob && !size) + { + tell(0, "Missing size for MLOB field '%s'", field->getName()); + return; + } + + if (value && size) + { + if (size > field->getSize()) + { + tell(0, "Warning, size of %d for '%s' exeeded, got %d bytes!", + field->getSize(), field->getName(), size); + + size = field->getSize(); + } + + if (memcmp(strValue, value, size) != 0 || isNull()) + modified = yes; + + clear(); + memcpy(strValue, value, size); + strValue[size] = 0; + strValueSize = size; + nullValue = 0; + } + + else if (value) + { + if (strlen(value) > (size_t)field->getSize()) + tell(0, "Warning, size of %d for '%s' exeeded (needed %ld) [%s]", + field->getSize(), field->getName(), (long)strlen(value), value); + + if (strncmp(strValue, value, strlen(value)) != 0 || isNull()) + modified = yes; + + clear(); + sprintf(strValue, "%.*s", field->getSize(), value); + strValueSize = strlen(strValue); + nullValue = 0; + } + + if (modified) // increment changed after calling clear() + changed++; + } + + void setCharValue(char value) + { + char tmp[2] = ""; + tmp[0] = value; + tmp[1] = 0; + setValue(tmp); + } + + void setValue(int value) + { + setValue((long)value); + } + + void setValue(long value) + { + if (field->getFormat() == ffInt || field->getFormat() == ffUInt) + { + if (numValue != value || isNull()) + changed++; + + numValue = value; + nullValue = 0; + } + else if (field->getFormat() == ffDateTime) + { + struct tm tm; + time_t v = value; + time_t o = getTimeValue(); + + memset(&tm, 0, sizeof(tm)); + localtime_r(&v, &tm); + + timeValue.year = tm.tm_year + 1900; + timeValue.month = tm.tm_mon + 1; + timeValue.day = tm.tm_mday; + + timeValue.hour = tm.tm_hour; + timeValue.minute = tm.tm_min; + timeValue.second = tm.tm_sec; + + nullValue = 0; + + if (o != getTimeValue()) + changed++; + } + else + { + tell(0, "Setting invalid field format for '%s'", field->getName()); + } + } + + void setValue(double value) + { + if (field->getFormat() == ffInt || field->getFormat() == ffUInt) + { + if (numValue != value || isNull()) + changed++; + + numValue = value; + nullValue = 0; + } + else if (field->getFormat() == ffBigInt || field->getFormat() == ffUBigInt) + { + if (longlongValue != value || isNull()) + changed++; + + longlongValue = value; + nullValue = 0; + } + else if (field->getFormat() == ffFloat) + { + if (floatValue != value || isNull()) + changed++; + + floatValue = value; + nullValue = 0; + } + else + { + tell(0, "Setting invalid field format for '%s'", field->getName()); + } + } + + void setBigintValue(int64_t value) + { + if (field->getFormat() == ffInt || field->getFormat() == ffUInt) + { + if (numValue != value) + changed++; + + numValue = value; + nullValue = 0; + } + + else if (field->getFormat() == ffBigInt || field->getFormat() == ffUBigInt) + { + if (longlongValue != value) + changed++; + + longlongValue = value; + nullValue = 0; + } + } + + int hasValue(long value) + { + if (field->getFormat() == ffInt || field->getFormat() == ffUInt) + return numValue == value; + + if (field->getFormat() == ffDateTime) + return no; // to be implemented! + + tell(0, "Setting invalid field format for '%s'", field->getName()); + + return no; + } + + int hasValue(double value) + { + if (field->getFormat() == ffInt || field->getFormat() == ffUInt) + return numValue == value; + + if (field->getFormat() == ffBigInt || field->getFormat() == ffUBigInt) + return longlongValue == value; + + if (field->getFormat() == ffFloat) + return floatValue == value; + + tell(0, "Setting invalid field format for '%s'", field->getName()); + + return no; + } + + int hasValue(const char* value) + { + if (!value) + value = ""; + + if (field->getFormat() != ffAscii && field->getFormat() != ffText && + field->getFormat() != ffMText && field->getFormat() != ffMlob) + { + tell(0, "Checking invalid field format for '%s', expected ASCII or TEXT", + field->getName()); + return no; + } + + return strcmp(getStrValue(), value) == 0; + } + + int hasCharValue(char value) + { + if (field->getFormat() != ffAscii) + { + tell(0, "Checking invalid field format for '%s', expected ASCII or TEXT", + field->getName()); + return no; + } + + return getStrValueSize() == 1 && toupper(getCharValue()) == toupper(value); + } + + time_t getTimeValue() + { + struct tm tm; + memset(&tm, 0, sizeof(tm)); + + tm.tm_isdst = -1; // force DST auto detect + tm.tm_year = timeValue.year - 1900; + tm.tm_mon = timeValue.month - 1; + tm.tm_mday = timeValue.day; + + tm.tm_hour = timeValue.hour; + tm.tm_min = timeValue.minute; + tm.tm_sec = timeValue.second; + + return mktime(&tm); + } + + unsigned long* getStrValueSizeRef() { return &strValueSize; } + unsigned long getStrValueSize() { return strValueSize; } + const char* getStrValue() { return !isNull() && strValue ? strValue : ""; } + char getCharValue() { return !isNull() && strValue ? strValue[0] : 0; } + long getIntValue() { return !isNull() ? numValue : 0; } + + int64_t getBigintValue() + { + if (isNull()) + return 0; + + if (field->getFormat() == ffBigInt || field->getFormat() == ffUBigInt) + return longlongValue; + + return numValue; + } + + float getFloatValue() { return !isNull() ? floatValue : 0; } + int isNull() { return nullValue; } + int getChanges() { return changed; } + + int isEmpty() + { + if (isNull()) + return yes; + + if (field->getFormat() == ffInt || field->getFormat() == ffUInt) + return numValue == 0; + else if (field->getFormat() == ffDateTime) + return no; + else if (field->getFormat() == ffAscii || field->getFormat() == ffText || + field->getFormat() == ffMText || field->getFormat() == ffMlob) + return ::isEmpty(strValue); + else if (field->getFormat() == ffFloat) + return floatValue == 0; + + return no; + } + + char* getStrValueRef() { return strValue; } + long* getIntValueRef() { return &numValue; } + int64_t* getBigIntValueRef() { return &longlongValue; } + MYSQL_TIME* getTimeValueRef() { return &timeValue; } + float* getFloatValueRef() { return &floatValue; } + my_bool* getNullRef() { return &nullValue; } + + private: + + cDbFieldDef* ownField; + cDbFieldDef* field; + long numValue; + int64_t longlongValue; + float floatValue; + MYSQL_TIME timeValue; + char* strValue; + unsigned long strValueSize; + my_bool nullValue; + int changed; +}; + +//*************************************************************************** +// cDbStatement +//*************************************************************************** + +class cDbStatement : public cDbService +{ + public: + + cDbStatement(cDbTable* aTable); + cDbStatement(cDbConnection* aConnection, const char* sText = ""); + virtual ~cDbStatement(); + + int execute(int noResult = no); + int find(); + int fetch(); + int freeResult(); + void clear(); + + // interface + + virtual int __attribute__ ((format(printf, 2, 3))) build(const char* format, ...); + + void setBindPrefix(const char* p) { bindPrefix = p; } + void clrBindPrefix() { bindPrefix = 0; } + int bind(const char* fname, int mode, const char* delim = 0); + int bind(cDbValue* value, int mode, const char* delim = 0); + int bind(cDbTable* aTable, cDbFieldDef* field, int mode, const char* delim); + int bind(cDbTable* aTable, const char* fname, int mode, const char* delim); + int bind(cDbFieldDef* field, int mode, const char* delim = 0); + int bindAllOut(const char* delim = 0, int incTypes = ftData | ftPrimary, int excTypes = 0); + + int bindCmp(const char* ctable, cDbValue* value, + const char* comp, const char* delim = 0); + int bindCmp(const char* ctable, cDbFieldDef* field, cDbValue* value, + const char* comp, const char* delim = 0); + int bindCmp(const char* ctable, const char* fname, cDbValue* value, + const char* comp, const char* delim = 0); + int bindText(const char* text, cDbValue* value, + const char* comp, const char* delim = 0); + int bindTextFree(const char* text, cDbValue* value, int mode = bndIn); + + int bindInChar(const char* ctable, const char* fname, + cDbValue* value = 0, const char* delim = 0); + + int appendBinding(cDbValue* value, BindType bt); // use this interface method seldom from external and with care! + + // .. + + int prepare(); + int getAffected() { return affected; } + int getResultCount(); + int getLastInsertId(); + const char* asText() { return stmtTxt.c_str(); } + cDbTable* getTable() { return table; } + void showStat(); + + // data + + static int explain; // debug explain + + private: + + std::string stmtTxt; + MYSQL_STMT* stmt; + int affected; + cDbConnection* connection; + cDbTable* table; + int inCount; + MYSQL_BIND* inBind; // to db + int outCount; + MYSQL_BIND* outBind; // from db (result) + MYSQL_RES* metaResult; + const char* bindPrefix; + int firstExec; // debug explain + int buildErrors; + + unsigned long callsPeriod; + unsigned long callsTotal; + double duration; +}; + +//*************************************************************************** +// cDbStatements +//*************************************************************************** + +class cDbStatements +{ + public: + + cDbStatements() { statisticPeriod = time(0); } + ~cDbStatements() {}; + + void append(cDbStatement* s) { statements.push_back(s); } + void remove(cDbStatement* s) { statements.remove(s); } + + void showStat(const char* name) + { + tell(0, "Statement statistic of last %ld seconds from '%s':", time(0) - statisticPeriod, name); + + for (std::list<cDbStatement*>::iterator it = statements.begin() ; it != statements.end(); ++it) + { + if (*it) + (*it)->showStat(); + } + + statisticPeriod = time(0); + } + + private: + + time_t statisticPeriod; + std::list<cDbStatement*> statements; +}; + +//*************************************************************************** +// Class Database Row +//*************************************************************************** + +#define GET_FIELD(name) \ + cDbFieldDef* f = tableDef->getField(name); \ + if (!f) \ + { \ + tell(0, "Fatal: Field '%s.%s' not defined (missing in dictionary)", tableDef->getName(), name); \ + return ; \ + } \ + +#define GET_FIELD_RES(name, def) \ + cDbFieldDef* f = tableDef->getField(name); \ + if (!f) \ + { \ + tell(0, "Fatal: Field '%s.%s' not defined (missing in dictionary)", tableDef->getName(), name); \ + return def; \ + } \ + +class cDbRow : public cDbService +{ + public: + + cDbRow(cDbTableDef* t) + { + set(t); + } + + cDbRow(const char* name) + { + cDbTableDef* t = dbDict.getTable(name); + + if (t) + set(t); + else + tell(0, "Fatal: Table '%s' missing in dictionary '%s'!", name, dbDict.getPath()); + } + + virtual ~cDbRow() { delete[] dbValues; } + + void set(cDbTableDef* t) + { + std::map<std::string, cDbFieldDef*>::iterator f; + + tableDef = t; + dbValues = new cDbValue[tableDef->fieldCount()]; + + for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++) + dbValues[f->second->getIndex()].setField(f->second); + } + + void clear() + { + for (int f = 0; f < tableDef->fieldCount(); f++) + dbValues[f].clear(); + } + + void clearChanged() + { + for (int f = 0; f < tableDef->fieldCount(); f++) + dbValues[f].clearChanged(); + } + + int getChanges() + { + int count = 0; + + for (int f = 0; f < tableDef->fieldCount(); f++) + count += dbValues[f].getChanges(); + + return count; + } + + std::string getChangedFields() + { + std::string s = ""; + + for (int f = 0; f < tableDef->fieldCount(); f++) + { + if (dbValues[f].getChanges()) + { + if (s.length()) + s += ","; + + s += dbValues[f].getName() + std::string("="); + + if (dbValues[f].getField()->hasFormat(ffInt) || dbValues[f].getField()->hasFormat(ffUInt)) + s += num2Str(dbValues[f].getIntValue()); + else + s += dbValues[f].getStrValue(); + } + } + + return s; + } + + virtual cDbFieldDef* getField(int id) { return tableDef->getField(id); } + virtual cDbFieldDef* getField(const char* name) { return tableDef->getField(name); } + virtual cDbFieldDef* getFieldByDbName(const char* dbname) { return tableDef->getFieldByDbName(dbname); } + virtual int fieldCount() { return tableDef->fieldCount(); } + + void setValue(cDbFieldDef* f, const char* value, + int size = 0) { dbValues[f->getIndex()].setValue(value, size); } + void setValue(cDbFieldDef* f, int value) { dbValues[f->getIndex()].setValue(value); } + void setValue(cDbFieldDef* f, long value) { dbValues[f->getIndex()].setValue(value); } + void setValue(cDbFieldDef* f, double value) { dbValues[f->getIndex()].setValue(value); } + void setBigintValue(cDbFieldDef* f, int64_t value) { dbValues[f->getIndex()].setBigintValue(value); } + void setCharValue(cDbFieldDef* f, char value) { dbValues[f->getIndex()].setCharValue(value); } + + void setValue(const char* n, const char* value, + int size = 0) { GET_FIELD(n); dbValues[f->getIndex()].setValue(value, size); } + void setValue(const char* n, int value) { GET_FIELD(n); dbValues[f->getIndex()].setValue(value); } + void setValue(const char* n, long value) { GET_FIELD(n); dbValues[f->getIndex()].setValue(value); } + void setValue(const char* n, double value) { GET_FIELD(n); dbValues[f->getIndex()].setValue(value); } + void setBigintValue(const char* n, int64_t value) { GET_FIELD(n); dbValues[f->getIndex()].setBigintValue(value); } + void setCharValue(const char* n, char value) { GET_FIELD(n); dbValues[f->getIndex()].setCharValue(value); } + + int hasValue(cDbFieldDef* f, const char* value) const { return dbValues[f->getIndex()].hasValue(value); } + int hasCharValue(cDbFieldDef* f, char value) const { return dbValues[f->getIndex()].hasCharValue(value); } + int hasValue(cDbFieldDef* f, long value) const { return dbValues[f->getIndex()].hasValue(value); } + int hasValue(cDbFieldDef* f, double value) const { return dbValues[f->getIndex()].hasValue(value); } + + int hasValue(const char* n, const char* value) const { GET_FIELD_RES(n, no); return dbValues[f->getIndex()].hasValue(value); } + int hasCharValue(const char* n, char value) const { GET_FIELD_RES(n, no); return dbValues[f->getIndex()].hasCharValue(value); } + int hasValue(const char* n, long value) const { GET_FIELD_RES(n, no); return dbValues[f->getIndex()].hasValue(value); } + int hasValue(const char* n, double value) const { GET_FIELD_RES(n, no); return dbValues[f->getIndex()].hasValue(value); } + + cDbValue* getValue(cDbFieldDef* f) { return &dbValues[f->getIndex()]; } + cDbValue* getValue(const char* n) { GET_FIELD_RES(n, 0); return &dbValues[f->getIndex()]; } + + time_t getTimeValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getTimeValue(); } + const char* getStrValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getStrValue(); } + long getIntValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getIntValue(); } + int64_t getBigintValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getBigintValue(); } + float getFloatValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getFloatValue(); } + int isNull(cDbFieldDef* f) const { return dbValues[f->getIndex()].isNull(); } + + const char* getStrValue(const char* n) const { GET_FIELD_RES(n, ""); return dbValues[f->getIndex()].getStrValue(); } + long getIntValue(const char* n) const { GET_FIELD_RES(n, 0); return dbValues[f->getIndex()].getIntValue(); } + int64_t getBigintValue(const char* n) const { GET_FIELD_RES(n, 0); return dbValues[f->getIndex()].getBigintValue(); } + float getFloatValue(const char* n) const { GET_FIELD_RES(n, 0); return dbValues[f->getIndex()].getFloatValue(); } + int isNull(const char* n) const { GET_FIELD_RES(n, yes); return dbValues[f->getIndex()].isNull(); } + + cDbTableDef* getTableDef() { return tableDef; } + + protected: + + cDbTableDef* tableDef; + cDbValue* dbValues; +}; + +//*************************************************************************** +// Connection +//*************************************************************************** + +class cDbConnection +{ + public: + + cDbConnection() + { + mysql = 0; + attached = 0; + inTact = no; + connectDropped = yes; + } + + virtual ~cDbConnection() + { + close(); + } + + int isConnected() { return getMySql() != 0; } + + int attachConnection() + { + static int first = yes; + + if (!mysql) + { + connectDropped = yes; + + tell(0, "Calling mysql_init(%ld)", syscall(__NR_gettid)); + + if (!(mysql = mysql_init(0))) + return errorSql(this, "attachConnection(init)"); + + if (!mysql_real_connect(mysql, dbHost, + dbUser, dbPass, dbName, dbPort, 0, 0)) + { + close(); + + tell(0, "Error, connecting to database at '%s' on port (%d) failed", + dbHost, dbPort); + + return fail; + } + + connectDropped = no; + + // init encoding + + if (encoding && *encoding) + { + if (mysql_set_character_set(mysql, encoding)) + errorSql(this, "init(character_set)"); + + if (first) + { + tell(0, "SQL client character now '%s'", mysql_character_set_name(mysql)); + first = no; + } + } + } + + attached++; + + return success; + } + + void detachConnection() + { + attached--; + + if (!attached) + close(); + } + + void close() + { + if (mysql) + { + tell(0, "Closing mysql connection and calling mysql_thread_end(%ld)", syscall(__NR_gettid)); + + mysql_close(mysql); + mysql_thread_end(); + mysql = 0; + attached = 0; + } + } + + int check() + { + if (!isConnected()) + return fail; + + query("SELECT SYSDATE();"); + queryReset(); + + return isConnected() ? success : fail; + } + + virtual int __attribute__ ((format(printf, 2, 3))) query(const char* format, ...) + { + va_list more; + + if (!format) + return fail; + + va_start(more, format); + + return vquery(format, more); + } + + virtual int __attribute__ ((format(printf, 3, 4))) query(int& count, const char* format, ...) + { + int status; + va_list more; + + count = 0; + + if (!format) + return fail; + + va_start(more, format); + + if ((status = vquery(format, more)) == success) + { + MYSQL_RES* res; + MYSQL_ROW data; + + // get affected rows .. + + if ((res = mysql_store_result(getMySql()))) + { + data = mysql_fetch_row(res); + + if (data) + count = atoi(data[0]); + + mysql_free_result(res); + } + } + + return status; + } + + virtual int vquery(const char* format, va_list more) + { + int status = 1; + MYSQL* h = getMySql(); + + if (h && format) + { + char* stmt; + + vasprintf(&stmt, format, more); + + if ((status = mysql_query(h, stmt))) + errorSql(this, stmt); + + free(stmt); + } + + return status ? fail : success; + } + + virtual void queryReset() + { + if (getMySql()) + { + MYSQL_RES* result = mysql_use_result(getMySql()); + mysql_free_result(result); + } + } + + // escapeSqlString - only need to be used in string statements not in bind values!! + + virtual std::string escapeSqlString(const char* str) + { + std::string result = ""; + + if (!isConnected()) + return result; + + int length = strlen(str); + int bufferSize = length*2 + TB; + + char* buffer = (char*)malloc(bufferSize); + mysql_real_escape_string(getMySql(), buffer, str, length); + result = buffer; + free(buffer); + + return result; + } + + virtual int executeSqlFile(const char* file) + { + FILE* f; + int res; + char* buffer; + int size = 1000; + int nread = 0; + + if (!getMySql()) + return fail; + + if (!(f = fopen(file, "r"))) + { + tell(0, "Fatal: Can't execute sql file '%s'; Error was '%s'", file, strerror(errno)); + return fail; + } + + buffer = (char*)malloc(size+1); + + while ((res = fread(buffer+nread, 1, 1000, f))) + { + nread += res; + size += 1000; + buffer = srealloc(buffer, size+1); + } + + fclose(f); + buffer[nread] = 0; + + // execute statement + + tell(2, "Executing '%s'", buffer); + + if (query("%s", buffer)) + { + free(buffer); + return errorSql(this, "executeSqlFile()"); + } + + free(buffer); + + return success; + } + + virtual int startTransaction() + { + inTact = yes; + return query("START TRANSACTION"); + } + + virtual int commit() + { + inTact = no; + return query("COMMIT"); + } + + virtual int rollback() + { + inTact = no; + return query("ROLLBACK"); + } + + virtual int inTransaction() { return inTact; } + + MYSQL* getMySql() + { + if (connectDropped) + close(); + + return mysql; + } + + int getAttachedCount() { return attached; } + void showStat(const char* name = "") { statements.showStat(name); } + int errorSql(cDbConnection* mysql, const char* prefix, MYSQL_STMT* stmt = 0, const char* stmtTxt = 0); + + // data + + cDbStatements statements; // all statements of this connection + + // -------------- + // static stuff + + // set/get connecting data + + static void setHost(const char* s) { free(dbHost); dbHost = strdup(s); } + static const char* getHost() { return dbHost; } + static void setName(const char* s) { free(dbName); dbName = strdup(s); } + static const char* getName() { return dbName; } + static void setUser(const char* s) { free(dbUser); dbUser = strdup(s); } + static const char* getUser() { return dbUser; } + static void setPass(const char* s) { free(dbPass); dbPass = strdup(s); } + static const char* getPass() { return dbPass; } + static void setPort(int port) { dbPort = port; } + static int getPort() { return dbPort; } + static void setEncoding(const char* enc) { free(encoding); encoding = strdup(enc); } + static const char* getEncoding() { return encoding; } + static void setConfPath(const char* cpath) { free(confPath); confPath = strdup(cpath); } + static const char* getConfPath() { return confPath; } + + // ----------------------------------------------------------- + // init() and exit() must exactly called 'once' per process + + static int init() + { + int status = success; + + initMutex.Lock(); + + if (!initThreads) + { + tell(1, "Info: Calling mysql_library_init()"); + + if (mysql_library_init(0, 0, 0)) + { + tell(0, "Error: mysql_library_init() failed"); + status = fail; + } + } + else + { + tell(1, "Info: Skipping calling mysql_library_init(), it's already done!"); + } + + initThreads++; + initMutex.Unlock(); + + return status; + } + + static int exit() + { + initMutex.Lock(); + + initThreads--; + + if (!initThreads) + { + tell(1, "Info: Released the last usage of mysql_lib, calling mysql_library_end() now"); + mysql_library_end(); + + free(dbHost); + free(dbUser); + free(dbPass); + free(dbName); + free(encoding); + free(confPath); + } + else + { + tell(1, "Info: The mysql_lib is still in use, skipping mysql_library_end() call"); + } + + initMutex.Unlock(); + + return done; + } + + private: + + MYSQL* mysql; + + int initialized; + int attached; + int inTact; + int connectDropped; + + static cMyMutex initMutex; + static int initThreads; + + static char* encoding; + static char* confPath; + + // connecting data + + static char* dbHost; + static int dbPort; + static char* dbName; // database name + static char* dbUser; + static char* dbPass; +}; + +//*************************************************************************** +// cDbTable +//*************************************************************************** + +class cDbTable : public cDbService +{ + public: + + cDbTable(cDbConnection* aConnection, const char* name); + virtual ~cDbTable(); + + virtual const char* TableName() { return tableDef ? tableDef->getName() : "<unknown>"; } + virtual int fieldCount() { return tableDef->fieldCount(); } + cDbFieldDef* getField(int f) { return tableDef->getField(f); } + cDbFieldDef* getField(const char* name) { return tableDef->getField(name); } + + virtual int open(int allowAlter = 0); // 0 - off, 1 - on, 2 on with allow drop unused columns + virtual int close(); + virtual int attach(); + virtual int detach(); + int isAttached() { return attached; } + + virtual int find(); + virtual void reset() { reset(stmtSelect); } + + virtual int find(cDbStatement* stmt); + virtual int fetch(cDbStatement* stmt); + virtual void reset(cDbStatement* stmt); + + virtual int insert(time_t inssp = 0); + virtual int update(time_t updsp = 0); + virtual int store(); + + virtual int __attribute__ ((format(printf, 2, 3))) deleteWhere(const char* where, ...); + virtual int countWhere(const char* where, int& count, const char* what = 0); + virtual int truncate(); + + // interface to cDbRow + + void clear() { row->clear(); } + void clearChanged() { row->clearChanged(); } + int getChanges() { return row->getChanges(); } + std::string getChangedFields() { return row->getChangedFields(); } + void setValue(cDbFieldDef* f, const char* value, int size = 0) { row->setValue(f, value, size); } + void setValue(cDbFieldDef* f, int value) { row->setValue(f, value); } + void setValue(cDbFieldDef* f, long value) { row->setValue(f, value); } + void setValue(cDbFieldDef* f, double value) { row->setValue(f, value); } + void setBigintValue(cDbFieldDef* f, int64_t value) { row->setBigintValue(f, value); } + void setCharValue(cDbFieldDef* f, char value) { row->setCharValue(f, value); } + + void setValue(const char* n, const char* value, int size = 0) { row->setValue(n, value, size); } + void setValue(const char* n, int value) { row->setValue(n, value); } + void setValue(const char* n, long value) { row->setValue(n, value); } + void setValue(const char* n, double value) { row->setValue(n, value); } + void setBigintValue(const char* n, int64_t value) { row->setBigintValue(n, value); } + void setCharValue(const char* n, char value) { row->setCharValue(n, value); } + + void copyValues(cDbRow* r, int types = ftData); + + int hasValue(cDbFieldDef* f, const char* value) { return row->hasValue(f, value); } + int hasCharValue(cDbFieldDef* f, char value) { return row->hasCharValue(f, value); } + int hasValue(cDbFieldDef* f, long value) { return row->hasValue(f, value); } + int hasValue(cDbFieldDef* f, double value) { return row->hasValue(f, value); } + + int hasValue(const char* n, const char* value) { return row->hasValue(n, value); } + int hasCharValue(const char* n, char value) { return row->hasCharValue(n, value); } + int hasValue(const char* n, long value) { return row->hasValue(n, value); } + int hasValue(const char* n, double value) { return row->hasValue(n, value); } + + const char* getStrValue(cDbFieldDef* f) const { return row->getStrValue(f); } + long getIntValue(cDbFieldDef* f) const { return row->getIntValue(f); } + int64_t getBigintValue(cDbFieldDef* f) const { return row->getBigintValue(f); } + float getFloatValue(cDbFieldDef* f) const { return row->getFloatValue(f); } + int isNull(cDbFieldDef* f) const { return row->isNull(f); } + + const char* getStrValue(const char* n) const { return row->getStrValue(n); } + long getIntValue(const char* n) const { return row->getIntValue(n); } + int64_t getBigintValue(const char* n) const { return row->getBigintValue(n); } + float getFloatValue(const char* n) const { return row->getFloatValue(n); } + int isNull(const char* n) const { return row->isNull(n); } + + cDbValue* getValue(cDbFieldDef* f) { return row->getValue(f); } + cDbValue* getValue(const char* fname) { return row->getValue(fname); } + int init(cDbValue*& dbvalue, const char* fname) { dbvalue = row->getValue(fname); return dbvalue ? success : fail; } + cDbRow* getRow() { return row; } + + cDbTableDef* getTableDef() { return tableDef; } + cDbConnection* getConnection() { return connection; } + MYSQL* getMySql() { return connection->getMySql(); } + int isConnected() { return connection && connection->getMySql(); } + + int getLastInsertId() { return lastInsertId; } + + virtual int exist(const char* name = 0); + virtual int validateStructure(int allowAlter = 1); // 0 - off, 1 - on, 2 on with allow drop unused columns + virtual int createTable(); + virtual int createIndices(); + + protected: + + virtual int init(int allowAlter = 0); // 0 - off, 1 - on, 2 on with allow drop unused columns + virtual int checkIndex(const char* idxName, int& fieldCount); + virtual int alterModifyField(cDbFieldDef* def); + virtual int alterAddField(cDbFieldDef* def); + virtual int alterDropField(const char* name); + + // data + + cDbRow* row; + int holdInMemory; // hold table additionally in memory (not implemented yet) + int attached; + int lastInsertId; + + cDbConnection* connection; + cDbTableDef* tableDef; + + // basic statements + + cDbStatement* stmtSelect; + cDbStatement* stmtInsert; + cDbStatement* stmtUpdate; +}; + +//*************************************************************************** +// cDbView +//*************************************************************************** + +class cDbView : public cDbService +{ + public: + + cDbView(cDbConnection* c, const char* aName) + { + connection = c; + name = strdup(aName); + } + + ~cDbView() { free(name); } + + int exist() + { + if (connection->getMySql()) + { + MYSQL_RES* result = mysql_list_tables(connection->getMySql(), name); + MYSQL_ROW tabRow = mysql_fetch_row(result); + mysql_free_result(result); + + return tabRow ? yes : no; + } + + return no; + } + + int create(const char* path, const char* sqlFile) + { + int status; + char* file = 0; + + asprintf(&file, "%s/%s", path, sqlFile); + + tell(0, "Creating view '%s' using definition in '%s'", + name, file); + + status = connection->executeSqlFile(file); + + free(file); + + return status; + } + + int drop() + { + tell(0, "Drop view '%s'", name); + + return connection->query("drop view %s", name); + } + + protected: + + cDbConnection* connection; + char* name; +}; + +//*************************************************************************** +// cDbProcedure +//*************************************************************************** + +class cDbProcedure : public cDbService +{ + public: + + cDbProcedure(cDbConnection* c, const char* aName, ProcType pt = ptProcedure) + { + connection = c; + type = pt; + name = strdup(aName); + } + + ~cDbProcedure() { free(name); } + + const char* getName() { return name; } + + int call(int ll = 1) + { + if (!connection || !connection->getMySql()) + return fail; + + cDbStatement stmt(connection); + + tell(ll, "Calling '%s'", name); + + stmt.build("call %s", name); + + if (stmt.prepare() != success || stmt.execute() != success) + return fail; + + tell(ll, "'%s' suceeded", name); + + return success; + } + + int created() + { + if (!connection || !connection->getMySql()) + return fail; + + cDbStatement stmt(connection); + + stmt.build("show %s status where name = '%s'", + type == ptProcedure ? "procedure" : "function", name); + + if (stmt.prepare() != success || stmt.execute() != success) + { + tell(0, "%s check of '%s' failed", + type == ptProcedure ? "Procedure" : "Function", name); + return no; + } + else + { + if (stmt.getResultCount() != 1) + return no; + } + + return yes; + } + + int create(const char* path) + { + int status; + char* file = 0; + + asprintf(&file, "%s/%s.sql", path, name); + + tell(1, "Creating %s '%s'", + type == ptProcedure ? "procedure" : "function", name); + + status = connection->executeSqlFile(file); + + free(file); + + return status; + } + + int drop() + { + tell(1, "Drop %s '%s'", type == ptProcedure ? "procedure" : "function", name); + + return connection->query("drop %s %s", type == ptProcedure ? "procedure" : "function", name); + } + + static int existOnFs(const char* path, const char* name) + { + int state; + char* file = 0; + + asprintf(&file, "%s/%s.sql", path, name); + state = fileExists(file); + + free(file); + + return state; + } + + protected: + + cDbConnection* connection; + ProcType type; + char* name; + +}; + +//*************************************************************************** +#endif //__DB_H diff --git a/lib/dbdict.c b/lib/dbdict.c new file mode 100644 index 0000000..1052a13 --- /dev/null +++ b/lib/dbdict.c @@ -0,0 +1,527 @@ +/* + * dbdict.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "errno.h" + +#include "common.h" +#include "dbdict.h" + +//*************************************************************************** +// Get Token +//*************************************************************************** + +int getToken(const char*& p, char* token, int size, char delimiter = ' ', char end = ',') +{ + char* dest = token; + int num = 0; + int insideStr = no; + + while (*p && (*p == ' ' || *p == delimiter)) + p++; + + if (*p == end || isEmpty(p)) + return fail; + + while (*p && num < size && !((*p == delimiter || *p == end) && !insideStr)) + { + if (*p == '"') + { + insideStr = !insideStr; + p++; + } + else + { + *dest++ = *p++; + num++; + } + } + + *dest = 0; + + return success; +} + +//*************************************************************************** +// cDbService +//*************************************************************************** + +const char* cDbService::formats[] = +{ + "INT", + "INT", + "BIGINT", + "BIGINT", + "VARCHAR", + "TEXT", + "MEDIUMTEXT", + "MEDIUMBLOB", + "FLOAT", + "DATETIME", + + 0 +}; + +const char* cDbService::toString(FieldFormat t) +{ + return formats[t]; +} + +const char* cDbService::dictFormats[] = +{ + "int", + "uint", + "bigint", + "ubigint", + "ascii", + "text", + "mtext", + "mlob", + "float", + "datetime", + + 0 +}; + +cDbService::FieldFormat cDbService::toDictFormat(const char* format) +{ + for (int i = 0; i < ffCount; i++) + if (strcasecmp(dictFormats[i], format) == 0) + return (FieldFormat)i; + + return ffUnknown; +} + +cDbService::TypeDef cDbService::types[] = +{ + { ftData, "data" }, + { ftPrimary, "primary" }, + { ftMeta, "meta" }, + { ftAutoinc, "autoinc" }, + + { ftUnknown, "" } +}; + +int cDbService::toType(const char* theTypes) +{ + const int sizeTokenMax = 100; + char token[sizeTokenMax+TB]; + const char* p = theTypes; + + int type = ftNo; + + // field can of more than one type -> bitmask + + while (getToken(p, token, sizeTokenMax, '|') == success) + { + for (int i = 0; types[i].type != ftUnknown; i++) + { + if (strcasecmp(types[i].name, token) == 0) + { + type |= types[i].type; + continue; + } + } + } + + return type; +} + +const char* cDbService::toName(FieldType type, char* buf) +{ + *buf = 0; + + // field can of more than one type -> bitmask !! + + for (int i = 0; types[i].type != ftUnknown; i++) + { + if (types[i].type & type) + { + if (!isEmpty(buf)) + strcat(buf, "|"); + + strcat(buf, types[i].name); + continue; + } + } + + return buf; +} + +//*************************************************************************** +//*************************************************************************** +// Class cDbDict +//*************************************************************************** + +cDbDict dbDict; + +//*************************************************************************** +// Object +//*************************************************************************** + +cDbDict::cDbDict() +{ + curTable = 0; + inside = no; + path = 0; + fieldFilter = 0; // 0 -> filter off use all fields + fltFromNameFct = 0; +} + +cDbDict::~cDbDict() +{ + std::map<std::string, cDbTableDef*>::iterator t; + + while ((t = tables.begin()) != tables.end()) + { + if (t->second) + delete t->second; + + tables.erase(t); + } + + free(path); +} + +//*************************************************************************** +// Get Table +//*************************************************************************** + +cDbTableDef* cDbDict::getTable(const char* aName) +{ + std::map<std::string, cDbTableDef*>::iterator t; + + if ((t = tables.find(aName)) != tables.end()) + return t->second; + + return 0; +} + +//*************************************************************************** +// Init +//*************************************************************************** + +int cDbDict::init(cDbFieldDef*& field, const char* tname, const char* fname) +{ + cDbTableDef* table = getTable(tname); + + if (table) + if ((field = table->getField(fname))) + return success; + + tell(0, "Fatal: Can't init field %s.%s, not found in dictionary", tname, fname); + + return fail; +} + +//*************************************************************************** +// In +//*************************************************************************** + +int cDbDict::in(const char* file, int filter) +{ + FILE* f; + char* line = 0; + size_t size = 0; + + if (isEmpty(file)) + return fail; + + fieldFilter = filter; + asprintf(&path, "%s", file); + + f = fopen(path, "r"); + + if (!f) + { + tell(0, "Error: Can' open file '%s', error was '%s'", path, strerror(errno)); + return fail; + } + + while (getline(&line, &size, f) > 0) + { + char* p = strstr(line, "//"); + + if (p) *p = 0; + + allTrim(line); + + if (isEmpty(line)) + continue; + + if (atLine(line) != success) + { + tell(0, "Error: Found unexpected definition '%s', aborting", line); + free(path); + return fail; + } + } + + fclose(f); + free(line); + + return success; +} + +//*************************************************************************** +// Forget +//*************************************************************************** + +void cDbDict::forget() +{ + std::map<std::string, cDbTableDef*>::iterator t; + + while ((t = tables.begin()) != tables.end()) + { + if (t->second) + delete t->second; + + tables.erase(t); + } + + free(path); + + curTable = 0; + inside = no; + path = 0; + fieldFilter = 0; // 0 -> filter off use all fields + fltFromNameFct = 0; +} + +//*************************************************************************** +// Show +//*************************************************************************** + +void cDbDict::show() +{ + std::map<std::string, cDbTableDef*>::iterator t; + + for (t = tables.begin(); t != tables.end(); t++) + { + tell(0, "-------------------------------------------------------------------------------------------------"); + tell(0, "Table '%s'", t->first.c_str()); + tell(0, "-------------------------------------------------------------------------------------------------"); + t->second->show(); + } +} + +//*************************************************************************** +// At Line +//*************************************************************************** + +int cDbDict::atLine(const char* line) +{ + int status = success; + static int prsTable = no; + static int prsIndex = no; + + const char* p; + + if (strncasecmp(line, "Table ", 6) == 0) + { + char tableName[100]; + + prsTable = yes; + curTable = 0; + p = line + strlen("Table "); + strcpy(tableName, p); + allTrim(tableName); + + if (!getTable(tableName)) + { + curTable = new cDbTableDef(tableName); + tables[tableName] = curTable; + } + else + tell(0, "Fatal: Table '%s' doubly defined", tableName); + } + else if (strncasecmp(line, "Index ", 6) == 0) + { + prsIndex = yes; + p = line + strlen("Table "); + } + + else if (strchr(line, '{')) + inside = yes; + + else if (strchr(line, '}')) + { + inside = no; + prsTable = no; + prsIndex = no; + } + + else if (inside && prsTable) + status += parseField(line); + + else if (inside && prsIndex) + status += parseIndex(line); + + else + tell(0, "Info: Ignoring extra line [%s]", line); + + return status; +} + +//*************************************************************************** +// Parse Filter +//*************************************************************************** + +int cDbDict::parseFilter(cDbFieldDef* f, const char* value) +{ + const int sizeNameMax = 50; + char name[sizeNameMax+TB]; + const char* v = value; + + f->filter = 0; + + while (getToken(v, name, sizeNameMax, '|') == success) + { + if (isalpha(*name) && fltFromNameFct) + f->filter |= fltFromNameFct(name); + else + f->filter |= atoi(name); + } + + return success; +} + +//*************************************************************************** +// Parse Field +//*************************************************************************** + +int cDbDict::parseField(const char* line) +{ + const int sizeTokenMax = 100; + char token[sizeTokenMax+TB]; + const char* p = line; + + cDbFieldDef* f = new cDbFieldDef; + f->filter = 0xFFFF; + + if (!curTable) + return fail; + + // first parse fixed part of field definition up to the 'type' + + for (int i = 0; i < dtCount; i++) + { + if (getToken(p, token, sizeTokenMax) != success) + { + if (i >= dtType) // all after type is optional (filter, ...) + break; + + delete f; + tell(0, "Error: Can't parse line [%s]", line); + return fail; + } + + if (i == dtCount-1 && strchr(token, ',')) + *(strchr(token, ',')) = 0; + + switch (i) + { + case dtName: f->name = strdup(token); break; + case dtDescription: f->setDescription(token); break; + case dtDbName: f->dbname = strdup(token); break; + case dtFormat: f->format = toDictFormat(token); break; + case dtSize: f->size = atoi(token); break; + case dtType: f->type = toType(token); break; + } + } + + // second ... parse dynamic part of the field definition like filter and default + + while (getToken(p, token, sizeTokenMax) == success) + { + char content[sizeTokenMax+TB]; + + if (getToken(p, content, sizeTokenMax) != success || isEmpty(content)) + { + tell(0, "Error: Skipping token '%s' missing content!", token); + break; + } + + if (strcasecmp(token, "filter") == 0) + parseFilter(f, content); + + else if (strcasecmp(token, "default") == 0) + f->def = strdup(content); + + else + tell(0, "Warning: Skipping unexpected token '%s'", token); + } + + if (!f->isValid()) + { + tell(0, "Error: Can't parse line [%s], invalid field configuration", line); + delete f; + return fail; + } + + if (f->filterMatch(fieldFilter)) + { + f->index = curTable->fieldCount(); + curTable->addField(f); + } + else + { + tell(2, "Info: Ignoring field '%s' due to filter configiuration", + f->getName()); + delete f; + } + + return success; +} + +//*************************************************************************** +// Parse Index +//*************************************************************************** + +int cDbDict::parseIndex(const char* line) +{ + const int sizeTokenMax = 100; + char token[sizeTokenMax+TB]; + const char* p = line; + int done = no; + + if (!curTable) + return fail; + + cDbIndexDef* index = new cDbIndexDef(); + + for (int i = 0; !done && i < 20; i++) + { + if (getToken(p, token, sizeTokenMax) != success) + { + if (i <= idtFields) + { + delete index; + tell(0, "Error: Can't parse line [%s]", line); + return fail; + } + + break; + } + + if (strchr(token, ',')) + { + done = yes; + *(strchr(token, ',')) = 0; + } + + if (i == idtName) + index->setName(token); + else if (i == idtDescription) + index->setDescription(token); + else if (i >= idtFields && i < idtFields+20) + index->addField(curTable->getField(token)); + } + + curTable->addIndex(index); + + return success; +} diff --git a/lib/dbdict.h b/lib/dbdict.h new file mode 100644 index 0000000..2999390 --- /dev/null +++ b/lib/dbdict.h @@ -0,0 +1,471 @@ +/* + * dbdict.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __DBDICT_H +#define __DBDICT_H + +#include <stdio.h> + +#include <vector> +#include <map> +#include <string> + +class cDbDict; + +typedef int (*FilterFromName)(const char* name); + +//*************************************************************************** +// _casecmp_ +//*************************************************************************** + +class _casecmp_ +{ + public: + + bool operator() (const std::string& a, const std::string& b) const + { + return strcasecmp(a.c_str(), b.c_str()) < 0; + } +}; + +//*************************************************************************** +// cDbService +//*************************************************************************** + +class cDbService +{ + public: + + enum Misc + { + maxIndexFields = 20 + }; + + enum FieldFormat + { + ffUnknown = na, + + ffInt, + ffUInt, + ffBigInt, + ffUBigInt, + ffAscii, // -> VARCHAR + ffText, + ffMText, + ffMlob, // -> MEDIUMBLOB + ffFloat, + ffDateTime, + + ffCount + }; + + enum FieldType + { + ftUnknown = na, + ftNo = 0, + + ftData = 1, + ftPrimary = 2, + ftMeta = 4, + ftAutoinc = 8, + + ftAll = ftData | ftPrimary | ftMeta + }; + + enum BindType + { + bndIn = 0x001, + bndOut = 0x002, + bndSet = 0x004 + }; + + enum ProcType + { + ptProcedure, + ptFunction + }; + + struct TypeDef + { + FieldType type; + const char* name; + }; + + static const char* toString(FieldFormat t); + static FieldFormat toDictFormat(const char* format); + static const char* formats[]; + static const char* dictFormats[]; + + static int toType(const char* type); + static const char* toName(FieldType type, char* buf); + static TypeDef types[]; +}; + +typedef cDbService cDBS; + +//*************************************************************************** +// cDbFieldDef +//*************************************************************************** + +class cDbFieldDef : public cDbService +{ + public: + + friend class cDbDict; + + cDbFieldDef() + { + name = 0; + dbname = 0, + format = ffUnknown; + size = na; + type = ftUnknown; + description = 0; + dbdescription = 0; + filter = 0xFFFF; + def = 0; + } + + cDbFieldDef(const char* n, const char* dn, FieldFormat f, int s, int t, int flt = 0xFFFF) + { + name = strdup(n); + dbname = strdup(dn); + format = f; + size = s; + type = t; + filter = flt; + description = 0; + dbdescription = 0; + def = 0; + } + + ~cDbFieldDef() { free(name); free(dbname); free(description); free(dbdescription); free(def); } + + int getIndex() { return index; } + const char* getName() { return name; } + int hasName(const char* n) { return strcasecmp(n, name) == 0; } + int hasDbName(const char* n) { return strcasecmp(n, dbname) == 0; } + const char* getDescription() { return description; } + const char* getDbDescription() { return dbdescription; } + const char* getDbName() { return dbname; } + int getSize() { return size; } + FieldFormat getFormat() { return format; } + int getType() { return type; } + const char* getDefault() { return def ? def : ""; } + int getFilter() { return filter; } + int filterMatch(int f) { return !f || filter & f; } + int hasType(int types) { return types & type; } + int hasFormat(int f) { return format == f; } + + int isString() { return format == ffAscii || format == ffText || + format == ffMText || format == ffMlob; } + int isInt() { return format == ffInt || format == ffUInt; } + int isBigInt() { return format == ffBigInt || format == ffUBigInt; } + int isFloat() { return format == ffFloat; } + int isDateTime() { return format == ffDateTime; } + + void setDescription(const char* desc) + { + description = strdup(desc); + dbdescription = strdup(strReplace("'", "\\'", description).c_str()); + } + + const char* toColumnFormat(char* buf) // column type to be used for create/alter + { + if (!buf) + return 0; + + sprintf(buf, "%s", toString(format)); + + if (format != ffMlob) + { + if (!size) + { + if (format == ffAscii) + size = 100; + else if (format == ffInt || format == ffUInt || format == ffBigInt || format == ffUBigInt) + size = 11; + else if (format == ffFloat) + size = 10; + } + + if (format == ffFloat) + sprintf(eos(buf), "(%d,%d)", size/10 + size%10, size%10); // 62 -> 8,2 + else if (format == ffInt || format == ffUInt || format == ffBigInt || format == ffUBigInt || format == ffAscii) + sprintf(eos(buf), "(%d)", size); + + if (format == ffUInt || format == ffUBigInt) + sprintf(eos(buf), " unsigned"); + } + + return buf; + } + + int isValid() + { + if (!name) { tell(0, "Missing field name"); return no; } + if (!dbname) { tell(0, "Missing fields database name"); return no; } + if (size == na) { tell(0, "Missing field size"); return no; } + if (type == ftUnknown) { tell(0, "Missing or invalid field type"); return no; } + if (format == ffUnknown) { tell(0, "Missing or invalid field format"); return no; } + + return yes; + } + + void show() + { + char colFmt[100]; + char fType[100]; + char tmp[100]; + + sprintf(fType, "(%s)", toName((FieldType)type, tmp)); + + tell(0, "%-20s %-25s %-17s %-20s (0x%04X) default '%s' '%s'", name, dbname, + toColumnFormat(colFmt), fType, filter, notNull(def, ""), description); + } + + protected: + + char* name; + char* dbname; + char* description; + char* dbdescription; + FieldFormat format; + int size; + int index; + int type; + int filter; // bitmask (defaults to 0xFFFF) + char* def; +}; + +//*************************************************************************** +// cDbIndexDef +//*************************************************************************** + +class cDbIndexDef +{ + public: + + cDbIndexDef() { name = 0; description = 0; } + ~cDbIndexDef() { free(name); free(description); } + + void setName(const char* n) { free(name); name = strdup(n); } + const char* getName() { return name; } + + void setDescription(const char* d) { free(description); description = strdup(d); } + const char* getDescription() { return description; } + + int fieldCount() { return dfields.size(); } + void addField(cDbFieldDef* f) { dfields.push_back(f); } + cDbFieldDef* getField(int i) { return dfields[i]; } + + void show() + { + std::string s = ""; + + for (uint i = 0; i < dfields.size(); i++) + s += dfields[i]->getName() + std::string(" "); + + s.erase(s.find_last_not_of(' ')+1); + + tell(0, "Index %-25s (%s)", getName(), s.c_str()); + } + + protected: + + char* name; + char* description; + std::vector<cDbFieldDef*> dfields; // index fields +}; + +//*************************************************************************** +// cDbTableDef +//*************************************************************************** + +class cDbTableDef : public cDbService +{ + public: + + friend class cDbRow; + friend class cDbTable; + friend class cDbStatement; + + cDbTableDef(const char* n) { name = strdup(n); } + + ~cDbTableDef() + { + for (uint i = 0; i < indices.size(); i++) + delete indices[i]; + + indices.clear(); + + free(name); + clear(); + } + + const char* getName() { return name; } + int fieldCount() { return dfields.size(); } + cDbFieldDef* getField(int id) { return _dfields[id]; } + + cDbFieldDef* getField(const char* fname, int silent = no) + { + std::map<std::string, cDbFieldDef*, _casecmp_>::iterator f; + + if ((f = dfields.find(fname)) != dfields.end()) + return f->second; + + if (!silent) + tell(0, "Fatal: Missing definition of field '%s.%s' in dictionary!", name, fname); + + return 0; + } + + cDbFieldDef* getFieldByDbName(const char* dbname) + { + std::map<std::string, cDbFieldDef*, _casecmp_>::iterator it; + + for (it = dfields.begin(); it != dfields.end(); it++) + { + if (it->second->hasDbName(dbname)) + return it->second; + } + + tell(5, "Fatal: Missing definition of field '%s.%s' in dictionary!", name, dbname); + + return 0; + } + + void addField(cDbFieldDef* f) + { + if (dfields.find(f->getName()) == dfields.end()) + { + dfields[f->getName()] = f; // add to named map + _dfields.push_back(f); // add to indexed list + } + else + tell(0, "Fatal: Field '%s.%s' doubly defined", getName(), f->getName()); + } + + int indexCount() { return indices.size(); } + cDbIndexDef* getIndex(int i) { return indices[i]; } + void addIndex(cDbIndexDef* i) { indices.push_back(i); } + + void clear() + { + std::map<std::string, cDbFieldDef*>::iterator f; + + while ((f = dfields.begin()) != dfields.end()) + { + if (f->second) + delete f->second; + + dfields.erase(f); + } + } + + void show() + { + // show fields + + for (uint i = 0; i < _dfields.size(); i++) + _dfields[i]->show(); + + // show indices + + if (!indices.size()) + { + tell(0, " "); + return; + } + + tell(0, "-----------------------------------------------------"); + tell(0, "Indices from '%s'", getName()); + tell(0, "-----------------------------------------------------"); + + for (uint i = 0; i < indices.size(); i++) + indices[i]->show(); + + tell(0, " "); + } + + private: + + char* name; + std::vector<cDbIndexDef*> indices; + + // FiledDefs stored as list to have access via index + std::vector<cDbFieldDef*> _dfields; + + // same FiledDef references stored as a map to have access via name + std::map<std::string, cDbFieldDef*, _casecmp_> dfields; +}; + +//*************************************************************************** +// cDbDict +//*************************************************************************** + +class cDbDict : public cDbService +{ + public: + + // declarations + + enum TableDictToken + { + dtName, + dtDescription, + dtDbName, + dtFormat, + dtSize, + dtType, + + dtCount + }; + + enum IndexDictToken + { + idtName, + idtDescription, + idtFields + }; + + cDbDict(); + virtual ~cDbDict(); + + int in(const char* file, int filter = 0); + void setFilterFromNameFct(FilterFromName fct) { fltFromNameFct = fct; } + + cDbTableDef* getTable(const char* name); + void show(); + int init(cDbFieldDef*& field, const char* tname, const char* fname); + const char* getPath() { return path ? path : ""; } + void forget(); + + std::map<std::string, cDbTableDef*>::iterator getFirstTableIterator() { return tables.begin(); } + std::map<std::string, cDbTableDef*>::iterator getTableEndIterator() { return tables.end(); } + + protected: + + int atLine(const char* line); + int parseField(const char* line); + int parseIndex(const char* line); + int parseFilter(cDbFieldDef* f, const char* value); + + // data + + int inside; + cDbTableDef* curTable; + std::map<std::string, cDbTableDef*, _casecmp_> tables; + char* path; + int fieldFilter; + FilterFromName fltFromNameFct; +}; + +extern cDbDict dbDict; + +//*************************************************************************** +#endif // __DBDICT_H diff --git a/lib/demo.c b/lib/demo.c new file mode 100644 index 0000000..0c4c234 --- /dev/null +++ b/lib/demo.c @@ -0,0 +1,531 @@ +/* + * demo.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "config.h" +#include "common.h" + +#include "db.h" +#include "epgservice.h" + +cDbConnection* connection = 0; +const char* logPrefix = ""; + +//*************************************************************************** +// Init Connection +//*************************************************************************** + +void initConnection() +{ + cDbConnection::init(); + + cDbConnection::setHost("localhost"); + // cDbConnection::setHost("192.168.200.101"); + cDbConnection::setPort(3306); + cDbConnection::setName("epg2vdr"); + cDbConnection::setUser("epg2vdr"); + cDbConnection::setPass("epg"); + + cDbConnection::setConfPath("/etc/epgd/"); + cDbConnection::setEncoding("utf8"); + + connection = new cDbConnection(); +} + +void exitConnection() +{ + cDbConnection::exit(); + + if (connection) + delete connection; +} + +//*************************************************************************** +// +//*************************************************************************** + +int demoStatement() +{ + int status = success; + + cDbTable* eventsDb = new cDbTable(connection, "events"); + + tell(0, "------------------- attach table ---------------"); + + // open table (attach) + + if (eventsDb->open() != success) + return fail; + + tell(0, "---------------- prepare select statement -------------"); + + // vorbereiten (prepare) eines statement, am besten einmal bei programmstart! + // ---------- + // select eventid, compshorttext, episodepart, episodelang + // from events + // where eventid > ? + + cDbStatement* selectByCompTitle = new cDbStatement(eventsDb); + + status += selectByCompTitle->build("select "); + status += selectByCompTitle->bind("EventId", cDBS::bndOut); + status += selectByCompTitle->bind("ChannelId", cDBS::bndOut, ", "); + status += selectByCompTitle->bind("Title", cDBS::bndOut, ", "); + status += selectByCompTitle->build(" from %s where ", eventsDb->TableName()); + status += selectByCompTitle->bindCmp(0, eventsDb->getField("EventId"), 0, ">"); + + status += selectByCompTitle->prepare(); // prepare statement + + if (status != success) + { + // prepare sollte MySQL fehler ausgegeben haben! + + delete eventsDb; + delete selectByCompTitle; + + return fail; + } + + tell(0, "------------------ prepare done ----------------------"); + + tell(0, "------------------ create some rows ----------------------"); + + eventsDb->clear(); // alle values löschen + + for (int i = 0; i < 10; i++) + { + char* title; + asprintf(&title, "title %d", i); + + eventsDb->setValue(eventsDb->getField("EventId"), 800 + i * 100); + eventsDb->setValue(eventsDb->getField("ChannelId"), "xxx-yyyy-zzz"); + eventsDb->setValue(eventsDb->getField("Title"), title); + + eventsDb->store(); // store -> select mit anschl. update oder insert je nachdem ob dier PKey bereits vorhanden ist + // eventsDb->insert(); // sofern man schon weiß das es ein insert ist + // eventsDb->update(); // sofern man schon weiß das der Datensatz vorhanden ist + + free(title); + } + + tell(0, "------------------ done ----------------------"); + + tell(0, "-------- select all where eventid > 1000 -------------"); + + eventsDb->clear(); // alle values löschen + eventsDb->setValue(eventsDb->getField("EventId"), 1000); + + for (int f = selectByCompTitle->find(); f; f = selectByCompTitle->fetch()) + { + tell(0, "id: %ld", eventsDb->getIntValue(eventsDb->getField("EventId"))); + tell(0, "channel: %s", eventsDb->getStrValue(eventsDb->getField("ChannelId"))); + tell(0, "titel: %s", eventsDb->getStrValue(eventsDb->getField("Title"))); + } + + // freigeben der Ergebnissmenge !! + + selectByCompTitle->freeResult(); + + // folgendes am programmende + + delete eventsDb; // implizietes close (detach) + delete selectByCompTitle; // statement freigeben (auch gegen die DB) + + return success; +} + +//*************************************************************************** +// Join +//*************************************************************************** + +// #define F_INIT(a,b) cDbFieldDef* a##b = 0; dbDict.init(a##b, #a, #b) + +int joinDemo() +{ + int status = success; + + // grundsätzlich genügt hier auch eine Tabelle, für die anderen sind cDbValue Instanzen außreichend + // so ist es etwas einfacher die cDbValues zu initialisieren. + // Ich habe statische "virtual FieldDef* getFieldDef(int f)" Methode in der Tabellenklassen geplant + // um ohne Instanz der cTable ein Feld einfach initialisieren zu können + + cDbTable* eventsDb = new cDbTable(connection, "events"); + cDbTable* imageRefDb = new cDbTable(connection, "imagerefs"); + cDbTable* imageDb = new cDbTable(connection, "images"); + + cDbTable* timerDb = new cDbTable(connection, "timers"); + timerDb->open(yes); + delete timerDb; + + // init dict fields as needed (normaly done once at programm start) + // init and using the pointer improve the speed since the lookup via + // the name is dine only once + + // F_INIT(events, EventId); // ergibt: cDbFieldDef* eventsEventId; dbDict.init(eventsEventId, "events", "EventId"); + + tell(0, "----------------- open table connection ---------------"); + + // open tables (attach) + + if (eventsDb->open() != success) + return fail; + + if (imageDb->open() != success) + return fail; + + if (imageRefDb->open() != success) + return fail; + + tell(0, "---------------- prepare select statement -------------"); + + // all images + + cDbStatement* selectAllImages = new cDbStatement(imageRefDb); + + // prepare fields + + cDbValue imageUpdSp; + cDbValue imageSize; + cDbValue masterId; + + cDbFieldDef imageSizeDef("image", "image", cDBS::ffUInt, 999, cDBS::ftData, 0); // eine Art ein Feld zu erzeugen + imageSize.setField(&imageSizeDef); + imageUpdSp.setField(imageDb->getField("UpdSp")); + masterId.setField(eventsDb->getField("MasterId")); + + // select e.masterid, r.imagename, r.eventid, r.lfn, length(i.image) + // from imagerefs r, images i, events e + // where i.imagename = r.imagename + // and e.eventid = r.eventid + // and (i.updsp > ? or r.updsp > ?) + + selectAllImages->build("select "); + selectAllImages->setBindPrefix("e."); + selectAllImages->bind(&masterId, cDBS::bndOut); + selectAllImages->setBindPrefix("r."); + selectAllImages->bind("ImgName", cDBS::bndOut, ", "); + selectAllImages->bind("EventId", cDBS::bndOut, ", "); + selectAllImages->bind("Lfn", cDBS::bndOut, ", "); + selectAllImages->setBindPrefix("i."); + selectAllImages->build(", length("); + selectAllImages->bind(&imageSize, cDBS::bndOut); + selectAllImages->build(")"); + selectAllImages->clrBindPrefix(); + selectAllImages->build(" from %s r, %s i, %s e where ", + imageRefDb->TableName(), imageDb->TableName(), eventsDb->TableName()); + selectAllImages->build("e.%s = r.%s and i.%s = r.%s and (", + "EventId", // eventsEventId->getDbName(), + imageRefDb->getField("EventId")->getDbName(), + "imagename", // direkt den DB Feldnamen verwenden -> nicht so schön da nicht dynamisch + imageRefDb->getField("ImgName")->getDbName()); // ordentlich via dictionary übersetzt -> schön ;) + selectAllImages->bindCmp("i", &imageUpdSp, ">"); + selectAllImages->build(" or "); + selectAllImages->bindCmp("r", imageRefDb->getField("UpdSp"), 0, ">"); + selectAllImages->build(")"); + + status += selectAllImages->prepare(); + + if (status != success) + { + // prepare sollte MySQL Fehler ausgegeben haben! + + delete eventsDb; + delete imageDb; + delete imageRefDb; + delete selectAllImages; + + return fail; + } + + tell(0, "------------------ prepare done ----------------------"); + + + tell(0, "------------------ select ----------------------"); + + time_t since = 0; //time(0) - 60 * 60; + imageRefDb->clear(); + imageRefDb->setValue(imageRefDb->getField("UpdSp"), since); + imageUpdSp.setValue(since); + + for (int res = selectAllImages->find(); res; res = selectAllImages->fetch()) + { + // so kommst du an die Werte der unterschiedlichen Tabellen + + int eventid = masterId.getIntValue(); + const char* imageName = imageRefDb->getStrValue("ImgName"); + int lfn = imageRefDb->getIntValue(imageRefDb->getField("Lfn")); + int size = imageSize.getIntValue(); + + tell(0, "Found (%d) '%s', %d, %d", eventid, imageName, lfn, size); + } + + tell(0, "------------------ done ----------------------"); + + // freigeben der Ergebnissmenge !! + + selectAllImages->freeResult(); + + + // folgendes am programmende + + delete eventsDb; // implizites close (detach) + delete imageDb; + delete imageRefDb; + delete selectAllImages; // statement freigeben (auch gegen die DB) + + return success; +} + +//*************************************************************************** +// +//*************************************************************************** + +int insertDemo() +{ + cDbTable* timerDb = new cDbTable(connection, "timers"); + if (timerDb->open() != success) return fail; + + timerDb->clear(); + timerDb->setValue("EventId", 4444444); + + if (timerDb->insert() == success) + { + tell(0, "Insert succeeded with ID %d", timerDb->getLastInsertId()); + } + + delete timerDb; + + return done; +} + +//*************************************************************************** +// +//*************************************************************************** + +int findUseEvent() +{ + cDbTable* useeventsDb = new cDbTable(connection, "useevents"); + if (useeventsDb->open() != success) return fail; + + // select event by useid + + tell(0, "========================================"); + + cDbStatement* selectEventById = new cDbStatement(useeventsDb); + + // select * from eventsview + // where useid = ? + // and updflg in (.....) + + selectEventById->build("select "); + selectEventById->bindAllOut(); + selectEventById->build(" from %s where ", useeventsDb->TableName()); + selectEventById->bind("USEID", cDBS::bndIn | cDBS::bndSet); + selectEventById->build(" and %s in (%s)", + useeventsDb->getField("UPDFLG")->getDbName(), + Us::getNeeded()); + + selectEventById->prepare(); + + tell(0, "========================================"); + + useeventsDb->clear(); + useeventsDb->setValue("USEID", 1146680); + + const char* flds[] = + { + "ACTOR", + "AUDIO", + "CATEGORY", + "COUNTRY", + "DIRECTOR", + "FLAGS", + "GENRE", + "INFO", + "MUSIC", + "PRODUCER", + "SCREENPLAY", + "SHORTREVIEW", + "TIPP", + "TOPIC", + "YEAR", + "RATING", + "NUMRATING", + "MODERATOR", + "OTHER", + "GUEST", + "CAMERA", + + 0 + }; + + if (selectEventById->find()) + { + FILE* f; + char* fileName = 0; + + asprintf(&fileName, "%s/info.epg2vdr", "."); + + if (!(f = fopen(fileName, "w"))) + { + selectEventById->freeResult(); + tell(0, "Error opening file '%s' failed, %s", fileName, strerror(errno)); + free(fileName); + + return fail; + } + + tell(0, "Storing event details to '%s'", fileName); + + for (int i = 0; flds[i]; i++) + { + cDbFieldDef* field = useeventsDb->getField(flds[i]); + + if (!field) + { + tell(0, "Warning: Field '%s' not found at table '%s'", flds[i], useeventsDb->TableName()); + continue; + } + + if (field->getFormat() == cDbService::ffAscii || + field->getFormat() == cDbService::ffText || + field->getFormat() == cDbService::ffMText) + { + fprintf(f, "%s: %s\n", flds[i], useeventsDb->getStrValue(flds[i])); + } + else + { + fprintf(f, "%s: %ld\n", flds[i], useeventsDb->getIntValue(flds[i])); + } + } + + selectEventById->freeResult(); + free(fileName); + fclose(f); + } + + tell(0, "========================================"); + + selectEventById->freeResult(); + + return done; +} + +//*************************************************************************** +// +//*************************************************************************** + +int updateRecordingDirectory() +{ + int ins = 0; + + cDbTable* recordingDirDb = new cDbTable(connection, "recordingdirs"); + if (recordingDirDb->open() != success) return fail; + + // char* dir = strdup("more~Marvel's Agents of S.H.I.E.L.D.~xxx.ts"); + char* dir = strdup("aaaaa~bbbbbb~ccccc.ts"); + char* pos = strrchr(dir, '~'); + + if (pos) + { + *pos = 0; + + for (int level = 0; level < 3; level++) + { + recordingDirDb->clear(); + + recordingDirDb->setValue("VDRUUID", "foobar"); + recordingDirDb->setValue("DIRECTORY", dir); + + if (!recordingDirDb->find()) + { + ins++; + recordingDirDb->store(); + } + + recordingDirDb->reset(); + + char* pos = strrchr(dir, '~'); + if (pos) *pos=0; + } + } + + tell(0, "inserted %d directories", ins); + + delete recordingDirDb; + free(dir); + + return ins; +} + +//*************************************************************************** +// Main +//*************************************************************************** + +int main(int argc, char** argv) +{ + cEpgConfig::logstdout = yes; + cEpgConfig::loglevel = 2; + + const char* path = "/etc/epgd/epg.dat"; + + if (argc > 1) + path = argv[1]; + + // read deictionary + + dbDict.setFilterFromNameFct(toFieldFilter); + + if (dbDict.in(path, ffEpgd) != success) + { + tell(0, "Invalid dictionary configuration, aborting!"); + return 1; + } + + cUserTimes userTimes; + + tell(0, "--------------"); + //userTimes.clear(); + tell(0, "--------------"); + + userTimes.add("@Now", "What's on now?") ; + userTimes.add("@Next", "What's on next?" ); + userTimes.add("!20:15", "prime time"); + userTimes.add("22:20", "late night"); + userTimes.add("00:00"); + + tell(0, "--------------"); + + for (int i = 0; i < 6; i++) + { + cUserTimes::UserTime* t = userTimes.next(); + + tell(0, "%d - %s (%s) %s", t->getHHMM(), t->getTitle(), t->getHHMMStr(), t->isHighlighted() ? "highlighted" : ""); + } + + return 0; + + // dbDict.show(); + + initConnection(); + + // demoStatement(); + // joinDemo(); + // insertDemo(); + + tell(0, "uuid: '%s'", getUniqueId()); + + tell(0, "- - - - - - - - - - - - - - - - - "); + + //updateRecordingDirectory(); + findUseEvent(); + + tell(0, "- - - - - - - - - - - - - - - - - "); + + exitConnection(); + + return 0; +} diff --git a/lib/demo.dat b/lib/demo.dat new file mode 100644 index 0000000..5ae9123 --- /dev/null +++ b/lib/demo.dat @@ -0,0 +1,17 @@ + + +Table _test +{ + ID "" id UInt 0 Primary|Autoinc, + VDRUUID "" vdruuid Ascii 40 Primary, + + INSSP "" inssp Int 0 Meta, + UPDSP "" updsp Int 0 Meta, + + EVENTID "" eventid UInt 0 Data, + CHANNELID "" channelid Ascii 50 Data default none, + + SOURCE "" source Ascii 20 Data default WEB filter osd|webif|10, + STATE "'D'eleted, 'R'unning, 'F'inished" state Ascii 1 Data default -, + INFO "error reason if state is failed" info Ascii 255 Data, +} diff --git a/lib/epgservice.c b/lib/epgservice.c new file mode 100644 index 0000000..69e5a8e --- /dev/null +++ b/lib/epgservice.c @@ -0,0 +1,121 @@ +/* + * epgservice.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "epgservice.h" + +//*************************************************************************** +// Timer State / Action +//*************************************************************************** + +const char* toName(TimerState s) +{ + switch (s) + { + case tsPending: return "pending"; + case tsRunning: return "running"; + case tsFinished: return "finished"; + case tsDeleted: return "deleted"; + case tsError: return "failed"; + case tsIgnore: return "ignore"; + case tsUnknown: return "unknown"; + } + + return "unknown"; +} + +const char* toName(TimerAction a, int nice) +{ + switch (a) + { + case taCreate: return "create"; + case taModify: return "modify"; + case taAdjust: return "adjust"; + case taDelete: return "delete"; + case taAssumed: return nice ? "-" : "assumed"; + case taFailed: return "failed"; + case taReject: return "reject"; + } + + return nice ? "-" : "unknown"; +} + +//*************************************************************************** +// cEpgdState +//*************************************************************************** + +const char* cEpgdState::states[] = +{ + "init", + "standby", + "stopped", + + "busy (events)", + "busy (match)", + "busy (scraping)", + "busy (images)", + + 0 +}; + +const char* cEpgdState::toName(cEpgdState::State s) +{ + if (!isValid(s)) + return "unknown"; + + return states[s]; +} + +cEpgdState::State cEpgdState::toState(const char* name) +{ + for (int i = 0; i < esCount; i++) + if (strcmp(states[i], name) == 0) + return (State)i; + + return esUnknown; +} + +//*************************************************************************** +// Field Filter +//*************************************************************************** + +FieldFilterDef fieldFilters[] = +{ + { ffAll, "all" }, + { ffEpgd, "epgd" }, + { ffEpgHttpd, "httpd" }, + { ffEpg2Vdr, "epg2vdr" }, + { ffScraper2Vdr, "scraper" }, + + { 0, 0 } +}; + +const char* toName(FieldFilter f) +{ + for (int i = 0; fieldFilters[i].name; i++) + if (fieldFilters[i].filter == f) + return fieldFilters[i].name; + + return "unknown"; +} + +int toFieldFilter(const char* name) +{ + for (int i = 0; fieldFilters[i].name; i++) + if (strcasecmp(fieldFilters[i].name, name) == 0) + return fieldFilters[i].filter; + + return ffAll; +} + +//*************************************************************************** +// User Rights Check +//*************************************************************************** + +int hasUserMask(unsigned int rights, UserMask mask) +{ + return rights & mask; +} diff --git a/lib/epgservice.h b/lib/epgservice.h new file mode 100644 index 0000000..969d17d --- /dev/null +++ b/lib/epgservice.h @@ -0,0 +1,468 @@ +/* + * epgservice.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __EPGSERVICE_H +#define __EPGSERVICE_H + +#include <list> + +#include "common.h" + +#define EPG_PLUGIN_SEM_KEY 0x3db00001 + +//*************************************************************************** +// Globals +//*************************************************************************** + +typedef unsigned long long tEventId; + +enum EpgServiceMisc +{ + sizeMaxParameterValue = 150 // should match the field size in parameters table +}; + +enum FieldFilter +{ + ffAll = 0xFFFF, + ffEpgd = 1, + ffEpgHttpd = 2, + ffEpg2Vdr = 4, + ffScraper2Vdr = 8, + + ffCount = 5 +}; + +struct FieldFilterDef +{ + int filter; + const char* name; +}; + +const char* toName(FieldFilter f); +int toFieldFilter(const char* name); + +enum SearchFields +{ + sfNone = 0, // off + sfTitle = 1, + sfFolge = 2, + sfDescription = 4 +}; + +enum SearchMode +{ + smExact = 1, + smRegexp, + smLike, + smContained +}; + +enum TimerNamingMode +{ + tnmDefault = 0, // naming would done by VDR + + // naming of following modes handled by recording.py an can 'configured' there + + tnmAuto = 1, // autodetect if 'constabel', 'serie' or 'normal movie' + tnmConstabel = 2, // naming in constabel series style with season, number, .. + tnmSerie = 3, // series style, like Title/Subtitle + tnmCategorized = 4, // sorted in sub folders which are auto-named by category + tnmUser = 5 // user defined mode 'to implement in recording.py' + +}; + +enum RecordingState +{ + rsFinished = 'F', + rsRecording = 'R', + rsDeleted = 'D' +}; + +enum TimerState +{ + tsUnknown = 'U', + tsPending = 'P', + tsRunning = 'R', // timer is recording + tsFinished = 'F', + tsDeleted = 'D', + tsError = 'E', + tsIgnore = '-' // ignore in timer menu -> already tooked 'any-VDR' timer +}; + +const char* toName(TimerState s); + +enum TimerAction +{ + taCreate = 'C', + taModify = 'M', + taAdjust = 'J', + taDelete = 'D', + taAssumed = 'A', + taFailed = 'F', + taReject = 'T' +}; + +enum TimerType +{ + ttRecord = 'R', // Aufnahme-Timer + ttView = 'V', // Umschalt-Timer + ttSearch = 'S' // Such-Timer +}; + +const char* toName(TimerAction a, int nice = no); + +enum TimerDoneState +{ + tdsTimerRequested = 'Q', // timer already requested by epgd/webif + tdsTimerCreated = 'C', // timer created by VDR + tdsTimerCreateFailed = 'f', // create/delete of timer failed by VDR + + tdsRecordingDone = 'R', // Recording finished successfull + tdsRecordingFailed = 'F', // Recording failed + + tdsTimerDeleted = 'D', // timer deleted by user + tdsTimerRejected = 'J' // timer rejected due to user action or timer conflict +}; + +enum UserMask +{ + umNone = 0x0, + + umNologin = 0x1, // the right without a session + + umConfig = 0x2, + umConfigEdit = 0x4, + umConfigUsers = 0x8, + + umFree1 = 0x10, + umFree2 = 0x20, + + umTimer = 0x40, + umTimerEdit = 0x80, + umSearchTimer = 0x100, + umSearchTimerEdit = 0x200, + + umFree3 = 0x400, + umFree4 = 0x800, + + umFsk = 0x1000, + + umFree5 = 0x2000, + umFree6 = 0x4000, + + umRecordings = 0x8000, + umRecordingsEdit = 0x10000, + + umAll = 0xFFFFFFFF & ~umNologin +}; + +int hasUserMask(unsigned int rights, UserMask mask); + +//*************************************************************************** +// cEpgdState +//*************************************************************************** + +class cEpgdState +{ + public: + + enum State + { + esUnknown = na, + + esInit, + esStandby, + esStopped, + + // handler pause on this states! + + esBusy, + esBusyEvents = esBusy, + esBusyMatch, + esBusyScraping, + + // handler don't pause on this states! + + esBusyImages, + + esCount + }; + + static const char* toName(State s); + static State toState(const char* name); + static int isValid(State s) { return s > esUnknown && s < esCount; } + + static const char* states[]; +}; + +typedef cEpgdState Es; + +//*************************************************************************** +// cEventState +//*************************************************************************** + +class cEventState +{ + public: + + enum State + { + // add to VDRs EPG => wird von allen VDR angezeigt + + usActive = 'A', // => Aktiv + usLink = 'L', // => DVB Event welches auf ein externes zeigt + usPassthrough = 'P', // => Durchgeleitetes Event: vdr:000 + + // remove from VDRs EPG => Löschsignal wird an alle vdr gesendet + + usChanged = 'C', // => war mal aktiv wurde jedoch später zum Target,seine eigene ID muss also aus dem epg gelöscht werden. C ist also eine Ausprägung von T + usDelete = 'D', // => Sender oder Providerseitig gelöscht + usRemove = 'R', // => von uns gelöscht weil wir uns für ein anderes Event entschieden haben, zB eins mit Bildern oder Serieninformationen. + + // don't care for VDRs EPG => werden von den VDRs nicht beachtet und nicht mal gesehen. + + usInactive = 'I', // => inaktiv + usTarget = 'T', // => verknüftes Event zum Link, wird unter seiner ID mithilfe des Link im vdr geführt + usMergeSpare = 'S' // => ersatzevent welches für Multimerge ur Verfügung steht + }; + + // get lists for SQL 'in' statements + + static const char* getDeletable() { return "'A','L','P','R','I'"; } // epg plugins + static const char* getNeeded() { return "'A','L','P','C','D','R'"; } // epg2vdr + static const char* getVisible() { return "'A','L','P'"; } // epghttpd + + // checks + + static int isNeeded(char c) { return strchr("ALPCDR", c) != 0; } // epgd2vdr + static int isRemove(char c) { return strchr("CDR", c) != 0; } // epgd2vdr + +}; + +typedef cEventState Us; // remove later, not uses anymore + +//*************************************************************************** +// cUserTimes +//*************************************************************************** + +class cUserTimes +{ + public: + + enum Mode + { + mUnknown = na, + mNow, + mNext, + mTime, + mSearch + }; + + struct UserTime + { + UserTime(const char* strTime, const char* strTitle = 0) + { + highlighted = yes; + mode = mUnknown; + *hhmmStr = 0; + title = 0; + hhmm = 0; + search = 0; + setTime(strTime, strTitle); + } + + UserTime(const UserTime& cp) + { + highlighted = cp.highlighted; + mode = cp.mode; + hhmm = cp.hhmm; + strcpy(hhmmStr, cp.hhmmStr); + title = strdup(cp.title); + search = cp.search ? strdup(cp.search) : 0; + } + + ~UserTime() { free(title); free(search); } + + void setTime(const char* strTime, const char* strTitle) + { + hhmm = 0; + *hhmmStr = 0; + + if (strTime[0] == '!') + { + highlighted = no; + strTime++; + } + + if (strchr(strTime, ':')) + { + hhmm = atoi(strTime) * 100 + atoi(strchr(strTime, ':')+1); + sprintf(hhmmStr, "%02d:%02d", hhmm / 100, hhmm % 100); + mode = mTime; + } + else if (*strTime == '@') + { + highlighted = no; + + if (strcmp(strTime, "@Now") == 0) + mode = mNow; + else if (strcmp(strTime, "@Next") == 0) + mode = mNext; + else + mode = mSearch; + } + + // title + + free(title); + title = 0; + + if (!isEmpty(strTitle)) + title = strdup(strTitle); + else if (!isEmpty(hhmmStr)) + asprintf(&title, "%02d:%02d", hhmm / 100, hhmm % 100); + + // search + + if (*strTime == '@') + { + free(search); + search = strdup(strTime+1); + } + } + + int getHHMM() const { return hhmm; } + const char* getHHMMStr() const { return hhmmStr; } + const char* getTitle() const { return title; } + const char* getSearch() const { return search; } + int getMode() const { return mode; } + const char* getHelpKey() const { return title; } + int isHighlighted() const { return highlighted; } + + time_t getTime() + { + struct tm tmnow; + time_t now = time(0); + + localtime_r(&now, &tmnow); + tmnow.tm_hour = hhmm / 100; + tmnow.tm_min = hhmm % 100; + tmnow.tm_sec = 0; + + time_t ltime = mktime(&tmnow); + + if (ltime < time(0)-tmeSecondsPerHour) + ltime += tmeSecondsPerDay; + + return ltime; + } + + int mode; + int highlighted; + char* title; + char* search; + int hhmm; + char hhmmStr[5+TB]; + }; + + cUserTimes() + { + clear(); + it = times.end(); + } + + void clear() + { + times.clear(); + } + + void add(const char* strTime, const char* strTitle = 0) + { + UserTime ut(strTime, strTitle); + times.push_back(ut); + } + + UserTime* first() + { + it = times.begin(); + + if (it == times.end()) + return 0; + + return &(*it); + } + + UserTime* next() + { + if (it == times.end()) + it = times.begin(); + else + it++; + + if (it == times.end()) + it = times.begin(); + + return &(*it); + } + + UserTime* getFirst() + { + std::list<UserTime>::iterator i; + + i = times.begin(); + + return &(*i); + } + + UserTime* getNext() + { + std::list<UserTime>::iterator i = it; + + if (i == times.end()) + i = times.begin(); + else + i++; + + if (i == times.end()) + i = times.begin(); + + return &(*i); + } + + UserTime* current() { return &(*it); } + + private: + + std::list<UserTime>::iterator it; + std::list<UserTime> times; +}; + +//*************************************************************************** +// EPG Services +//*************************************************************************** + +#define EPG2VDR_UUID_SERVICE "epg2vdr-UuidService-v1.0" + +struct Epg2vdr_UUID_v1_0 +{ + const char* uuid; +}; + +#define MYSQL_INIT_EXIT "Mysql_Init_Exit-v1.0" + +enum MysqlInitExitAction +{ + mieaInit = 0, + mieaExit = 1 +}; + +struct Mysql_Init_Exit_v1_0 +{ + MysqlInitExitAction action; +}; + +#endif // __EPGSERVICE_H diff --git a/lib/imgtools.c b/lib/imgtools.c new file mode 100644 index 0000000..63007e5 --- /dev/null +++ b/lib/imgtools.c @@ -0,0 +1,217 @@ +/* + * imgtools.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "imgtools.h" + +//*************************************************************************** +// Image converting stuff +//*************************************************************************** + +int fromJpeg(Imlib_Image& image, unsigned char* buffer, int size) +{ + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + int w, h; + DATA8 *ptr, *line[16], *data; + DATA32 *ptr2, *dest; + int x, y; + + cinfo.err = jpeg_std_error(&jerr); + + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, buffer, size); + jpeg_read_header(&cinfo, TRUE); + cinfo.do_fancy_upsampling = FALSE; + cinfo.do_block_smoothing = FALSE; + + jpeg_start_decompress(&cinfo); + + w = cinfo.output_width; + h = cinfo.output_height; + + image = imlib_create_image(w, h); + imlib_context_set_image(image); + + dest = ptr2 = imlib_image_get_data(); + data = (DATA8*)malloc(w * 16 * cinfo.output_components); + + for (int i = 0; i < cinfo.rec_outbuf_height; i++) + line[i] = data + (i * w * cinfo.output_components); + + for (int l = 0; l < h; l += cinfo.rec_outbuf_height) + { + jpeg_read_scanlines(&cinfo, line, cinfo.rec_outbuf_height); + int scans = cinfo.rec_outbuf_height; + + if (h - l < scans) + scans = h - l; + + ptr = data; + + for (y = 0; y < scans; y++) + { + for (x = 0; x < w; x++) + { + *ptr2 = (0xff000000) | ((ptr[0]) << 16) | ((ptr[1]) << 8) | (ptr[2]); + ptr += cinfo.output_components; + ptr2++; + } + } + } + + free(data); + + imlib_image_put_back_data(dest); + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + + return success; +} + +long toJpeg(Imlib_Image image, MemoryStruct* data, int quality) +{ + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + DATA32* ptr; + DATA8* buf; + + imlib_context_set_image(image); + + data->clear(); + + cinfo.err = jpeg_std_error(&jerr); + + jpeg_create_compress(&cinfo); + jpeg_mem_dest(&cinfo, (unsigned char**)(&data->memory), &data->size); + + cinfo.image_width = imlib_image_get_width(); + cinfo.image_height = imlib_image_get_height(); + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + jpeg_start_compress(&cinfo, TRUE); + + // get data pointer + + if (!(ptr = imlib_image_get_data_for_reading_only())) + return 0; + + // allocate a small buffer to convert image data */ + + buf = (DATA8*)malloc(imlib_image_get_width() * 3 * sizeof(DATA8)); + + while (cinfo.next_scanline < cinfo.image_height) + { + // convert scanline from ARGB to RGB packed + + for (int j = 0, i = 0; i < imlib_image_get_width(); i++) + { + buf[j++] = ((*ptr) >> 16) & 0xff; + buf[j++] = ((*ptr) >> 8) & 0xff; + buf[j++] = ((*ptr)) & 0xff; + + ptr++; + } + + // write scanline + + jpeg_write_scanlines(&cinfo, (JSAMPROW*)(&buf), 1); + } + + free(buf); + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + + return data->size; +} + +int scaleImageToJpegBuffer(Imlib_Image image, MemoryStruct* data, int width, int height) +{ + if (width && height) + { + Imlib_Image scaledImage; + + imlib_context_set_image(image); + + int imgWidth = imlib_image_get_width(); + int imgHeight = imlib_image_get_height(); + double ratio = (double)imgWidth / (double)imgHeight; + + if ((double)width/(double)imgWidth < (double)height/(double)imgHeight) + height = (int)((double)width / ratio); + else + width = (int)((double)height * ratio); + + scaledImage = imlib_create_image(width, height); + imlib_context_set_image(scaledImage); + + imlib_context_set_color(240, 240, 240, 255); + imlib_image_fill_rectangle(0, 0, width, height); + + imlib_blend_image_onto_image(image, 0, 0, 0, + imgWidth, imgHeight, 0, 0, + width, height); + + toJpeg(scaledImage, data, 70); + + imlib_context_set_image(scaledImage); + imlib_free_image(); + + tell(2, "Scaled image to %d/%d, now %d bytes", width, height, (int)data->size); + } + else + { + toJpeg(image, data, 70); + } + + return success; +} + +int scaleJpegBuffer(MemoryStruct* data, int width, int height) +{ + Imlib_Image image; + + fromJpeg(image, (unsigned char*)data->memory, data->size); + + scaleImageToJpegBuffer(image, data, width, height); + + imlib_context_set_image(image); + imlib_free_image(); + + return success; +} + +//*************************************************************************** +// Str Imlib Error +//*************************************************************************** + +const char* strImlibError(Imlib_Load_Error err) +{ + switch (err) + { + case IMLIB_LOAD_ERROR_NONE: + case IMLIB_LOAD_ERROR_FILE_DOES_NOT_EXIST: return "File does not exist"; + case IMLIB_LOAD_ERROR_FILE_IS_DIRECTORY: return "File is directory"; + case IMLIB_LOAD_ERROR_PERMISSION_DENIED_TO_READ: return "Permission denied to read"; + case IMLIB_LOAD_ERROR_NO_LOADER_FOR_FILE_FORMAT: return "No loader for file format"; + case IMLIB_LOAD_ERROR_PATH_TOO_LONG: return "Path too long"; + case IMLIB_LOAD_ERROR_PATH_COMPONENT_NON_EXISTANT: return "Path component non existant"; + case IMLIB_LOAD_ERROR_PATH_COMPONENT_NOT_DIRECTORY: return "Path component not directory"; + case IMLIB_LOAD_ERROR_PATH_POINTS_OUTSIDE_ADDRESS_SPACE: return "Path points outside address space"; + case IMLIB_LOAD_ERROR_TOO_MANY_SYMBOLIC_LINKS: return "Too many symbolic links"; + case IMLIB_LOAD_ERROR_OUT_OF_MEMORY: return "Out of memory"; + case IMLIB_LOAD_ERROR_OUT_OF_FILE_DESCRIPTORS: return "Out of file descriptors"; + case IMLIB_LOAD_ERROR_PERMISSION_DENIED_TO_WRITE: return "Permission denied to write"; + case IMLIB_LOAD_ERROR_OUT_OF_DISK_SPACE: return "Out of disk space"; + case IMLIB_LOAD_ERROR_UNKNOWN: return "Unknown format"; + } + + return ""; +} diff --git a/lib/imgtools.h b/lib/imgtools.h new file mode 100644 index 0000000..315afaf --- /dev/null +++ b/lib/imgtools.h @@ -0,0 +1,31 @@ +/* + * imgtools.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __IMGTOOLS_H +#define __IMGTOOLS_H + +#include <stdio.h> + +#define X_DISPLAY_MISSING 1 + +#include <jpeglib.h> +#include <Imlib2.h> + +#include "common.h" + +//*************************************************************************** +// Image Manipulating +//*************************************************************************** + +int fromJpeg(Imlib_Image& image, unsigned char* buffer, int size); +long toJpeg(Imlib_Image image, MemoryStruct* data, int quality); +int scaleImageToJpegBuffer(Imlib_Image image, MemoryStruct* data, int width = 0, int height = 0); +int scaleJpegBuffer(MemoryStruct* data, int width = 0, int height = 0); +const char* strImlibError(Imlib_Load_Error err); + +//*************************************************************************** +#endif // __IMGTOOLS_H diff --git a/lib/json.c b/lib/json.c new file mode 100644 index 0000000..31a14e0 --- /dev/null +++ b/lib/json.c @@ -0,0 +1,164 @@ +/* + * json.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "json.h" + +#ifdef USEJSON + +const char* charset = "utf-8"; // #TODO, move to configuration? + +//*************************************************************************** +// Copy JSON Object to Data Buffer +//*************************************************************************** + +int json2Data(json_t* obj, MemoryStruct* data, const char* encoding) +{ + int status = success; + + // will be freed by data's dtor + + data->memory = json_dumps(obj, JSON_PRESERVE_ORDER); + data->size = strlen(data->memory); + sprintf(data->contentType, "application/json; charset=%s", charset); + + // gzip content if supported by browser ... + + if (data->size && encoding && strstr(encoding, "gzip")) + status = data->toGzip(); + + return status; +} + +//*************************************************************************** +// Add field to json object +//*************************************************************************** + +int addFieldToJson(json_t* obj, cDbValue* value, int ignoreEmpty, const char* extName) +{ + char* name; + + if (!value || !value->getField()) + return fail; + + name = strdup(extName ? extName : value->getField()->getName()); + toCase(cLower, name); // use always lower case names in json + + switch (value->getField()->getFormat()) + { + case cDBS::ffAscii: + case cDBS::ffText: + case cDBS::ffMText: + if (!ignoreEmpty || !isEmpty(value->getStrValue())) + json_object_set_new(obj, name, json_string(value->getStrValue())); + break; + + case cDBS::ffInt: + case cDBS::ffUInt: + json_object_set_new(obj, name, json_integer(value->getIntValue())); + break; + + case cDBS::ffFloat: + json_object_set_new(obj, name, json_real(value->getFloatValue())); + break; + + default: + break; + } + + free(name); + + return success; +} + +//*************************************************************************** +// Add field to json object +//*************************************************************************** + +int addFieldToJson(json_t* obj, cDbTable* table, const char* fname, + int ignoreEmpty, const char* extName) +{ + return addFieldToJson(obj, table->getValue(fname), ignoreEmpty, extName); +} + +//*************************************************************************** +// Get Field From Json +// - if a default is required put it into the row +// before calling this function +//*************************************************************************** + +int getFieldFromJson(json_t* obj, cDbRow* row, const char* fname, const char* extName) +{ + cDbValue* value = row->getValue(fname); + char* jname; + + if (!value) + return fail; + + jname = strdup(!isEmpty(extName) ? extName : value->getField()->getName()); + toCase(cLower, jname); // use always lower case names in json + + switch (value->getField()->getFormat()) + { + case cDBS::ffAscii: + case cDBS::ffText: + case cDBS::ffMText: + { + const char* v = getStringFromJson(obj, jname, ""); + if (!isEmpty(v) || !value->isEmpty()) + value->setValue(v); + break; + } + + case cDBS::ffInt: + case cDBS::ffUInt: + { + int v = getIntFromJson(obj, jname, na); + if (v != na || !value->isEmpty()) + value->setValue(v); + break; + } +// #TODO to be implemented +// case cDBS::ffFloat: +// { +// double v = getFloatFromJson(obj, jname, na); +// if (v != na) value->setValue(v); +// break; +// } + + default: + break; + } + + return success; +} + +//*************************************************************************** +// Get Elements +//*************************************************************************** + +const char* getStringFromJson(json_t* obj, const char* name, const char* def) +{ + json_t* o = json_object_get(obj, name); + + if (!o) + return def; + + return json_string_value(o); +} + +int getIntFromJson(json_t* obj, const char* name, int def) +{ + json_t* o = json_object_get(obj, name); + + if (!o) + return def; + + return json_integer_value(o); +} + +//*************************************************************************** +#endif // USEJSON diff --git a/lib/json.h b/lib/json.h new file mode 100644 index 0000000..dd66a93 --- /dev/null +++ b/lib/json.h @@ -0,0 +1,36 @@ +/* + * json.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __JSON_H +#define __JSON_H + +//*************************************************************************** +// Include +//*************************************************************************** + +#ifdef USEJSON + +#include <jansson.h> + +#include "db.h" + +//*************************************************************************** +// JSON Helper Functions +//*************************************************************************** + +int json2Data(json_t* obj, MemoryStruct* data, const char* encoding = 0); + +int addFieldToJson(json_t* obj, cDbTable* table, const char* fname, int ignoreEmpty = yes, const char* extName = 0); +int addFieldToJson(json_t* obj, cDbValue* value, int ignoreEmpty = yes, const char* extName = 0); +int getFieldFromJson(json_t* obj, cDbRow* row, const char* fname, const char* extName = 0); + +const char* getStringFromJson(json_t* obj, const char* name, const char* def = 0); +int getIntFromJson(json_t* obj, const char* name, int def = na); + +#endif // USEJSON + +#endif // __JSON_H diff --git a/lib/python.c b/lib/python.c new file mode 100644 index 0000000..2ec2175 --- /dev/null +++ b/lib/python.c @@ -0,0 +1,356 @@ +/* + * python.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "python.h" + +cDbTable* Python::globalEventsDb = 0; +int Python::globalNamingMode = 0; +const char* Python::globalTmplExpression = ""; + +//*************************************************************************** +// Static Interface Methods (Table events) +//*************************************************************************** + +PyObject* Python::eventTitle(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("TITLE")); +} + +PyObject* Python::eventShortText(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("SHORTTEXT")); +} + +PyObject* Python::eventStartTime(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("i", 0); + + return Py_BuildValue("i", globalEventsDb->getIntValue("STARTTIME")); +} + +PyObject* Python::eventYear(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("YEAR")); +} + +PyObject* Python::eventCategory(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("CATEGORY")); +} + +PyObject* Python::episodeName(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODENAME")); +} + +PyObject* Python::episodeShortName(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODESHORTNAME")); +} + +PyObject* Python::episodePartName(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODEPARTNAME")); +} + +PyObject* Python::extracol1(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODEEXTRACOL1")); +} + +PyObject* Python::extracol2(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODEEXTRACOL2")); +} + +PyObject* Python::extracol3(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("s", ""); + + return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODEEXTRACOL3")); +} + +PyObject* Python::episodeSeason(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("i", na); + + return Py_BuildValue("i", globalEventsDb->getIntValue("EPISODESEASON")); +} + +PyObject* Python::episodePart(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("i", na); + + return Py_BuildValue("i", globalEventsDb->getIntValue("EPISODEPART")); +} + +PyObject* Python::episodeNumber(PyObject* /*self*/, PyObject* /*args*/) +{ + if (!globalEventsDb) + return Py_BuildValue("i", na); + + return Py_BuildValue("i", globalEventsDb->getIntValue("EPISODENUMBER")); +} + +PyObject* Python::namingMode(PyObject* /*self*/, PyObject* /*args*/) +{ + return Py_BuildValue("i", globalNamingMode); +} + +PyObject* Python::tmplExpression(PyObject* /*self*/, PyObject* /*args*/) +{ + return Py_BuildValue("s", globalTmplExpression); +} + +//*************************************************************************** +// The Methods +//*************************************************************************** + +PyMethodDef Python::eventMethods[] = +{ + { "title", Python::eventTitle, METH_VARARGS, "Return the events ..." }, + { "shorttext", Python::eventShortText, METH_VARARGS, "Return the events ..." }, + { "starttime", Python::eventStartTime, METH_VARARGS, "Return the events ..." }, + { "year", Python::eventYear, METH_VARARGS, "Return the events ..." }, + { "category", Python::eventCategory, METH_VARARGS, "Return the events ..." }, + + { "episodname", Python::episodeName, METH_VARARGS, "Return the events ..." }, + { "shortname", Python::episodeShortName, METH_VARARGS, "Return the events ..." }, + { "partname", Python::episodePartName, METH_VARARGS, "Return the events ..." }, + + { "season", Python::episodeSeason, METH_VARARGS, "Return the events ..." }, + { "part", Python::episodePart, METH_VARARGS, "Return the events ..." }, + { "number", Python::episodeNumber, METH_VARARGS, "Return the events ..." }, + + { "extracol1", Python::extracol1, METH_VARARGS, "Return the events ..." }, + { "extracol2", Python::extracol2, METH_VARARGS, "Return the events ..." }, + { "extracol3", Python::extracol3, METH_VARARGS, "Return the events ..." }, + + { "namingmode", Python::namingMode, METH_VARARGS, "Return the ..." }, + { "tmplExpression", Python::tmplExpression, METH_VARARGS, "Return the ..." }, + + { 0, 0, 0, 0 } +}; + +#if PY_MAJOR_VERSION >= 3 + +PyModuleDef Python::moduledef = +{ + PyModuleDef_HEAD_INIT, // m_base + "event", // m_name - name of module + 0, // m_doc - module documentation, may be NULL + -1, // m_size - size of per-interpreter state of the module, or -1 if the module keeps state in global variables. + eventMethods, // m_methods + 0, // m_slots - array of slot definitions for multi-phase initialization, terminated by a {0, NULL} entry. + // when using single-phase initialization, m_slots must be NULL. + 0, // traverseproc m_traverse - traversal function to call during GC traversal of the module object, or NULL if not needed. + 0, // inquiry m_clear - clear function to call during GC clearing of the module object, or NULL if not needed. + 0 // freefunc m_free - function to call during deallocation of the module object or NULL if not needed. +}; + +#endif + +//*************************************************************************** +// Object +//*************************************************************************** + +Python::Python(const char* aFile, const char* aFunction) +{ + pFunc = 0; + pModule = 0; + result = 0; + file = strdup(aFile); + function = strdup(aFunction); +} + +Python::~Python() +{ + exit(); + + free(result); + free(file); + free(function); +} + +//*************************************************************************** +// Init / Exit +//*************************************************************************** + +int Python::init(const char* modulePath) +{ + PyObject* pName; + + tell(0, "Initialize python script '%s/%s.py'", modulePath, file); + + // register event methods + +#if PY_MAJOR_VERSION >= 3 + PyImport_AppendInittab("event", &PyInitEvent); + Py_Initialize(); // initialize the Python interpreter + pName = PyUnicode_FromString(file); +#else + Py_Initialize(); // initialize the Python interpreter + Py_InitModule("event", eventMethods); + pName = PyString_FromString(file); +#endif + + // add search path for Python modules + + if (modulePath) + { + char* p; + asprintf(&p, "sys.path.append(\"%s\")", modulePath); + PyRun_SimpleString("import sys"); + PyRun_SimpleString(p); + free(p); + } + + // import the module + + pModule = PyImport_Import(pName); + Py_DECREF(pName); + + if (!pModule) + { + showError(); + tell(0, "Failed to load '%s.py'", file); + + return fail; + } + + pFunc = PyObject_GetAttrString(pModule, function); + + // pFunc is a new reference + + if (!pFunc || !PyCallable_Check(pFunc)) + { + if (PyErr_Occurred()) + showError(); + + tell(0, "Cannot find function '%s'", function); + + return fail; + } + + return success; +} + +int Python::exit() +{ + if (pFunc) + Py_XDECREF(pFunc); + + if (pModule) + Py_DECREF(pModule); + + Py_Finalize(); + + return success; +} + +//*************************************************************************** +// Execute +//*************************************************************************** + +int Python::execute(cDbTable* eventsDb, int namingmode, const char* tmplExpression) +{ + PyObject* pValue; + + free(result); + result = 0; + + globalEventsDb = eventsDb; + globalNamingMode = namingmode; + globalTmplExpression = tmplExpression; + + pValue = PyObject_CallObject(pFunc, 0); + + if (!pValue) + { + showError(); + tell(0, "Python: Call of function '%s()' failed", function); + + return fail; + } + +#if PY_MAJOR_VERSION >= 3 + // PyObject* strExc = PyObject_Repr(pValue); + // PyObject* pyStr = PyUnicode_AsEncodedString(strExc, "utf-8", "replace"); + PyObject* pyStr = PyUnicode_AsEncodedString(pValue, "utf-8", "replace"); + result = strdup(PyBytes_AsString(pyStr)); + // Py_XDECREF(strExc); + +#else + result = strdup(PyString_AsString(pValue)); +#endif + + tell(3, "Result of call: %s", result); + + Py_DECREF(pValue); + + return success; +} + +//*************************************************************************** +// Error +//*************************************************************************** + +void Python::showError() +{ + const char* error = ""; + PyObject *ptype = 0, *pvalue = 0, *ptraceback = 0; + + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + +#if PY_MAJOR_VERSION >= 3 + PyObject* strExc = PyObject_Repr(pvalue); // Now a unicode object + PyObject* pyStr = PyUnicode_AsEncodedString(strExc, "utf-8", "Error ~"); + error = PyBytes_AsString(pyStr); + + Py_XDECREF(strExc); + Py_XDECREF(pyStr); +#else + error = PyString_AsString(pvalue); +#endif + + tell(0, "Python error was: %s", error); + + Py_DECREF(pvalue); + Py_DECREF(ptype); + Py_XDECREF(ptraceback); +} diff --git a/lib/python.h b/lib/python.h new file mode 100644 index 0000000..eb50964 --- /dev/null +++ b/lib/python.h @@ -0,0 +1,78 @@ +/* + * python.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __PYTHON_H +#define __PYTHON_H + +#include <Python.h> + +#include "db.h" + +//*************************************************************************** +// Class Python +//*************************************************************************** + +class Python +{ + public: + + Python(const char* aFile, const char* aFunction); + ~Python(); + + int init(const char* modulePath = 0); + int exit(); + + int execute(cDbTable* eventsDb, int namingmode, const char* tmplExpression); + + const char* getResult() { return result ? result : ""; } + + protected: + + static PyObject* eventTitle(PyObject* self, PyObject* args); + static PyObject* eventShortText(PyObject* self, PyObject* args); + static PyObject* eventStartTime(PyObject* self, PyObject* args); + static PyObject* eventYear(PyObject* self, PyObject* args); + static PyObject* eventCategory(PyObject* self, PyObject* args); + + static PyObject* episodeName(PyObject* self, PyObject* args); + static PyObject* episodeShortName(PyObject* self, PyObject* args); + static PyObject* episodePartName(PyObject* self, PyObject* args); + static PyObject* extracol1(PyObject* self, PyObject* args); + static PyObject* extracol2(PyObject* self, PyObject* args); + static PyObject* extracol3(PyObject* self, PyObject* args); + static PyObject* episodeSeason(PyObject* self, PyObject* args); + static PyObject* episodePart(PyObject* self, PyObject* args); + static PyObject* episodeNumber(PyObject* self, PyObject* args); + static PyObject* namingMode(PyObject* self, PyObject* args); + static PyObject* tmplExpression(PyObject* self, PyObject* args); + + void showError(); + + // data + + PyObject* pModule; + PyObject* pFunc; + + char* file; + char* function; + char* result; + + // static stuff + + static cDbTable* globalEventsDb; + static int globalNamingMode; + static const char* globalTmplExpression; + static PyMethodDef eventMethods[]; + +#if PY_MAJOR_VERSION >= 3 + static PyObject* PyInitEvent() { return PyModule_Create(&Python::moduledef); } + static PyModuleDef moduledef; +#endif +}; + +//*************************************************************************** +#endif // __PYTHON_H diff --git a/lib/pytst.c b/lib/pytst.c new file mode 100644 index 0000000..9bd8eb6 --- /dev/null +++ b/lib/pytst.c @@ -0,0 +1,122 @@ + +#include "python.h" + + +#include "config.h" +#include "common.h" +#include "db.h" +#include "epgservice.h" + + +cDbTable* eventsDb = 0; +cDbConnection* connection = 0; +const char* logPrefix = ""; + +//*************************************************************************** +// Init / Exit +//*************************************************************************** + +void initConnection() +{ + cDbConnection::init(); + + cDbConnection::setEncoding("utf8"); + cDbConnection::setHost("localhost"); + + cDbConnection::setPort(3306); + cDbConnection::setName("epg2vdr"); + cDbConnection::setUser("epg2vdr"); + cDbConnection::setPass("epg"); + cDbConnection::setConfPath("/etc/epgd/"); + + connection = new cDbConnection(); +} + +void exitConnection() +{ + cDbConnection::exit(); + + if (connection) + delete connection; +} + +int init() +{ + eventsDb = new cDbTable(connection, "useevents"); + if (eventsDb->open() != success) return fail; + + return success; +} + +int exit() +{ + delete eventsDb; + return done; +} + +//*************************************************************************** +// Main +//*************************************************************************** + +int main(int argc, char** argv) +{ + cEpgConfig::logstdout = yes; + cEpgConfig::loglevel = 0; + int namingmode = tnmAuto; + + if (argc < 3) + { + tell(0, "Usage: pytst <channnelid> <eventid> [<namingmode>]"); + return 1; + } + + if (argc >= 4) + namingmode = atoi(argv[3]); + + // at first allpy locale !! + + setlocale(LC_CTYPE, ""); + + // read dictionary + + if (dbDict.in("/etc/epgd/epg.dat") != success) + { + tell(0, "Invalid dictionary configuration, aborting!"); + return 1; + } + + initConnection(); + init(); + + eventsDb->clear(); + eventsDb->setValue("CNTSOURCE", "tvm"); + eventsDb->setValue("CHANNELID", argv[1]); + eventsDb->setBigintValue("CNTEVENTID", atol(argv[2])); + + if (!eventsDb->find()) + { + tell(0, "Event %s/%ld not found", argv[1], atol(argv[2])); + return 1; + } + + tell(2, "Event '%s/%s' found", + eventsDb->getStrValue("TITLE"), eventsDb->getStrValue("SHORTTEXT")); + + // Python stuff .. + + Python py("recording", "name"); + + if (py.init("/etc/epgd") != success) + { + tell(0, "Init of python failed!"); + return 1; + } + + if (py.execute(eventsDb, namingmode) == success) + tell(0, "Info: The recording name calculated by 'recording.py' (with namingmode %d) is '%s'", namingmode, py.getResult()); + + py.exit(); + exitConnection(); + + return 0; +} diff --git a/lib/searchtimer.c b/lib/searchtimer.c new file mode 100644 index 0000000..971230f --- /dev/null +++ b/lib/searchtimer.c @@ -0,0 +1,1373 @@ +/* + * searchtimer.c + * + * See the README file for copyright information + * + */ + +#include "python.h" +#include "searchtimer.h" + +//*************************************************************************** +// +//*************************************************************************** + +int cSearchTimer::searchField[] = +{ + sfTitle, + sfFolge, + sfDescription, + 0 +}; + +const char* cSearchTimer::searchFieldName[] = +{ + "TITLE", + "SHORTTEXT", + "COMPLONGDESCRIPTION", + 0 +}; + +int cSearchTimer::repeadCheckField[] = +{ + sfTitle, + sfFolge, + sfDescription, + + 0 +}; + +const char* cSearchTimer::repeadCheckFieldName[] = +{ + "COMPTITLE", // <- "EPISODECOMPNAME" <- "EPISODECOMPSHORTNAME" + "COMPSHORTTEXT", // <- "EPISODECOMPPARTNAME" + "COMPLONGDESCRIPTION", + + 0 +}; + +//*************************************************************************** +// Class Search Timer +//*************************************************************************** + +cSearchTimer::cSearchTimer() + : startValue("START", cDBS::ffInt, 10), + endValue("END", cDBS::ffInt, 10) +{ + connection = 0; + useeventsDb = 0; + searchtimerDb = 0; + timerDb = 0; + timersDoneDb = 0; + mapDb = 0; + vdrDb = 0; + + selectChannelFromMap = 0; + selectDoneTimer = 0; + selectActiveSearchtimers = 0; + selectSearchtimerMaxModSp = 0; + selectAllTimer = 0; + selectTimerByEvent = 0; + // selectConflictingTimers = 0; + + ptyRecName = 0; + lastSearchTimerUpdate = 0; +} + +cSearchTimer::~cSearchTimer() +{ + delete ptyRecName; +} + +int cSearchTimer::init(const char* confDir) +{ + ptyRecName = new Python("recording", "name"); + + if (ptyRecName->init(confDir) != success) + { + tell(0, "Init of python script recording.py failed, aborting"); + return fail; + } + + return done; +} + +int cSearchTimer::initDb() +{ + int status = success; + + exitDb(); + + connection = new cDbConnection(); + + useeventsDb = new cDbTable(connection, "useevents"); + if ((status = useeventsDb->open() != success)) return status; + + searchtimerDb = new cDbTable(connection, "searchtimers"); + if (searchtimerDb->open() != success) return fail; + + timerDb = new cDbTable(connection, "timers"); + if (timerDb->open() != success) return fail; + + timersDoneDb = new cDbTable(connection, "timersdone"); + if (timersDoneDb->open() != success) return fail; + + mapDb = new cDbTable(connection, "channelmap"); + if ((status = mapDb->open()) != success) return status; + + vdrDb = new cDbTable(connection, "vdrs"); + if ((status = vdrDb->open()) != success) return status; + + // ---------- + // select ... + // from searchtimers + // where state <> 'D' and active > 0 + // and (type = 'R' or type is null) + + selectActiveSearchtimers = new cDbStatement(searchtimerDb); + + selectActiveSearchtimers->build("select "); + selectActiveSearchtimers->bindAllOut(); + selectActiveSearchtimers->bind("UPDSP", cDBS::bndOut, ", "); + selectActiveSearchtimers->bind("INSSP", cDBS::bndOut, ", "); + selectActiveSearchtimers->build(" from %s where %s <> 'D' and %s > 0 and (%s = 'R' or %s is null)", + searchtimerDb->TableName(), + searchtimerDb->getField("STATE")->getDbName(), + searchtimerDb->getField("ACTIVE")->getDbName(), + searchtimerDb->getField("TYPE")->getDbName(), + searchtimerDb->getField("TYPE")->getDbName()); + + status += selectActiveSearchtimers->prepare(); + + // -------------------- + // select max(modsp) from searchtimers + + selectSearchtimerMaxModSp = new cDbStatement(searchtimerDb); + + selectSearchtimerMaxModSp->build("select "); + selectSearchtimerMaxModSp->bind("MODSP", cDBS::bndOut, "max("); + selectSearchtimerMaxModSp->build(") from %s", searchtimerDb->TableName()); + + status += selectSearchtimerMaxModSp->prepare(); + + // ---------- + // select id + // from timersdone + // ... + + selectDoneTimer = new cDbStatement(timersDoneDb); + + // selectDoneTimer - will build and prepared later at runtime ... + + // ---------- + // select channelname + // from channelmap + + selectChannelFromMap = new cDbStatement(mapDb); + + selectChannelFromMap->build("select "); + selectChannelFromMap->bind("CHANNELNAME", cDBS::bndOut); + selectChannelFromMap->build(" from %s where ", mapDb->TableName()); + selectChannelFromMap->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet); + + status += selectChannelFromMap->prepare(); + + // select *, + // from timers + // where state in ('P','R') + + selectAllTimer = new cDbStatement(timerDb); + + selectAllTimer->build("select "); + selectAllTimer->setBindPrefix("t."); + selectAllTimer->bindAllOut(); + selectAllTimer->setBindPrefix("v."); + selectAllTimer->bind(vdrDb, "NAME", cDBS::bndOut, ", "); + selectAllTimer->bind(vdrDb, "TUNERCOUNT", cDBS::bndOut, ", "); + selectAllTimer->build(" from %s t, %s v ", timerDb->TableName(), vdrDb->TableName()); + selectAllTimer->build(" where t.%s in ('P','R')", timerDb->getField("STATE")->getDbName()); + selectAllTimer->build(" and t.VDRUUID = v.UUID"); + selectAllTimer->build(" order by t.%s, t.%s", + timerDb->getField("DAY")->getDbName(), + timerDb->getField("STARTTIME")->getDbName()); + + status += selectAllTimer->prepare(); + + // select * + // from timers + // where (state in ('P','R') or state is null) + // and eventid = ? + + selectTimerByEvent = new cDbStatement(timerDb); + + selectTimerByEvent->build("select "); + selectTimerByEvent->bindAllOut(); + selectTimerByEvent->build(" from %s where ", timerDb->TableName()); + selectTimerByEvent->build("(%s in ('P','R') or %s is null)", + timerDb->getField("STATE")->getDbName(), + timerDb->getField("STATE")->getDbName()); + selectTimerByEvent->bind("EVENTID", cDBS::bndIn | cDBS::bndSet, " and "); + status += selectTimerByEvent->prepare(); + + // select * from timers + // where state in ('P','R') + // and active = 1 + // and day + starttime div 100 * 60 * 60 + starttime % 100 * 60 >= ? + // and day + starttime div 100 * 60 * 60 + starttime % 100 * 60 <= ? + // and vdruuid = ? + // group by SUBSTRING_INDEX(channelid, '-', 3); + + // selectConflictingTimers = new cDbStatement(timerDb); + + // selectConflictingTimers->build("select "); + // selectConflictingTimers->bindAllOut(); + // selectConflictingTimers->build(" from %s where %s in ('P','R')", + // timerDb->TableName(), timerDb->getField("STATE")->getDbName()); + // selectConflictingTimers->build(" and active = 1"); + // selectConflictingTimers->bindText("day + starttime div 100 * 60 * 60 + starttime % 100 * 60", &startValue, ">=", " and "); + // selectConflictingTimers->bindText("day + starttime div 100 * 60 * 60 + starttime % 100 * 60", &endValue, "<=", " and "); + // selectConflictingTimers->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and "); + // selectConflictingTimers->build(" group by SUBSTRING_INDEX(channelid, '-', 3)"); + + // status += selectConflictingTimers->prepare(); + + // ---------- + + if (status != success) + { + tell(0, "Error: At least %d statements not prepared successfully", status*-1); + return status; + } + + return success; +} + +int cSearchTimer::exitDb() +{ + if (connection) + { + delete selectActiveSearchtimers; selectActiveSearchtimers = 0; + delete selectSearchtimerMaxModSp; selectSearchtimerMaxModSp = 0; + delete selectDoneTimer; selectDoneTimer = 0; + delete selectChannelFromMap; selectChannelFromMap = 0; + delete selectAllTimer; selectAllTimer = 0; + // delete selectConflictingTimers; selectConflictingTimers = 0; + delete selectTimerByEvent; selectTimerByEvent = 0; + + delete mapDb; mapDb = 0; + delete useeventsDb; useeventsDb = 0; + delete searchtimerDb; searchtimerDb = 0; + delete timerDb; timerDb = 0; + delete timersDoneDb; timersDoneDb = 0; + delete vdrDb; vdrDb = 0; + + delete connection; connection = 0; + } + + return success; +} + +//*************************************************************************** +// Any Search Timer Modified +//*************************************************************************** + +int cSearchTimer::modified() +{ + int modsp = 0; + + if (selectSearchtimerMaxModSp->find()) + modsp = searchtimerDb->getIntValue("MODSP"); + + selectSearchtimerMaxModSp->freeResult(); + + return modsp && modsp > lastSearchTimerUpdate; +} + +//*************************************************************************** +// Prepare Search Statement +//*************************************************************************** + +cDbStatement* cSearchTimer::prepareSearchStatement(cDbRow* searchTimer, cDbTable* db) +{ + cDbStatement* select = new cDbStatement(db); + + const char* searchOp = "="; + const char* expression = searchTimer->getStrValue("EXPRESSION"); + const char* expression1 = searchTimer->getStrValue("EXPRESSION1"); + const char* episodename = searchTimer->getStrValue("EPISODENAME"); + const char* season = searchTimer->getStrValue("SEASON"); + const char* seasonpart = searchTimer->getStrValue("SEASONPART"); + const char* category = searchTimer->getStrValue("CATEGORY"); + const char* genre = searchTimer->getStrValue("GENRE"); + const char* tipp = searchTimer->getStrValue("TIPP"); + const char* year = searchTimer->getStrValue("YEAR"); + const char* chformat = searchTimer->getStrValue("CHFORMAT"); + + int noepgmatch = searchTimer->getIntValue("NOEPGMATCH"); + int searchmode = searchTimer->getIntValue("SEARCHMODE"); + int searchfields = searchTimer->getIntValue("SEARCHFIELDS"); + int searchfields1 = searchTimer->getIntValue("SEARCHFIELDS1"); + int casesensitiv = searchTimer->getIntValue("CASESENSITIV"); + int weekdays = searchTimer->getValue("WEEKDAYS")->isNull() ? (int)na : searchTimer->getIntValue("WEEKDAYS"); + + switch (searchmode) + { + case smExact: searchOp = casesensitiv ? "= BINARY" : "="; break; + case smRegexp: searchOp = casesensitiv ? "regexp BINARY" : "regexp"; break; + case smLike: searchOp = casesensitiv ? "like BINARY" : "like"; break; + case smContained: searchOp = casesensitiv ? "like BINARY" : "like"; break; + } + + select->build("select "); + select->bindAllOut(0, cDBS::ftData | cDBS::ftPrimary, cDBS::ftMeta); + select->setBindPrefix("c."); + select->bind(mapDb, "FORMAT", cDBS::bndOut, ", "); + select->clrBindPrefix(); + select->build(" from eventsviewplain e, (select distinct channelid,channelname,format,ord,visible from %s) c where ", + mapDb->TableName()); + select->build("e.%s = c.%s", + db->getField("CHANNELID")->getDbName(), + mapDb->getField("CHANNELID")->getDbName()); + select->build(" and e.updflg in (%s)", cEventState::getVisible()); + select->build(" and e.cnt_starttime >= unix_timestamp()-120"); // not more than 2 minutes running + + // search fields 1 + + if (!isEmpty(expression) && strcmp(expression, "%") != 0 && strcmp(expression, "%%") != 0 && searchfields) + { + select->build(" and ("); + + for (int i = 0, n = 0; searchField[i]; i++) + { + if (!db->getField(searchFieldName[i])) + tell(0, "Fatal: Search field '%s' not known!", searchFieldName[i]); + + else if (searchfields & searchField[i]) + { + select->build("%s(%s %s '%s%s%s')", n++ ? " or " : "", + db->getField(searchFieldName[i])->getDbName(), + searchOp, + searchmode == smContained ? "%" : "", + connection->escapeSqlString(expression).c_str(), + searchmode == smContained ? "%" : ""); + } + } + + select->build(")"); + } + + // search fields 2 + + if (!isEmpty(expression1) && strcmp(expression1, "%") != 0 && strcmp(expression1, "%%") != 0 && searchfields1) + { + select->build(" and ("); + + for (int i = 0, n = 0; searchField[i]; i++) + { + if (!db->getField(searchFieldName[i])) + tell(0, "Fatal: Search field '%s' not known!", searchFieldName[i]); + + else if (searchfields1 & searchField[i]) + { + select->build("%s(%s %s '%s%s%s')", n++ ? " or " : "", + db->getField(searchFieldName[i])->getDbName(), + searchOp, + searchmode == smContained ? "%" : "", + connection->escapeSqlString(expression1).c_str(), + searchmode == smContained ? "%" : ""); + } + } + + select->build(")"); + } + + // Channel Format (CHFORMAT) + + if (!isEmpty(chformat)) + { + std::string format; + + format = "'" + strReplace(",", "','", chformat) + "'"; + + select->build(" and ("); + + select->build(" c.%s in (%s) ", + mapDb->getField("FORMAT")->getDbName(), + format.c_str()); + + select->build(")"); + } + + // Kategorie 'Spielfilm','Serie' (CATEGORY) + + if (!isEmpty(category)) + { + select->build(" and ("); + + if (noepgmatch) + select->build("%s is null or ", db->getField("CATEGORY")->getDbName()); + + select->build(" %s in (%s) ", + db->getField("CATEGORY")->getDbName(), + category); + + select->build(")"); + } + + // Genre 'Krimi','Action' (GENRE) + + if (!isEmpty(genre)) + { + select->build(" and ("); + + if (noepgmatch) + select->build("%s is null or ", db->getField("GENRE")->getDbName()); + + select->build(" %s in (%s) ", + db->getField("GENRE")->getDbName(), + genre); + + select->build(")"); + } + + // Tipp (TIPP) + + if (!isEmpty(tipp)) + { + select->build(" and ("); + + if (noepgmatch) + select->build("%s is null or ", db->getField("TIPP")->getDbName()); + + select->build(" %s in (%s) ", + db->getField("TIPP")->getDbName(), + tipp); + + select->build(")"); + } + + // Serien Titel (EPISODENAME) + + if (!isEmpty(episodename)) + { + select->build(" and ("); + + select->build(" %s = '%s' or (%s is null and %s = '%s')", + db->getField("EPISODENAME")->getDbName(), + connection->escapeSqlString(episodename).c_str(), + db->getField("EPISODENAME")->getDbName(), + db->getField("TITLE")->getDbName(), + connection->escapeSqlString(episodename).c_str()); + + select->build(")"); + } + + // Staffel like 3-5 (SEASON) + + if (!isEmpty(season)) + { + select->build(" and ("); + + if (noepgmatch) + select->build("%s is null or ", db->getField("EPISODESEASON")->getDbName()); + + select->build(" %s between %d and %d ", + db->getField("EPISODESEASON")->getDbName(), + rangeFrom(season), rangeTo(season)); + + select->build(")"); + } + + // Staffelfolge (SEASONPART) + + if (!isEmpty(seasonpart)) + { + select->build(" and ("); + + if (noepgmatch) + select->build("%s is null or ", db->getField("EPISODEPART")->getDbName()); + + select->build(" %s between %d and %d ", + db->getField("EPISODEPART")->getDbName(), + rangeFrom(seasonpart), rangeTo(seasonpart)); + + select->build(")"); + } + + // Jahr (YEAR) + + if (!isEmpty(year)) + { + select->build(" and ("); + + if (noepgmatch) + select->build("%s is null or ", db->getField("YEAR")->getDbName()); + + select->build(" %s between %d and %d ", + db->getField("YEAR")->getDbName(), + rangeFrom(year), rangeTo(year)); + + select->build(")"); + } + + if (weekdays > 0) + { + select->build(" and "); + select->build("(%d & (1 << weekday(from_unixtime(%s)))) <> 0", + weekdays, db->getField("STARTTIME")->getDbName()); + } + + select->build(" order by e.cnt_starttime, c.ord"); + + if (select->prepare() != success) + { + delete select; + select = 0; + tell(0, "AUTOTIMER: Prepare of statement for searchtimer failed, skipping"); + return 0; + } + + const char* p = strstr(select->asText(), " from "); + + tell(1, "AUTOTIMER: Search statement [%s;]", p ? p : select->asText()); + + return select; +} + +//*************************************************************************** +// Match Criterias +//*************************************************************************** + +int cSearchTimer::matchCriterias(cDbRow* searchTimer, cDbRow* event) +{ + const char* channelids = searchTimer->getStrValue("CHANNELIDS"); + int chexclude = searchTimer->getIntValue("CHEXCLUDE"); + int rangeStart = searchTimer->getValue("STARTTIME")->isNull() ? (int)na : searchTimer->getIntValue("STARTTIME"); + int rangeEnd = searchTimer->getValue("ENDTIME")->isNull() ? (int)na : searchTimer->getIntValue("ENDTIME"); + int nextDays = searchTimer->getValue("NEXTDAYS")->isNull() ? (int)na : searchTimer->getIntValue("NEXTDAYS"); + + const char* channelid = event->getStrValue("CHANNELID"); + time_t starttime = event->getIntValue("STARTTIME"); + int hhmm = l2hhmm(starttime); + + // check if channel known to VDRs + + mapDb->clear(); + mapDb->setValue("CHANNELID", channelid); + + if (!selectChannelFromMap->find() || mapDb->getIntValue("UNKNOWNATVDR") > 0) + { + mapDb->reset(); + tell(2, "AUTOTIMER: Skipping hit, channelid '%s' is unknown at least on one VDR!", + event->getStrValue("CHANNELID")); + return no; + } + + mapDb->reset(); + + // check channel matches + + if (!isEmpty(channelids)) + { + if (!chexclude && !strstr(channelids, channelid)) + { + tell(2, "AUTOTIMER: Skipping hit due to channelid - '%s' not in '%s'", + event->getStrValue("CHANNELID"), channelids); + return no; + } + else if (chexclude && strstr(channelids, channelid)) + { + tell(2, "AUTOTIMER: Skipping hit due to channelid - '%s' it in '%s'", + event->getStrValue("CHANNELID"), channelids); + return no; + } + } + + // check start + + if (rangeStart > 0 && hhmm < rangeStart) + { + tell(2, "AUTOTIMER: Skipping due to range (start before range)"); + return no; + } + + if (rangeEnd > 0 && hhmm > rangeEnd) + { + tell(2, "AUTOTIMER: Skipping due to range (start behind range)"); + return no; + } + + if (nextDays > 0 && event->getIntValue("STARTTIME") > time(0) + tmeSecondsPerDay * nextDays) + { + tell(2, "AUTOTIMER: Skipping due to nextdays (start behind range)"); + return no; + } + + // check range ... is start event in nextdays ... + + return yes; +} + +//*************************************************************************** +// Get Search Matches +//*************************************************************************** + +int cSearchTimer::getSearchMatches(cDbRow* searchTimer, json_t* obj) +{ + cDbStatement* select = 0; + long searchtimerid = searchTimer->getIntValue("ID"); + + searchtimerDb->clear(); + + if (searchtimerid > 0) + { + searchtimerDb->setValue("ID", searchtimerid); + + if (!searchtimerDb->find()) + { + tell(0, "Warning: Searchtimer %ld not found!", searchtimerid); + return fail; + } + + searchTimer = searchtimerDb->getRow(); + } + + if (!(select = prepareSearchStatement(searchTimer, useeventsDb))) + return fail; + + json_t* oEvents = json_array(); + + useeventsDb->clear(); + + for (int res = select->find(); res; res = select->fetch()) + { + cDbStatement* selectDones = 0; + + useeventsDb->find(); // get all fields .. + + if (!matchCriterias(searchTimer, useeventsDb->getRow())) + continue; + + // add to json object + + json_t* oData = json_object(); + + addFieldToJson(oData, useeventsDb, "USEID", no, "id"); + addFieldToJson(oData, useeventsDb, "CHANNELID"); + addFieldToJson(oData, useeventsDb, "STARTTIME"); + addFieldToJson(oData, useeventsDb, "DURATION"); + addFieldToJson(oData, useeventsDb, "CATEGORY"); + addFieldToJson(oData, useeventsDb, "GENRE"); + addFieldToJson(oData, useeventsDb, "TITLE"); + addFieldToJson(oData, useeventsDb, "SHORTTEXT"); + addFieldToJson(oData, useeventsDb, "SHORTDESCRIPTION"); + addFieldToJson(oData, useeventsDb, "TIPP"); + + if (prepareDoneSelect(useeventsDb->getRow(), searchtimerDb->getIntValue("REPEATFIELDS"), selectDones) == success && selectDones) + { + json_t* oDones = json_array(); + int cnt = 0; + + for (int f = selectDones->find(); f; f = selectDones->fetch()) + { + json_t* o = json_object(); + + long id = timersDoneDb->getIntValue("ID"); + const char* state = timersDoneDb->getStrValue("STATE"); + + // add to json object + + json_object_set_new(o, "id", json_integer(id)); + json_object_set_new(o, "state", json_string(state)); + json_array_append_new(oDones, o); + cnt++; + } + + selectDones->freeResult(); + + if (cnt) + json_object_set_new(oData, "dones", oDones); + } + + json_array_append_new(oEvents, oData); + } + + select->freeResult(); + delete select; + + searchtimerDb->reset(); + + json_object_set_new(obj, "events", oEvents); + + return success; +} + +//*************************************************************************** +// Get Done For Event by Searchtimer +//*************************************************************************** + +int cSearchTimer::getDoneFor(cDbRow* searchTimer, cDbRow* useevent, json_t* obj) +{ + cDbStatement* select = 0; + + searchtimerDb->clear(); + searchtimerDb->setValue("ID", searchTimer->getIntValue("ID")); + + if (!searchtimerDb->find()) + { + tell(0, "Warning: Searchtimer %ld not found", searchTimer->getIntValue("ID")); + return fail; + } + + useeventsDb->clear(); + useeventsDb->setBigintValue("CNTEVENTID", useevent->getBigintValue("CNTEVENTID")); + useeventsDb->setValue("CHANNELID", useevent->getStrValue("CHANNELID")); + useeventsDb->setValue("CNTSOURCE", useevent->getStrValue("CNTSOURCE")); + + if (!useeventsDb->find()) + { + tell(0, "Warning: Event '%s/%" PRId64 "/%s' not found", + useevent->getStrValue("CHANNELID"), + useevent->getBigintValue("CNTEVENTID"), + useevent->getStrValue("CNTSOURCE")); + + return fail; + } + + if (prepareDoneSelect(useeventsDb->getRow(), searchtimerDb->getIntValue("REPEATFIELDS"), select) != success || !select) + return success; + + json_t* oDones = json_array(); + int cnt = 0; + + for (int f = select->find(); f; f = select->fetch()) + { + json_t* oData = json_object(); + + long id = timersDoneDb->getIntValue("ID"); + const char* state = timersDoneDb->getStrValue("STATE"); + + // add to json object + + json_object_set_new(oData, "id", json_integer(id)); + json_object_set_new(oData, "state", json_string(state)); + json_array_append_new(oDones, oData); + cnt++; + } + + select->freeResult(); + useeventsDb->reset(); + searchtimerDb->reset(); + + if (cnt) + json_object_set_new(obj, "dones", oDones); + + return success; +} + +//*************************************************************************** +// Update Search Timers +//*************************************************************************** + +int cSearchTimer::updateSearchTimers(int force, const char* reason) +{ + uint64_t start = cMyTimeMs::Now(); + long total = 0; + + tell(0, "AUTOTIMER: Updating searchtimers due to '%s' %s", reason, force ? "(force)" : ""); + + searchtimerDb->clear(); + + for (int res = selectActiveSearchtimers->find(); res; res = selectActiveSearchtimers->fetch()) + { + long hits = 0; + cDbStatement* select = 0; + + // searchtimer updated after last run or force? + + if (!force && searchtimerDb->getIntValue("MODSP") <= searchtimerDb->getIntValue("LASTRUN")) + continue; + + select = prepareSearchStatement(searchtimerDb->getRow(), useeventsDb); + + if (!select) + { + lastSearchTimerUpdate = time(0); // protect for infinite call on error + return 0; + } + + useeventsDb->clear(); + + for (int res = select->find(); res; res = select->fetch()) + { + cDbStatement* select = 0; + time_t starttime = useeventsDb->getIntValue("STARTTIME"); + int weekday = weekdayOf(starttime); + + tell(3, "AUTOTIMER: Found event (%s) '%s' / '%s' (%ld/%s) at day %d", + l2pTime(starttime).c_str(), + useeventsDb->getStrValue("TITLE"), + useeventsDb->getStrValue("SHORTTEXT"), + useeventsDb->getIntValue("USEID"), + useeventsDb->getStrValue("CHANNELID"), + weekday); + + useeventsDb->find(); // get all fields .. + + // match + + if (!matchCriterias(searchtimerDb->getRow(), useeventsDb->getRow())) + { + useeventsDb->reset(); + continue; + } + + // check if event already recorded or schedule for recording + + tell(2, "Check if '%s/%s' already recorded by fields (%ld)", + useeventsDb->getStrValue("TITLE"), + useeventsDb->getStrValue("SHORTTEXT"), + searchtimerDb->getIntValue("REPEATFIELDS")); + + int isDone = no; + + if (prepareDoneSelect(useeventsDb->getRow(), searchtimerDb->getIntValue("REPEATFIELDS"), select) == success && select) + { + isDone = select->find() ? yes : no; + select->freeResult(); + } + + if (isDone) + continue; + + timerDb->clear(); + timerDb->setValue("EVENTID", useeventsDb->getIntValue("USEID")); + + if (selectTimerByEvent->find()) + { + tell(2, "Timer for event (%ld) '%s/%s' already scheduled, skipping", + useeventsDb->getIntValue("USEID"), + useeventsDb->getStrValue("TITLE"), + useeventsDb->getStrValue("SHORTTEXT")); + } + else + { +// #TODO time_t lEndTime = useeventsDb->getIntValue("DURATION"); +// #TODO int count = getUsedTransponderAt(useeventsDb->getIntValue("STARTTIME"), lEndTime); +// #TODO if (count <= availableTransponders) + { + if (createTimer(searchtimerDb->getIntValue("ID")) == success) + hits++; + } +// else +// tell(1, "AUTOTIMER: Skipping due to"); + } + + selectTimerByEvent->freeResult(); + useeventsDb->reset(); + } + + total += hits; + + select->freeResult(); + delete select; + + searchtimerDb->find(); + searchtimerDb->setValue("LASTRUN", time(0)); + + if (hits) + searchtimerDb->setValue("HITS", searchtimerDb->getIntValue("HITS") + hits); + + searchtimerDb->update(); + } + + selectActiveSearchtimers->freeResult(); + lastSearchTimerUpdate = time(0); + + tell(0, "AUTOTIMER: Update done after %s, created %ld timers", + ms2Dur(cMyTimeMs::Now()-start).c_str(), total); + + return total; +} + +//*************************************************************************** +// Prepare Done Select +//*************************************************************************** + +int cSearchTimer::prepareDoneSelect(cDbRow* useeventsRow, int repeatfields, cDbStatement*& select) +{ + std::string chkFields = ""; + select = 0; + + if (repeatfields <= 0) + return success; + + // prepare statement ... + + selectDoneTimer->clear(); + selectDoneTimer->build("select "); + selectDoneTimer->bind("ID", cDBS::bndOut); + selectDoneTimer->bind("STATE", cDBS::bndOut, ", "); + selectDoneTimer->build(" from %s where ", timersDoneDb->TableName()); + + // retry only 'F'ailed and re'J'ected timers, don't retry 'D'eleted timers sice they are deleted by user + + selectDoneTimer->build(" %s not in ('F','J')", // mysql ignoring case by default! + timersDoneDb->getField("STATE")->getDbName()); + + for (int i = 0; repeadCheckField[i]; i++) + { + const char* fieldName = repeadCheckFieldName[i]; + + if (!timersDoneDb->getField(fieldName)) + tell(0, "AUTOTIMER: Search (for 'done' check) field '%s' not known!", + fieldName); + + else if (repeatfields & repeadCheckField[i]) + { + // specical handling for episode, use EPISODECOMPSHORTNAME instead (if not null)! + + if (repeadCheckField[i] == sfTitle) + { + if (!useeventsRow->getValue("EPISODECOMPSHORTNAME")->isNull()) + fieldName = "EPISODECOMPSHORTNAME"; + if (!useeventsRow->getValue("EPISODECOMPPARTNAME")->isNull()) + fieldName = "EPISODECOMPPARTNAME"; + } + + if (repeadCheckField[i] == sfFolge) + { + if (!useeventsRow->getValue("EPISODECOMPSHORTNAME")->isNull()) + fieldName = "EPISODECOMPSHORTNAME"; + } + + selectDoneTimer->bind(timersDoneDb->getField(fieldName), + cDBS::bndIn | cDBS::bndSet, " and "); + + chkFields += " " + std::string(fieldName); + } + } + + if (selectDoneTimer->prepare() != success) + { + tell(0, "AUTOTIMER: Prepare of statement for 'done' check failed, skipping"); + return fail; + } + + timersDoneDb->clear(); + timersDoneDb->setValue("COMPTITLE", useeventsRow->getStrValue("COMPTITLE")); + + if (!useeventsRow->getValue("COMPSHORTTEXT")->isEmpty()) + timersDoneDb->setValue("COMPSHORTTEXT", useeventsRow->getStrValue("COMPSHORTTEXT")); + + if (!useeventsRow->getValue("EPISODECOMPNAME")->isEmpty()) + timersDoneDb->setValue("EPISODECOMPNAME", useeventsRow->getStrValue("EPISODECOMPNAME")); + if (!useeventsRow->getValue("EPISODECOMPSHORTNAME")->isEmpty()) + timersDoneDb->setValue("EPISODECOMPSHORTNAME", useeventsRow->getStrValue("EPISODECOMPSHORTNAME")); + + if (!useeventsRow->getValue("EPISODECOMPPARTNAME")->isEmpty()) + timersDoneDb->setValue("EPISODECOMPPARTNAME", useeventsRow->getStrValue("EPISODECOMPPARTNAME")); + + if (!useeventsRow->getValue("COMPLONGDESCRIPTION")->isEmpty()) + timersDoneDb->setValue("COMPLONGDESCRIPTION", useeventsRow->getStrValue("COMPLONGDESCRIPTION")); + + select = selectDoneTimer; + + return success; +} + +//*************************************************************************** +// Create Timer +// +// - searchtimerDb and useeventsDb has to be positioned and loaded +//*************************************************************************** + +int cSearchTimer::createTimer(int id) +{ + int status; + int timerid; + + const char* channelname = ""; + int namingmode = searchtimerDb->getIntValue("NAMINGMODE"); + const char* tmplExpression = timerDb->getStrValue("TEMPLATE"); + + // ------------------------------------------ + // lookup channel name to store in timersdone + // (just as a additional 'debug' info) + + mapDb->clear(); + mapDb->setValue("CHANNELID", useeventsDb->getStrValue("CHANNELID")); + + if (selectChannelFromMap->find()) + channelname = mapDb->getStrValue("CHANNELNAME"); + + if (isEmpty(channelname)) + channelname = useeventsDb->getStrValue("CHANNELID"); + + selectChannelFromMap->freeResult(); + + // ------------------------------------------ + // Create timer in timerdistribution ... + + cDbRow timerRow("timers"); + + timerRow.clear(); + + timerRow.setValue("ID", na); // 'na' is the signal for modifyCreateTimer to create new timer + timerRow.setValue("ACTIVE", yes); + timerRow.setValue("TYPE", searchtimerDb->getStrValue("TYPE")); + timerRow.setValue("SOURCE", "epgd"); + timerRow.setValue("EVENTID", useeventsDb->getIntValue("USEID")); + timerRow.setValue("CHANNELID", useeventsDb->getStrValue("CHANNELID")); + timerRow.setValue("NAMINGMODE", namingmode); + timerRow.setValue("TEMPLATE", tmplExpression); + timerRow.setValue("CHILDLOCK", searchtimerDb->getIntValue("CHILDLOCK")); + timerRow.setValue("VDRUUID", searchtimerDb->getStrValue("VDRUUID")); + timerRow.setValue("VPS", searchtimerDb->getIntValue("VPS")); + timerRow.setValue("PRIORITY", searchtimerDb->getIntValue("PRIORITY")); + timerRow.setValue("LIFETIME", searchtimerDb->getIntValue("LIFETIME")); + timerRow.setValue("DIRECTORY", searchtimerDb->getStrValue("DIRECTORY")); + + timerRow.setValue("AUTOTIMERID", searchtimerDb->getIntValue("ID")); + timerRow.setValue("AUTOTIMERNAME", searchtimerDb->getStrValue("NAME")); + timerRow.setValue("AUTOTIMERINSSP", searchtimerDb->getIntValue("INSSP")); + timerRow.setValue("EXPRESSION", searchtimerDb->getStrValue("EXPRESSION")); + + if (ptyRecName && namingmode != tnmDefault) + { + // execupe python - calc recording name + + if (ptyRecName->execute(useeventsDb, namingmode, tmplExpression) == success) + { + tell(0, "Info: The recording name calculated by 'recording.py' is '%s'", + ptyRecName->getResult()); + + if (!isEmpty(ptyRecName->getResult())) + timerRow.setValue("FILE", ptyRecName->getResult()); + } + } + + status = modifyCreateTimer(&timerRow, timerid); + + // on scuccess add to timersdone .. + + if (status == success) + { + timersDoneDb->clear(); + timersDoneDb->setCharValue("STATE", tdsTimerRequested); + timersDoneDb->setValue("SOURCE", "epgd"); + timersDoneDb->setValue("AUTOTIMERID", id); + timersDoneDb->setValue("TIMERID", timerid); + timersDoneDb->setValue("TITLE", useeventsDb->getStrValue("TITLE")); + timersDoneDb->setValue("COMPTITLE", useeventsDb->getStrValue("COMPTITLE")); + + if (!useeventsDb->getValue("SHORTTEXT")->isEmpty()) + timersDoneDb->setValue("SHORTTEXT", useeventsDb->getStrValue("SHORTTEXT")); + if (!useeventsDb->getValue("COMPSHORTTEXT")->isEmpty()) + timersDoneDb->setValue("COMPSHORTTEXT", useeventsDb->getStrValue("COMPSHORTTEXT")); + + if (!useeventsDb->getValue("LONGDESCRIPTION")->isEmpty()) + timersDoneDb->setValue("LONGDESCRIPTION", useeventsDb->getStrValue("LONGDESCRIPTION")); + if (!useeventsDb->getValue("COMPLONGDESCRIPTION")->isEmpty()) + timersDoneDb->setValue("COMPLONGDESCRIPTION", useeventsDb->getStrValue("COMPLONGDESCRIPTION")); + + if (!useeventsDb->getValue("EPISODECOMPNAME")->isEmpty()) + timersDoneDb->setValue("EPISODECOMPNAME", useeventsDb->getStrValue("EPISODECOMPNAME")); + if (!useeventsDb->getValue("EPISODECOMPSHORTNAME")->isEmpty()) + timersDoneDb->setValue("EPISODECOMPSHORTNAME", useeventsDb->getStrValue("EPISODECOMPSHORTNAME")); + if (!useeventsDb->getValue("EPISODECOMPPARTNAME")->isEmpty()) + + timersDoneDb->setValue("EPISODECOMPPARTNAME", useeventsDb->getStrValue("EPISODECOMPPARTNAME")); + if (!useeventsDb->getValue("EPISODELANG")->isEmpty()) + timersDoneDb->setValue("EPISODELANG", useeventsDb->getStrValue("EPISODELANG")); + if (!useeventsDb->getValue("EPISODESEASON")->isEmpty()) + timersDoneDb->setValue("EPISODESEASON", useeventsDb->getIntValue("EPISODESEASON")); + if (!useeventsDb->getValue("EPISODEPART")->isEmpty()) + timersDoneDb->setValue("EPISODEPART", useeventsDb->getIntValue("EPISODEPART")); + + timersDoneDb->setValue("CHANNELID", useeventsDb->getStrValue("CHANNELID")); + timersDoneDb->setValue("STARTTIME", useeventsDb->getIntValue("STARTTIME")); + timersDoneDb->setValue("DURATION", useeventsDb->getIntValue("DURATION")); + timersDoneDb->setValue("EXPRESSION", searchtimerDb->getStrValue("EXPRESSION")); + + if (!isEmpty(channelname)) + timersDoneDb->setValue("CHANNELNAME", channelname); + + timersDoneDb->insert(); + + int doneid = timersDoneDb->getLastInsertId(); + + tell(0, "AUTOTIMER: Created timer request (%d) for (%s) '%s' / '%s' on channel '%s' at %s with doneid (%d)", + timerid, + l2pTime(useeventsDb->getIntValue("STARTTIME")).c_str(), + useeventsDb->getStrValue("TITLE"), useeventsDb->getStrValue("SHORTTEXT"), + channelname, toWeekdayName(weekdayOf(useeventsDb->getIntValue("STARTTIME"))), + doneid); + + timerDb->clear(); + timerDb->setValue("ID", timerid); + timerDb->setValue("VDRUUID", searchtimerDb->getStrValue("VDRUUID")); + + if (timerDb->find()) + { + timerDb->setValue("DONEID", doneid); + timerDb->update(); + } + } + + return status; +} + +//*************************************************************************** +// Modify Timer (copy paste from cMenuDb of epg2vdr/httpd) +// +// - timerRow contains the destination vdrUuid +//*************************************************************************** + +int cSearchTimer::modifyCreateTimer(cDbRow* timerRow, int& newid) +{ + int status = success; + int timerid = timerRow->getIntValue("ID"); + int knownTimer = timerid != na; + int move = no; + + newid = na; + connection->startTransaction(); + + timerDb->clear(); + + // lookup known (existing) timer + + if (knownTimer) + { + timerDb->copyValues(timerRow, cDBS::ftPrimary); + + if (!timerDb->find()) + { + connection->commit(); + + tell(0, "Timer (%d) at vdr '%s' not found, aborting modify request!", + timerid, timerDb->getStrValue("VDRUUID")); + + return fail; + } + + // found and all values are loaded! + + // move to another vdr? + + if (!timerDb->hasValue("VDRUUID", timerRow->getStrValue("VDRUUID"))) + move = yes; + } + else + { + timerDb->setValue("VDRUUID", timerRow->getStrValue("VDRUUID")); + } + + if (move) + { + // request 'D'elete of 'old' timer + + timerDb->setCharValue("ACTION", taDelete); + timerDb->setValue("SOURCE", timerRow->getStrValue("SOURCE")); + status = timerDb->update(); + + // triggerVdrs("TIMERJOB", timerDb->getStrValue("VDRUUID")); + + // create new on other vdr + + timerDb->copyValues(timerRow, cDBS::ftData); // takeover all data (can be modified by user) + timerDb->setValue("ID", 0); + timerDb->setCharValue("ACTION", taCreate); + status += timerDb->insert(); + newid = timerDb->getLastInsertId(); + + if (status == success) + tell(0, "Created 'move' request for timer (%d) at vdr '%s'", + timerid, timerDb->getStrValue("VDRUUID")); + } + else + { + // create 'C'reate oder 'M'odify request ... + + timerDb->copyValues(timerRow, cDBS::ftData); + + timerDb->setCharValue("ACTION", knownTimer ? taModify : taCreate); + + // if (!knownTimer) + // timerDb->setValue("NAMINGMODE", tnmDefault); + + if (knownTimer) + { + status = timerDb->update(); + newid = timerid; + } + else + { + status = timerDb->insert(); + newid = timerDb->getLastInsertId(); + } + + if (status == success) + { + tell(0, "Created '%s' request for timer (%d), event (%ld) at vdr '%s' by autotimer (%ld)", + knownTimer ? "modify" : "create", + newid, timerDb->getIntValue("EVENTID"), timerDb->getStrValue("VDRUUID"), + timerDb->getIntValue("AUTOTIMERID")); + } + } + + connection->commit(); + + // triggerVdrs("TIMERJOB", timerDb->getStrValue("VDRUUID")); + + return status; +} + +//*************************************************************************** +// Check Timer Conficts +//*************************************************************************** +/* +int cSearchTimer::checkTimerConflicts(std::string& mailBody) +{ + int conflicts = 0; + + tell(0, "TCC: Starting timer conflict check"); + + timerDb->clear(); + vdrDb->clear(); + + for (int f = selectAllTimer->find(); f; f = selectAllTimer->fetch()) + { + if (!timerDb->getIntValue("ACTIVE")) + continue; + + if (timerDb->getIntValue("TCCMAILCNT") > 0) + continue; + + tell(2, "TCC: Check conflicts for timer (%ld) '%s' on '%s'", + timerDb->getIntValue("ID"), timerDb->getStrValue("FILE"), + vdrDb->getStrValue("NAME")); + + // calc 'start' and 'end' time of this timer .. + + int sDay = timerDb->getIntValue("DAY"); + int sTime = timerDb->getIntValue("STARTTIME"); + int eDay = timerDb->getIntValue("DAY"); + int eTime = timerDb->getIntValue("ENDTIME"); + + if (eTime < sTime) + eDay += tmeSecondsPerDay; + + time_t lStartTime = sDay + sTime / 100 * tmeSecondsPerHour + sTime % 100 * tmeSecondsPerMinute; + time_t lEndTime = eDay + eTime / 100 * tmeSecondsPerHour + eTime % 100 * tmeSecondsPerMinute; + + // check for conflicts + + std::string mailPart; + int tunerCount = getUsedTransponderAt(lStartTime, lEndTime, mailPart); + + if (tunerCount > vdrDb->getIntValue("TUNERCOUNT")) + { + conflicts++; + + tell(0, "TCC: Timer (%ld) '%s' conflict at '%s - %s' needed %d transponders on '%s'", + timerDb->getIntValue("ID"), + timerDb->getStrValue("FILE"), + l2pTime(lStartTime).c_str(), + l2pTime(lEndTime).c_str(), + tunerCount, + vdrDb->getStrValue("NAME")); + + mailBody += " <tr>\n" + " <th><font face=\"Arial\"> conflict #" + num2Str(conflicts) + + "on" + vdrDb->getStrValue("NAME") + + " </font></th>\n" + " </tr>\n"; + + mailBody += mailPart; + + timerDb->setValue("TCCMAILCNT", timerDb->getIntValue("TCCMAILCNT") + 1); + timerDb->update(); + + // #TODO - reject autotimer ... ? + // rejectTimer(timerDb->getRow()); + } + + else if (tunerCount <= 0) // DEBUG-tell - to be removed? + tell(0, "TCC: Fatal got 0 used transponders for timer (%ld) between %s (%ld) and %s (%ld)", + timerDb->getIntValue("ID"), + l2pTime(lStartTime).c_str(), lStartTime, + l2pTime(lEndTime).c_str(), lEndTime); + } + + selectAllTimer->freeResult(); + + tell(0, "TCC: Finished timer conflict check"); + + return conflicts; +} +*/ +//*************************************************************************** +// Get Used Transponder At +//*************************************************************************** +/* +int cSearchTimer::getUsedTransponderAt(time_t lStartTime, time_t lEndTime, std::string& mailPart) +{ + char buf[1024+TB]; + int count = 0; + + startValue.setValue(lStartTime); + endValue.setValue(lEndTime); + + // #TODO + // this loop don't get all timers at this time and VDR but the conflicting + // to add all timer to the mail a inner select loop for each found transponder is needed! + + for (int f = selectConflictingTimers->find(); f; f = selectConflictingTimers->fetch()) + { + count++; + + tell(3, "TCC: found '%s'", timerDb->getStrValue("FILE")); + + sprintf(buf, + " <tr>" + "<td><font face=\"Arial\">%ld</font></td>" + "<td><font face=\"Arial\">%s</font></td>" + "<td><font face=\"Arial\">%s</font></td>" + "<td><font face=\"Arial\">%s</font></td>" + "<td><font face=\"Arial\">%s</font></td>" + "</tr>\n", + timerDb->getIntValue("ID"), + timerDb->getStrValue("FILE"), + l2pTime(lStartTime, "%d. %b %H:%M").c_str(), + l2pTime(lEndTime, "%H:%M").c_str(), + vdrDb->getStrValue("NAME")); + + mailPart += buf; + } + + selectConflictingTimers->freeResult(); + + return count; +} +*/ +// //*************************************************************************** +// // Reject Timer +// //*************************************************************************** + +// int cSearchTimer::rejectTimer(cDbRow* timerRow) +// { +// tell(0, "Rejecting timer (%ld)", timerRow->getIntValue("ID")); + +// timerJobsDb->clear(); +// timerJobsDb->setValue("TIMERID", timerRow->getIntValue("ID")); +// timerJobsDb->setValue("DONEID", timerRow->getIntValue("DONEID")); +// timerJobsDb->setValue("STATE", "J"); +// timerJobsDb->setValue("ASSUMED", "N"); +// timerJobsDb->insert(); + +// tell(0, "Created delete job for timer (%ld)", timerRow->getIntValue("ID")); + +// return success; +// } diff --git a/lib/searchtimer.h b/lib/searchtimer.h new file mode 100644 index 0000000..ecd3624 --- /dev/null +++ b/lib/searchtimer.h @@ -0,0 +1,86 @@ +/* + * searchtimer.h + * + * See the README file for copyright information + * + */ + +#ifndef __SEARCHTIMER_H +#define __SEARCHTIMER_H + +#include "common.h" +#include "db.h" +#include "epgservice.h" +#include "json.h" + +class Python; + +//*************************************************************************** +// Search Timer +//*************************************************************************** + +class cSearchTimer +{ + public: + + cSearchTimer(); + ~cSearchTimer(); + + int init(const char* confDir); + int initDb(); + int exitDb(); + + int modified(); // check if a search timer is modified by user + int updateSearchTimers(int force = yes, const char* reason = ""); + + int getSearchMatches(cDbRow* searchTimer, json_t* obj); + int getDoneFor(cDbRow* searchTimer, cDbRow* useevent, json_t* obj); + // int checkTimerConflicts(std::string& mailBody); + // int getUsedTransponderAt(time_t lStartTime, time_t lEndTime, std::string& mailBody); + + int prepareDoneSelect(cDbRow* useeventsRow, int repeatfields, cDbStatement*& select); + cDbStatement* prepareSearchStatement(cDbRow* searchTimer, cDbTable* db); + int matchCriterias(cDbRow* searchTimer, cDbRow* event); + + private: + + int createTimer(int id); + int modifyCreateTimer(cDbRow* timerRow, int& newid); + // int rejectTimer(cDbRow* timerRow); + + // data + + Python* ptyRecName; + + cDbConnection* connection; + + cDbTable* searchtimerDb; + cDbTable* useeventsDb; + cDbTable* timersDoneDb; + cDbTable* timerDb; + cDbTable* mapDb; + cDbTable* vdrDb; + + cDbStatement* selectChannelFromMap; + cDbStatement* selectDoneTimer; + cDbStatement* selectActiveSearchtimers; + cDbStatement* selectSearchtimerMaxModSp; + cDbStatement* selectActiveVdrs; + cDbStatement* selectAllTimer; + cDbStatement* selectTimerByEvent; + // cDbStatement* selectConflictingTimers; + + cDbValue startValue; + cDbValue endValue; + + time_t lastSearchTimerUpdate; + + static int searchField[]; + static const char* searchFieldName[]; + static int repeadCheckField[]; + static const char* repeadCheckFieldName[]; + +}; + +//*************************************************************************** +#endif // __SEARCHTIMER_H diff --git a/lib/semtst.c b/lib/semtst.c new file mode 100644 index 0000000..68a784c --- /dev/null +++ b/lib/semtst.c @@ -0,0 +1,42 @@ + + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include "common.h" + +const char* logPrefix = ""; + +int main() +{ + + Sem s(0x3db00001); + + if (s.check() == success) + printf("free \n"); + else + printf("locked \n"); + + printf("inc\n"); + + s.inc(); + + printf("inc done\n"); + sleep(5); + + s.inc(); + + printf("inc done\n"); + sleep(5); + + s.v(); + s.v(); + + if (s.check() == success) + printf("free \n"); + else + printf("locked \n"); + + return 0; +} diff --git a/lib/test.c b/lib/test.c new file mode 100644 index 0000000..e8a0001 --- /dev/null +++ b/lib/test.c @@ -0,0 +1,756 @@ +/* + * test.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <stdint.h> // uint_64_t +#include <sys/time.h> +#include <time.h> + +#include <stdio.h> +#include <string> + +#include "config.h" +#include "common.h" +#include "db.h" +#include "epgservice.h" +#include "dbdict.h" +//#include "wol.h" + +cDbConnection* connection = 0; +const char* logPrefix = ""; + +//*************************************************************************** +// Init Connection +//*************************************************************************** + +void initConnection() +{ + cDbConnection::init(); + + cDbConnection::setHost("10.241.0.3"); + //cDbConnection::setHost("localhost"); + cDbConnection::setPort(3306); + + cDbConnection::setName("epg2vdr"); + cDbConnection::setUser("epg2vdr"); + cDbConnection::setPass("epg"); + + cDbConnection::setConfPath("/etc/epgd/"); + cDbConnection::setEncoding("utf8"); + + connection = new cDbConnection(); +} + +void exitConnection() +{ + cDbConnection::exit(); + + if (connection) + delete connection; +} + +//*************************************************************************** +// +//*************************************************************************** + +void chkCompress() +{ + std::string s = "_+*!#?=&%$< Hallo TEIL Hallo Folge "; + + printf("'%s'\n", s.c_str()); + prepareCompressed(s); + printf("'%s'\n", s.c_str()); + + s = "Place Vendôme - Heiße Diamanten"; + printf("'%s'\n", s.c_str()); + prepareCompressed(s); + printf("'%s'\n", s.c_str()); + + s = "Halöö älter"; + printf("'%s'\n", s.c_str()); + prepareCompressed(s); + printf("'%s'\n", s.c_str()); +} + +//*************************************************************************** +// +//*************************************************************************** + +void chkStatement1() +{ + cDbTable* epgDb = new cDbTable(connection, "events"); + + if (epgDb->open() != success) + { + tell(0, "Could not access database '%s:%d' (%s)", + cDbConnection::getHost(), cDbConnection::getPort(), epgDb->TableName()); + + return ; + } + + tell(0, "---------------------------------------------------"); + + // prepare statement to mark wasted DVB events + + cDbValue* endTime = new cDbValue("starttime+duration", cDBS::ffInt, 10); + cDbStatement* updateDelFlg = new cDbStatement(epgDb); + + // update events set delflg = ?, updsp = ? + // where channelid = ? and source = ? + // and starttime+duration > ? + // and starttime < ? + // and (tableid > ? or (tableid = ? and version <> ?)) + + updateDelFlg->build("update %s set ", epgDb->TableName()); + updateDelFlg->bind(epgDb->getField("DelFlg"), cDBS::bndIn | cDBS::bndSet); + updateDelFlg->bind(epgDb->getField("UpdSp"), cDBS::bndIn | cDBS::bndSet, ", "); + updateDelFlg->build(" where "); + updateDelFlg->bind(epgDb->getField("ChannelId"), cDBS::bndIn | cDBS::bndSet); + updateDelFlg->bind(epgDb->getField("Source"), cDBS::bndIn | cDBS::bndSet, " and "); + + updateDelFlg->bindCmp(0, endTime, ">", " and "); + + updateDelFlg->bindCmp(0, epgDb->getField("StartTime"), 0, "<" , " and "); + updateDelFlg->bindCmp(0, epgDb->getField("TableId"), 0, ">" , " and ("); + updateDelFlg->bindCmp(0, epgDb->getField("TableId"), 0, "=" , " or ("); + updateDelFlg->bindCmp(0, epgDb->getField("Version"), 0, "<>" , " and "); + updateDelFlg->build("));"); + + updateDelFlg->prepare(); + + tell(0, "---------------------------------------------------"); +} + +// //*************************************************************************** +// // +// //*************************************************************************** + +// void chkStatement2() +// { +// cDbTable* imageRefDb = new cTableImageRefs(connection); +// cDbTable* imageDb = new cTableImages(connection); + +// if (imageRefDb->open() != success) +// return ; + +// if (imageDb->open() != success) +// return ; + +// tell(0, "---------------------------------------------------"); + +// cDbStatement* selectAllImages = new cDbStatement(imageRefDb); + +// cDbValue imageData; +// imageData.setField(imageDb->getField(cTableImages::fiImage)); + +// // select r.imagename, r.eventid, r.lfn, i.image from imagerefs r, images i +// // where r.imagename = i.imagename and i.image is not null; + +// selectAllImages->build("select "); +// selectAllImages->setBindPrefix("r."); +// selectAllImages->bind(cTableImageRefs::fiImgName, cDBS::bndOut); +// selectAllImages->bind(cTableImageRefs::fiEventId, cDBS::bndOut, ", "); +// selectAllImages->bind(cTableImageRefs::fiLfn, cDBS::bndOut, ", "); +// selectAllImages->setBindPrefix("i."); +// selectAllImages->bind(&imageData, cDBS::bndOut, ","); +// selectAllImages->clrBindPrefix(); +// selectAllImages->build(" from %s r, %s i where ", imageRefDb->TableName(), imageDb->TableName()); +// selectAllImages->build("r.%s = i.%s and i.%s is not null;", +// imageRefDb->getField(cTableImageRefs::fiImgName)->name, +// imageDb->getField(cTableImages::fiImgName)->name, +// imageDb->getField(cTableImages::fiImage)->name); + +// selectAllImages->prepare(); + + +// tell(0, "---------------------------------------------------"); + +// //delete s; +// delete imageRefDb; +// delete imageDb; +// } + +// //*************************************************************************** +// // +// //*************************************************************************** + +// void chkStatement3() +// { +// int count = 0; +// int lcount = 0; + +// cDbTable* epgDb = new cTableEvents(connection); +// cDbTable* mapDb = new cTableChannelMap(connection); + +// if (epgDb->open() != success) +// return ; + +// if (mapDb->open() != success) +// return ; + +// tell(0, "---------------------------------------------------"); + +// cDbStatement* s = new cDbStatement(epgDb); + +// s->build("select "); +// s->setBindPrefix("e."); +// s->bind(cTableEvents::fiEventId, cDBS::bndOut); +// s->bind(cTableEvents::fiChannelId, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiSource, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiDelFlg, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiFileRef, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiTableId, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiVersion, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiTitle, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiShortText, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiStartTime, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiDuration, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiParentalRating, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiVps, cDBS::bndOut, ", "); +// s->bind(cTableEvents::fiDescription, cDBS::bndOut, ", "); +// s->clrBindPrefix(); +// s->build(" from eventsview e, %s m where ", mapDb->TableName()); +// s->build("e.%s = m.%s and e.%s = m.%s and ", +// epgDb->getField(cTableEvents::fiChannelId)->name, +// mapDb->getField(cTableChannelMap::fiChannelName)->name, +// epgDb->getField(cTableEvents::fiSource)->name, +// mapDb->getField(cTableChannelMap::fiSource)->name); +// s->bindCmp("e", cTableEvents::fiUpdSp, 0, ">"); +// s->build(" order by m.%s;", mapDb->getField(cTableChannelMap::fiChannelName)->name); + +// s->prepare(); + +// epgDb->clear(); +// epgDb->setValue(cTableEvents::fiUpdSp, (double)0); +// epgDb->setValue(cTableEvents::fiSource, "vdr"); // used by selectUpdEventsByChannel +// epgDb->setValue(cTableEvents::fiChannelId, "xxxxxxxxxxxxx"); // used by selectUpdEventsByChannel + +// int channels = 0; +// char chan[100]; *chan = 0; + +// tell(0, "---------------------------------------------------"); + +// for (int found = s->find(); found; found = s->fetch()) +// { +// if (!*chan || strcmp(chan, epgDb->getStrValue(cTableEvents::fiChannelId)) != 0) +// { +// if (*chan) +// tell(0, "processed %-20s with %d events", chan, count - lcount); + +// lcount = count; +// channels++; +// strcpy(chan, epgDb->getStrValue(cTableEvents::fiChannelId)); + +// tell(0, "processing %-20s now", chan); +// } + +// tell(0, "-> '%s' - (%ld)", epgDb->getStrValue(cTableEvents::fiChannelId), +// epgDb->getIntValue(cTableEvents::fiEventId)); + + +// count++; +// } + +// s->freeResult(); + +// tell(0, "---------------------------------------------------"); +// tell(0, "updated %d channels and %d events", channels, count); +// tell(0, "---------------------------------------------------"); + +// delete s; +// delete epgDb; +// delete mapDb; +// } + +// //*************************************************************************** +// // +// //*************************************************************************** + +// void chkStatement4() +// { +// cDbTable* eventDb = new cTableEvents(connection); +// if (eventDb->open() != success) return; + +// cDbTable* imageRefDb = new cTableImageRefs(connection); +// if (imageRefDb->open() != success) return; + +// cDbTable* imageDb = new cTableImages(connection); +// if (imageDb->open() != success) return; + +// // select e.masterid, r.imagename, r.eventid, r.lfn, i.image +// // from imagerefs r, images i, events e +// // where r.imagename = i.imagename +// // and e.eventid = r.eventid, +// // and i.image is not null +// // and (i.updsp > ? or r.updsp > ?); + +// cDBS::FieldDef masterFld = { "masterid", cDBS::ffUInt, 0, 999, cDBS::ftData }; +// cDbValue masterId; +// cDbValue imageData; +// cDbValue imageUpdSp; + +// masterId.setField(&masterFld); +// imageData.setField(imageDb->getField(cTableImages::fiImage)); +// imageUpdSp.setField(imageDb->getField(cTableImages::fiUpdSp)); + +// cDbStatement* selectAllImages = new cDbStatement(imageRefDb); + +// selectAllImages->build("select "); +// selectAllImages->setBindPrefix("e."); +// selectAllImages->bind(&masterId, cDBS::bndOut); +// selectAllImages->setBindPrefix("r."); +// selectAllImages->bind(cTableImageRefs::fiImgName, cDBS::bndOut, ", "); +// selectAllImages->bind(cTableImageRefs::fiEventId, cDBS::bndOut, ", "); +// selectAllImages->bind(cTableImageRefs::fiLfn, cDBS::bndOut, ", "); +// selectAllImages->setBindPrefix("i."); +// selectAllImages->bind(&imageData, cDBS::bndOut, ", "); +// selectAllImages->clrBindPrefix(); +// selectAllImages->build(" from %s r, %s i, %s e where ", +// imageRefDb->TableName(), imageDb->TableName(), eventDb->TableName()); +// selectAllImages->build("e.%s = r.%s and i.%s = r.%s and i.%s is not null and (", +// eventDb->getField(cTableEvents::fiEventId)->name, +// imageRefDb->getField(cTableImageRefs::fiEventId)->name, +// imageDb->getField(cTableImageRefs::fiImgName)->name, +// imageRefDb->getField(cTableImageRefs::fiImgName)->name, +// imageDb->getField(cTableImages::fiImage)->name); +// selectAllImages->bindCmp("i", &imageUpdSp, ">"); +// selectAllImages->build(" or "); +// selectAllImages->bindCmp("r", cTableImageRefs::fiUpdSp, 0, ">"); +// selectAllImages->build(");"); + +// selectAllImages->prepare(); + +// imageRefDb->clear(); +// imageRefDb->setValue(cTableImageRefs::fiUpdSp, 1377733333L); +// imageUpdSp.setValue(1377733333L); + +// int count = 0; +// for (int res = selectAllImages->find(); res; res = selectAllImages->fetch()) +// { +// count ++; +// } +// tell(0,"%d", count); +// } + +// //*************************************************************************** +// // +// //*************************************************************************** + +// int structure() +// { +// cDbTable* table = new cTableEvents(connection); +// //cDbTable* table = new cTableVdrs(connection); + +// if (table->open(yes) != success) +// return fail; + +// // table->validateStructure(); + +// delete table; + +// return done; +// } + +//*************************************************************************** +// Content Of +//*************************************************************************** + +int contentOf(char* buf, const char* tag, const char* xml) +{ + std::string sTag = "<" + std::string(tag) + ">"; + std::string eTag = "</" + std::string(tag) + ">"; + + const char* s; + const char* e; + + *buf = 0; + + if ((s = strstr(xml, sTag.c_str())) && (e = strstr(xml, eTag.c_str()))) + { + s += strlen(sTag.c_str()); + + sprintf(buf, "%.*s", (int)(e-s), s); + + return success; + } + + return fail; +} + +//*************************************************************************** +// Get Timer Id Of +//*************************************************************************** + +long getTimerIdOf(const char* aux) +{ + char epgaux[1000+TB]; + char tid[100+TB]; + + if (isEmpty(aux)) + return na; + + if (contentOf(epgaux, "epgd", aux) != success) + return na; + + if (contentOf(tid, "timerid", epgaux) != success) + return na; + + return atol(tid); +} + +//*************************************************************************** +// Remove Tag +//*************************************************************************** + +void removeTag(char* xml, const char* tag) +{ + std::string sTag = "<" + std::string(tag) + ">"; + std::string eTag = "</" + std::string(tag) + ">"; + + const char* s; + const char* e; + + if ((s = strstr(xml, sTag.c_str())) && (e = strstr(xml, eTag.c_str()))) + { + char tmp[1000+TB]; + + e += strlen(eTag.c_str()); + + // sicher ist sicher ;) + + if (e <= s) + return; + + sprintf(tmp, "%.*s%s", int(s-xml), xml, e); + + strcpy(xml, tmp); + } +} + +//*************************************************************************** +// Insert Tag +//*************************************************************************** + +int insertTag(char* xml, const char* parent, const char* tag, int value) +{ + char tmp[1000+TB]; + std::string sTag = "<" + std::string(parent) + ">"; + const char* s; + + if ((s = strstr(xml, sTag.c_str()))) + { + s += strlen(sTag.c_str()); + sprintf(tmp, "%.*s<%s>%d</%s>%s", int(s-xml), xml, tag, value, tag, s); + } + else + { + sprintf(tmp, "%s<%s><%s>%d</%s></%s>", xml, parent, tag, value, tag, parent); + } + + strcpy(xml, tmp); + + return success; +} + +//*************************************************************************** +// +//*************************************************************************** + +void statementrecording() +{ + int insert; + + tell(0, "---------------------------------"); + + cDbTable* recordingListDb = new cDbTable(connection, "recordinglist"); + if (recordingListDb->open() != success) return ; + + recordingListDb->clear(); + +#ifdef USEMD5 + md5Buf md5path; + createMd5("rec->FileName() dummy", md5path); + recordingListDb->setValue("MD5PATH", md5path); +#else + recordingListDb->setValue("MD5PATH", "dummy"); +#endif + + recordingListDb->setValue("OWNER", "me"); + recordingListDb->setValue("STARTTIME", 12121212); + + insert = !recordingListDb->find(); + recordingListDb->clearChanged(); + + tell(0, "#1 %d changes", recordingListDb->getChanges()); + + // recordingListDb->setValue("STATE", "E"); + recordingListDb->getValue("STATE")->setNull(); + recordingListDb->setValue("PATH", "rec->FileName()"); + recordingListDb->setValue("TITLE", "title"); + recordingListDb->setValue("SHORTTEXT", "subTitle"); + // recordingListDb->setValue("DESCRIPTION", "description"); + + recordingListDb->setValue("DURATION", 120*60); + recordingListDb->setValue("EVENTID", 1212); + recordingListDb->setValue("CHANNELID", "xxxxxx"); + + tell(0, "#2 %d changes", recordingListDb->getChanges()); + recordingListDb->setValue("FSK", yes); + tell(0, "#3 %d changes", recordingListDb->getChanges()); + + // don't toggle uuid if already set! + + if (recordingListDb->getValue("VDRUUID")->isNull()) + recordingListDb->setValue("VDRUUID", "11111"); + + if (insert || recordingListDb->getChanges()) + { + tell(0, "storing '%s' due to %d changes ", insert ? "insert" : "update", recordingListDb->getChanges()); + recordingListDb->store(); + } + + recordingListDb->reset(); + + tell(0, "---------------------------------"); + + delete recordingListDb; +} + +//*************************************************************************** +// +//*************************************************************************** + +void statementTimer() +{ + cDbValue timerState; + cDbValue timerAction; + cDbFieldDef timerStateDef("STATE", "state", cDBS::ffAscii, 100, cDBS::ftData); + cDbFieldDef timerActionDef("ACTION", "action", cDBS::ffAscii, 100, cDBS::ftData); + + cEpgConfig::loglevel = 0; + + cDbTable* timerDb = new cDbTable(connection, "timers"); + if (timerDb->open() != success) return ; + + cDbTable* useeventsDb = new cDbTable(connection, "useevents"); + if (useeventsDb->open() != success) return ; + + // select t.*, + // e.eventid, e.channelid, e.title, e.shorttext, e.shortdescription, e.category, e.genre, e.tipp + // from timers t left outer join events e + // on (t.eventid = e.masterid and e.updflg in (...)) + // where + // t.state in (?) + + timerState.setField(&timerStateDef); + timerAction.setField(&timerActionDef); + + cDbStatement* selectAllTimer = new cDbStatement(timerDb); + + selectAllTimer->build("select "); + selectAllTimer->setBindPrefix("t."); + selectAllTimer->bindAllOut(); + selectAllTimer->setBindPrefix("e."); + selectAllTimer->bind(useeventsDb, "USEID", cDBS::bndOut, ", "); + selectAllTimer->bind(useeventsDb, "CHANNELID", cDBS::bndOut, ", "); + selectAllTimer->bind(useeventsDb, "TITLE", cDBS::bndOut, ", "); + selectAllTimer->bind(useeventsDb, "SHORTTEXT", cDBS::bndOut, ", "); + selectAllTimer->bind(useeventsDb, "SHORTDESCRIPTION", cDBS::bndOut, ", "); + selectAllTimer->bind(useeventsDb, "CATEGORY", cDBS::bndOut, ", "); + selectAllTimer->bind(useeventsDb, "GENRE", cDBS::bndOut, ", "); + selectAllTimer->bind(useeventsDb, "TIPP", cDBS::bndOut, ", "); + selectAllTimer->clrBindPrefix(); + selectAllTimer->build(" from %s t left outer join %s e", + timerDb->TableName(), "eventsviewplain"); + selectAllTimer->build(" on (t.eventid = e.cnt_useid) and e.updflg in (%s)", cEventState::getVisible()); + + selectAllTimer->setBindPrefix("t."); + selectAllTimer->build(" where "); + + selectAllTimer->bindInChar("t", timerDb->getField("STATE")->getDbName(), &timerState, " and ("); + selectAllTimer->build(" or t.%s is null)", timerDb->getField("STATE")->getDbName()); + + selectAllTimer->bindInChar("t", timerDb->getField("ACTION")->getDbName(), &timerAction, " and ("); + selectAllTimer->build(" or t.%s is null)", timerDb->getField("ACTION")->getDbName()); + + // selectAllTimer->bindInChar(0, "STATE", &timerState); + + cEpgConfig::loglevel = 2; + selectAllTimer->prepare(); + + // --------------------------------- + + timerDb->clear(); + timerState.setValue("P,R,u"); + timerState.setValue("C,M,A,F,a"); + // timerState.setValue("A,D,P"); + + tell(0, "---------------------------------"); + + for (int found = selectAllTimer->find(); found; found = selectAllTimer->fetch()) + { + tell(0, "%ld) %s/%s- %s", + timerDb->getIntValue("ID"), + timerDb->getStrValue("STATE"), + timerDb->getStrValue("ACTION"), + timerDb->getStrValue("FILE")); + } + + tell(0, "---------------------------------"); + + delete selectAllTimer; + delete timerDb; +} + +void statementVdrs() +{ + cDbTable* vdrDb = new cDbTable(connection, "vdrs"); + if (vdrDb->open() != success) return ; + + vdrDb->clear(); + vdrDb->setValue("UUID", "10"); + vdrDb->find(); + vdrDb->setValue("VIDEOTOTAL", 1782579); + vdrDb->store(); + + delete vdrDb; +} + +std::map<std::string, int> transponders; + +int tsp(std::string transponder) +{ + size_t endpos = transponder.find_last_of("-"); + + if (endpos == std::string::npos) + return 0; + + transponder = transponder.substr(0, endpos); + transponders[transponder]++; + + return 0; +} + +//*************************************************************************** +// Main +//*************************************************************************** + +int main(int argc, char** argv) +{ + cEpgConfig::logstdout = yes; + cEpgConfig::loglevel = 2; + +/* const char* interface = getFirstInterface(); + + tell(0, "%s: %s - %s [%s]", getFirstInterface(), getIpOf(""), getMaskOf(""), getMacOf("")); + tell(0, "%s: %s - %s [%s]", getFirstInterface(), getIpOf(interface), getMaskOf(interface), getMacOf(interface)); + + // if (sendWol(argv[1], bcastAddressOf(getIpOf(interface), getMaskOf(interface))) != success) + // tell(0, "Error occured during sending the WOL magic packet for mac address '%s' via bcat '%s'", + // argv[1], bcastAddressOf(getIpOf(interface), getMaskOf(interface))); + + return 0; + + tsp("S19.2E-1-1109-5402"); + tsp("S19.2E-1-1109-5404"); + tsp("S19.2E-1-1112-5404"); + + std::map<std::string, int>::iterator it; + + for (it = transponders.begin(); it != transponders.end(); it++) + printf("'%s' (%d)\n", it->first.c_str(), it->second); + + return 0; + + printf("%s\n", getInterfaces()); + + + if (argc > 1) + { + int timerId = getTimerIdOf(argv[1]); + char aux[10000+TB]; + strcpy(aux, argv[1]); + + tell(0, "TimerId = %d", timerId); + + removeTag(aux, "timerid"); + tell(0, "aux: '%s'", aux); + insertTag(aux, "epgd", "timerid", 677776); + tell(0, "aux: '%s'", aux); + + return 0; + } +*/ + setlocale(LC_CTYPE, ""); + char* lang = setlocale(LC_CTYPE, 0); + + if (lang) + { + tell(0, "Set locale to '%s'", lang); + + if ((strcasestr(lang, "UTF-8") != 0) || (strcasestr(lang, "UTF8") != 0)) + tell(0, "detected UTF-8"); + else + tell(0, "no UTF-8"); + } + else + { + tell(0, "Reseting locale for LC_CTYPE failed."); + } + + // read dictionary + + if (dbDict.in("/etc/epgd/epg.dat") != success) +// if (dbDict.in("../configs/epg.dat") != success) + { + tell(0, "Invalid dictionary configuration, aborting!"); + return 1; + } + + // dbDict.show(); + + initConnection(); + + /* + cDbTable* table = new cDbTable(connection, "_test"); + + if (table->open(yes) != success) + { + tell(0, "Could not access database '%s:%d' (%s)", + cDbConnection::getHost(), cDbConnection::getPort(), table->TableName()); + + return 0; + } + + table->setValue("ID", 5); + table->setValue("VDRUUID", "0000"); + + if (!table->find()) + tell(0, "not found"); + table->setValue("INFO", "abcd"); + + table->store(); + + delete table; + + tell(0, "---------------------------------------------------"); + */ + // structure(); + +// chkCompress(); + +// tell(0, "duration was: '%s'", ms2Dur(2340).c_str()); + +// statementVdrs(); + + statementTimer(); + // statementrecording(); + // chkStatement2(); + // chkStatement3(); + // chkStatement4(); exitConnection(); + + return 0; +} diff --git a/lib/thread.c b/lib/thread.c new file mode 100644 index 0000000..5938d75 --- /dev/null +++ b/lib/thread.c @@ -0,0 +1,342 @@ +/* + * thread.c: + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <linux/unistd.h> +#include <sys/resource.h> +#include <sys/syscall.h> +#include <sys/wait.h> +#include <sys/time.h> + +#include "thread.h" + +//*************************************************************************** +// get abs time plus 'millisecondsFromNow' +//*************************************************************************** + +static bool absTime(struct timespec* abstime, int millisecondsFromNow) +{ + struct timeval now; + + if (gettimeofday(&now, 0) == 0) + { + // get current time + + now.tv_sec += millisecondsFromNow / 1000; // add full seconds + now.tv_usec += (millisecondsFromNow % 1000) * 1000; // add microseconds + + // take care of an overflow + + if (now.tv_usec >= 1000000) + { + now.tv_sec++; + now.tv_usec -= 1000000; + } + + abstime->tv_sec = now.tv_sec; // seconds + abstime->tv_nsec = now.tv_usec * 1000; // nano seconds + + return success; + } + + return fail; +} + +//*************************************************************************** +// Class cThread +//*************************************************************************** + +cThread::cThread(const char* Description, bool LowPriority) +{ + active = running = no; + childTid = 0; + childThreadId = 0; + description = 0; + silent = no; + + if (Description) + SetDescription("%s", Description); + + lowPriority = LowPriority; +} + +cThread::~cThread() +{ + Cancel(); // just in case the derived class didn't call it + free(description); +} + +void cThread::SetDescription(const char *Description, ...) +{ + free(description); + description = NULL; + + if (Description) + { + va_list ap; + va_start(ap, Description); + vasprintf(&description, Description, ap); + va_end(ap); + } +} + +void *cThread::StartThread(cThread *Thread) +{ + Thread->childThreadId = ThreadId(); + if (Thread->description) + { + tell(Thread->silent ? 2 : 0, "'%s' thread started (pid=%d, tid=%d, prio=%s)", Thread->description, getpid(), Thread->childThreadId, Thread->lowPriority ? "low" : "high"); +#ifdef PR_SET_NAME + if (prctl(PR_SET_NAME, Thread->description, 0, 0, 0) < 0) + tell(0, "%s thread naming failed (pid=%d, tid=%d)", Thread->description, getpid(), Thread->childThreadId); +#endif + } + + if (Thread->lowPriority) + { + Thread->SetPriority(19); + Thread->SetIOPriority(7); + } + + Thread->action(); + + if (Thread->description) + tell(Thread->silent ? 2 : 0, "'%s' thread ended (pid=%d, tid=%d)", Thread->description, getpid(), Thread->childThreadId); + + Thread->running = false; + Thread->active = false; + + return NULL; +} + +//*************************************************************************** +// Priority +//*************************************************************************** + +void cThread::SetPriority(int priority) +{ + if (setpriority(PRIO_PROCESS, 0, priority) < 0) + tell(0, "Error: Setting priority failed"); +} + +void cThread::SetIOPriority(int priority) +{ + if (syscall(SYS_ioprio_set, 1, 0, (priority & 0xff) | (3 << 13)) < 0) // idle class + tell(0, "Error: Setting io priority failed"); +} + +//*************************************************************************** +// Start +//*************************************************************************** + +#define THREAD_STOP_TIMEOUT 3000 // ms to wait for a thread to stop before newly starting it +#define THREAD_STOP_SLEEP 30 // ms to sleep while waiting for a thread to stop + +bool cThread::Start(int s) +{ + silent = s; + + if (!running) + { + if (active) + { + // Wait until the previous incarnation of this thread has completely ended + // before starting it newly: + + cMyTimeMs RestartTimeout; + + while (!running && active && RestartTimeout.Elapsed() < THREAD_STOP_TIMEOUT) + cCondWait::SleepMs(THREAD_STOP_SLEEP); + } + if (!active) + { + active = running = true; + + if (pthread_create(&childTid, NULL, (void *(*) (void *))&StartThread, (void *)this) == 0) + { + pthread_detach(childTid); // auto-reap + } + else + { + tell(0, "Error: Thread won't start"); + active = running = false; + return false; + } + } + } + + return true; +} + +bool cThread::Active(void) +{ + if (active) + { + int err; + + if ((err = pthread_kill(childTid, 0)) != 0) + { + if (err != ESRCH) + tell(0, "Error: Thread ..."); + childTid = 0; + active = running = false; + } + else + return true; + } + + return false; +} + +void cThread::Cancel(int WaitSeconds) +{ + running = false; + + if (active && WaitSeconds > -1) + { + if (WaitSeconds > 0) + { + for (time_t t0 = time(NULL) + WaitSeconds; time(NULL) < t0; ) + { + if (!Active()) + return; + cCondWait::SleepMs(10); + } + + tell(0, "ERROR: %s thread %d won't end (waited %d seconds) - canceling it...", description ? description : "", childThreadId, WaitSeconds); + } + + pthread_cancel(childTid); + childTid = 0; + active = false; + } +} + +pid_t cThread::ThreadId() +{ + return syscall(__NR_gettid); +} + +//*************************************************************************** +// cCondWait +//*************************************************************************** + +cCondWait::cCondWait() +{ + signaled = false; + pthread_mutex_init(&mutex, NULL); + pthread_cond_init(&cond, NULL); +} + +cCondWait::~cCondWait() +{ + pthread_cond_broadcast(&cond); // wake up any sleepers + pthread_cond_destroy(&cond); + pthread_mutex_destroy(&mutex); +} + +void cCondWait::SleepMs(int TimeoutMs) +{ + cCondWait w; + w.Wait(max(TimeoutMs, 3)); // making sure the time is >2ms to avoid a possible busy wait +} + +bool cCondWait::Wait(int TimeoutMs) +{ + pthread_mutex_lock(&mutex); + + if (!signaled) + { + if (TimeoutMs) + { + struct timespec abstime; + + if (absTime(&abstime, TimeoutMs) == success) + { + while (!signaled) + { + if (pthread_cond_timedwait(&cond, &mutex, &abstime) == ETIMEDOUT) + break; + } + } + } + else + pthread_cond_wait(&cond, &mutex); + } + + bool r = signaled; + signaled = false; + pthread_mutex_unlock(&mutex); + + return r; +} + +void cCondWait::Signal() +{ + pthread_mutex_lock(&mutex); + signaled = true; + pthread_cond_broadcast(&cond); + pthread_mutex_unlock(&mutex); +} + +//*************************************************************************** +// cCondVar +//*************************************************************************** + +cCondVar::cCondVar(void) +{ + pthread_cond_init(&cond, 0); +} + +cCondVar::~cCondVar() +{ + pthread_cond_broadcast(&cond); // wake up any sleepers + pthread_cond_destroy(&cond); +} + +void cCondVar::Wait(cMyMutex &Mutex) +{ + if (Mutex.locked) + { + int locked = Mutex.locked; + Mutex.locked = 0; // have to clear the locked count here, as pthread_cond_wait + // does an implicit unlock of the mutex + pthread_cond_wait(&cond, &Mutex.mutex); + Mutex.locked = locked; + } +} + +bool cCondVar::TimedWait(cMyMutex &Mutex, int TimeoutMs) +{ + bool r = true; // true = condition signaled, false = timeout + + if (Mutex.locked) + { + struct timespec abstime; + + if (absTime(&abstime, TimeoutMs) == success) + { + int locked = Mutex.locked; + + // have to clear the locked count here, as pthread_cond_timedwait + // does an implicit unlock of the mutex. + + Mutex.locked = 0; + + if (pthread_cond_timedwait(&cond, &Mutex.mutex, &abstime) == ETIMEDOUT) + r = false; + + Mutex.locked = locked; + } + } + + return r; +} + +void cCondVar::Broadcast(void) +{ + pthread_cond_broadcast(&cond); +} diff --git a/lib/thread.h b/lib/thread.h new file mode 100644 index 0000000..1598ff2 --- /dev/null +++ b/lib/thread.h @@ -0,0 +1,92 @@ +/* + * thread.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __LIB_THREAD_H +#define __LIB_THREAD_H + +#include "common.h" + +//*************************************************************************** +// Class cThread +//*************************************************************************** + +class cThread +{ + public: + + cThread(const char *Description = NULL, bool LowPriority = false); + virtual ~cThread(); + + void SetDescription(const char* Description, ...) __attribute__ ((format (printf, 2, 3))); + bool Start(int s = no); + bool Active(); + void SetPriority(int priority); + void SetIOPriority(int priority); + + static pid_t ThreadId(); + + private: + + static void* StartThread(cThread *Thread); + + bool active; + bool running; + pthread_t childTid; + pid_t childThreadId; + cMyMutex mutex; + char* description; + bool lowPriority; + int silent; + + protected: + + virtual void action() = 0; + + void Lock() { mutex.Lock(); } + void Unlock() { mutex.Unlock(); } + bool Running() { return running; } + void Cancel(int WaitSeconds = 0); +}; + +class cCondWait +{ + public: + + cCondWait(); + ~cCondWait(); + + bool Wait(int TimeoutMs = 0); + void Signal(); + + static void SleepMs(int TimeoutMs); + + private: + + pthread_mutex_t mutex; + pthread_cond_t cond; + bool signaled; +}; + +class cCondVar +{ + public: + + cCondVar(); + ~cCondVar(); + + void Wait(cMyMutex &Mutex); + bool TimedWait(cMyMutex &Mutex, int TimeoutMs); + void Broadcast(); + + private: + + pthread_cond_t cond; +}; + +//*************************************************************************** + +#endif // __LIB_THREAD_H @@ -0,0 +1,823 @@ +/* + * menu.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "svdrpclient.h" +#include "plgconfig.h" +#include "menu.h" + +#include "update.h" + +//*************************************************************************** +// Object +//*************************************************************************** + +cMenuDb::cMenuDb() +{ + vdrList = 0; + vdrCount = 0; + vdrUuidList = 0; + + timersCacheMaxUpdsp = 0; + dbInitialized = no; + connection = 0; + + timerDb = 0; + vdrDb = 0; + timerDoneDb = 0; + userDb = 0; + searchtimerDb = 0; + recordingListDb = 0; + useeventsDb = 0; + + selectTimers = 0; + selectMaxUpdSp = 0; + selectTimerById = 0; + selectActiveVdrs = 0; + selectAllVdrs = 0; + selectAllUser = 0; + selectSearchTimers = 0; + selectSearchTimerByName = 0; + + selectDoneTimerByStateTitleOrder = 0; + selectDoneTimerByStateTimeOrder = 0; + selectRecordingForEvent = 0; + selectRecordingForEventByLv = 0; + + webLoginEnabled = no; + user = "@"; + startWithSched = no; + + search = new cSearchTimer(); + + userTimes = new cUserTimes; + initDb(); +} + +cMenuDb::~cMenuDb() +{ + exitDb(); + delete userTimes; +} + +cDbFieldDef startTimeDef("starttime", "starttime", cDBS::ffInt, 0, cDBS::ftData); +cDbFieldDef timerStateDef("state", "state", cDBS::ffAscii, 100, cDBS::ftData); +cDbFieldDef timerActionDef("action", "action", cDBS::ffAscii, 100, cDBS::ftData); + +//*************************************************************************** +// initDb / exitDb +//*************************************************************************** + +int cMenuDb::initDb() +{ + int status = success; + + exitDb(); + + connection = new cDbConnection(); + + timerDb = new cDbTable(connection, "timers"); + if (timerDb->open() != success) return fail; + + vdrDb = new cDbTable(connection, "vdrs"); + if (vdrDb->open() != success) return fail; + + timerDoneDb = new cDbTable(connection, "timersdone"); + if (timerDoneDb->open() != success) return fail; + + userDb = new cDbTable(connection, "users"); + if (userDb->open() != success) return fail; + + searchtimerDb = new cDbTable(connection, "searchtimers"); + if (searchtimerDb->open() != success) return fail; + + recordingListDb = new cDbTable(connection, "recordinglist"); + if (recordingListDb->open() != success) return fail; + + useeventsDb = new cDbTable(connection, "useevents"); + if ((status = useeventsDb->open() != success)) return status; + + if ((status = cParameters::initDb(connection)) != success) + return status; + + valueStartTime.setField(&startTimeDef); + timerState.setField(&timerStateDef); + timerAction.setField(&timerActionDef); + + //----------- + // select + // max(updsp) + // from timers + + selectMaxUpdSp = new cDbStatement(timerDb); + + selectMaxUpdSp->build("select "); + selectMaxUpdSp->bind("UPDSP", cDBS::bndOut, "max("); + selectMaxUpdSp->build(") from %s", timerDb->TableName()); + + status += selectMaxUpdSp->prepare(); + + // ---------- + // select + // t.*, + // t.day + t.starttime div 100 * 60 * 60 + t.starttime % 100 * 60, + // v.name, v.state + // from timers t, vdrs v + // where + // t.vdruuid = v.uuid + // and (t.state in ('P','R') or t.state is null) + // order by t.day, t.starttime + + selectTimers = new cDbStatement(timerDb); + + selectTimers->build("select "); + selectTimers->setBindPrefix("t."); + selectTimers->bindAllOut(); + selectTimers->bindTextFree(", t.day + t.starttime div 100 * 60 * 60 + t.starttime % 100 * 60", + &valueStartTime, cDBS::bndOut); + selectTimers->setBindPrefix("v."); + selectTimers->bind(vdrDb, "NAME", cDBS::bndOut, ", "); + selectTimers->bind(vdrDb, "UUID", cDBS::bndOut, ", "); + selectTimers->bind(vdrDb, "STATE", cDBS::bndOut, ", "); + selectTimers->clrBindPrefix(); + selectTimers->build(" from %s t, %s v where ", + timerDb->TableName(), vdrDb->TableName()); + selectTimers->build("t.%s = v.%s", + timerDb->getField("VDRUUID")->getDbName(), + vdrDb->getField("UUID")->getDbName()); + + selectTimers->bindInChar("t", timerDb->getField("STATE")->getDbName(), &timerState, " and ("); + selectTimers->build(" or t.%s is null)", timerDb->getField("STATE")->getDbName()); + + selectTimers->bindInChar("t", timerDb->getField("ACTION")->getDbName(), &timerAction, " and ("); + selectTimers->build(" or t.%s is null)", timerDb->getField("ACTION")->getDbName()); + + // selectTimers->build(" and (t.%s in ('P','R') or t.%s is null)", + // timerDb->getField("STATE")->getDbName(), + // timerDb->getField("STATE")->getDbName()); + + selectTimers->build(" order by t.%s, t.%s", + timerDb->getField("DAY")->getDbName(), + timerDb->getField("STARTTIME")->getDbName()); + + status += selectTimers->prepare(); + + // select + // t.*, + // t.day + t.starttime div 100 * 60 * 60 + t.starttime % 100 * 60, + // v.name, v.state + // from timers t, vdrs v + // where + // t.id = ? + // and t.vdruuid = v.uuid + + selectTimerById = new cDbStatement(timerDb); + + selectTimerById->build("select "); + selectTimerById->setBindPrefix("t."); + selectTimerById->bindAllOut(); + selectTimerById->bindTextFree(", t.day + t.starttime div 100 * 60 * 60 + t.starttime % 100 * 60", + &valueStartTime, cDBS::bndOut); + selectTimerById->setBindPrefix("v."); + selectTimerById->bind(vdrDb, "NAME", cDBS::bndOut, ", "); + selectTimerById->bind(vdrDb, "UUID", cDBS::bndOut, ", "); + selectTimerById->bind(vdrDb, "STATE", cDBS::bndOut, ", "); + selectTimerById->clrBindPrefix(); + selectTimerById->build(" from %s t, %s v where ", timerDb->TableName(), vdrDb->TableName()); + selectTimerById->setBindPrefix("t."); + selectTimerById->bind("ID", cDBS::bndIn | cDBS::bndSet); + selectTimerById->clrBindPrefix(); + selectTimerById->build(" and t.%s = v.%s", + timerDb->getField("VDRUUID")->getDbName(), + vdrDb->getField("UUID")->getDbName()); + + status += selectTimerById->prepare(); + + // ---------- + // select ip, svdrp from vdrs + // where state = 'attached' + + selectActiveVdrs = new cDbStatement(vdrDb); + + selectActiveVdrs->build("select "); + selectActiveVdrs->bind("IP", cDBS::bndOut); + selectActiveVdrs->bind("SVDRP", cDBS::bndOut, ", "); + selectActiveVdrs->bind("UUID", cDBS::bndOut, ", "); + selectActiveVdrs->build(" from %s where %s = 'attached' and %s > 0", + vdrDb->TableName(), + vdrDb->getField("STATE")->getDbName(), + vdrDb->getField("SVDRP")->getDbName()); + + status += selectActiveVdrs->prepare(); + + // ---------- + // select ip, name, state, uuid from vdrs + + selectAllVdrs = new cDbStatement(vdrDb); + + selectAllVdrs->build("select "); + selectAllVdrs->bind("IP", cDBS::bndOut); + selectAllVdrs->bind("NAME", cDBS::bndOut, ", "); + selectAllVdrs->bind("STATE", cDBS::bndOut, ", "); + selectAllVdrs->bind("UUID", cDBS::bndOut, ", "); + selectAllVdrs->build(" from %s where svdrp > 0", vdrDb->TableName()); + + status += selectAllVdrs->prepare(); + + // ---------- + // select * from users + + selectAllUser = new cDbStatement(userDb); + + selectAllUser->build("select "); + selectAllUser->bindAllOut(); + selectAllUser->build(" from %s", userDb->TableName()); + + status += selectAllUser->prepare(); + + // ---------- + // select * from searchtimers + // where state != 'D' + + selectSearchTimers = new cDbStatement(searchtimerDb); + + selectSearchTimers->build("select "); + selectSearchTimers->bindAllOut(); + selectSearchTimers->build(" from %s where %s != 'D'", + searchtimerDb->TableName(), + searchtimerDb->getField("STATE")->getDbName()); + + status += selectSearchTimers->prepare(); + + // ---------- + // select * from searchtimers + // where state != 'D' + // and name = ? + + selectSearchTimerByName = new cDbStatement(searchtimerDb); + + selectSearchTimerByName->build("select "); + selectSearchTimerByName->bindAllOut(); + selectSearchTimerByName->build(" from %s where %s != 'D'", + searchtimerDb->TableName(), + searchtimerDb->getField("STATE")->getDbName()); + selectSearchTimerByName->bind("NAME", cDBS::bndIn | cDBS::bndSet, " and "); + + status += selectSearchTimerByName->prepare(); + + // ---------- + // select updsp, id, state, title, shorttext, + // from timersdone where state like ? order by title + + selectDoneTimerByStateTitleOrder = new cDbStatement(timerDoneDb); + + selectDoneTimerByStateTitleOrder->build("select "); + selectDoneTimerByStateTitleOrder->bind("ID", cDBS::bndOut); + selectDoneTimerByStateTitleOrder->bind("STATE", cDBS::bndOut, ", "); + selectDoneTimerByStateTitleOrder->bind("STARTTIME", cDBS::bndOut, ", "); + selectDoneTimerByStateTitleOrder->bind("UPDSP", cDBS::bndOut, ", "); + selectDoneTimerByStateTitleOrder->bind("TITLE", cDBS::bndOut, ", "); + selectDoneTimerByStateTitleOrder->bind("SHORTTEXT", cDBS::bndOut, ", "); + selectDoneTimerByStateTitleOrder->clrBindPrefix(); + selectDoneTimerByStateTitleOrder->build(" from %s where ", timerDoneDb->TableName()); + selectDoneTimerByStateTitleOrder->bindCmp(0, "STATE", 0, "like"); + selectDoneTimerByStateTitleOrder->build(" order by %s", timerDoneDb->getField("TITLE")->getDbName()); + + status += selectDoneTimerByStateTitleOrder->prepare(); + + // same statement with + // 'order by starttimer' + + selectDoneTimerByStateTimeOrder = new cDbStatement(timerDoneDb); + selectDoneTimerByStateTimeOrder->build("select "); + selectDoneTimerByStateTimeOrder->bind("ID", cDBS::bndOut); + selectDoneTimerByStateTimeOrder->bind("STATE", cDBS::bndOut, ", "); + selectDoneTimerByStateTimeOrder->bind("STARTTIME", cDBS::bndOut, ", "); + selectDoneTimerByStateTimeOrder->bind("UPDSP", cDBS::bndOut, ", "); + selectDoneTimerByStateTimeOrder->bind("TITLE", cDBS::bndOut, ", "); + selectDoneTimerByStateTimeOrder->bind("SHORTTEXT", cDBS::bndOut, ", "); + selectDoneTimerByStateTimeOrder->clrBindPrefix(); + selectDoneTimerByStateTimeOrder->build(" from %s where ", timerDoneDb->TableName()); + selectDoneTimerByStateTimeOrder->bindCmp(0, "STATE", 0, "like"); + selectDoneTimerByStateTimeOrder->build(" order by %s", timerDoneDb->getField("STARTTIME")->getDbName()); + + status += selectDoneTimerByStateTimeOrder->prepare(); + + // select * + // from recordinglist where + // (state <> 'D' or state is null) + // and title like ? + // and shorttext like ? + + selectRecordingForEvent = new cDbStatement(recordingListDb); + + selectRecordingForEvent->build("select "); + selectRecordingForEvent->bindAllOut(); + selectRecordingForEvent->build(" from %s where ", recordingListDb->TableName()); + selectRecordingForEvent->build(" (%s <> 'D' or %s is null)", + recordingListDb->getField("STATE")->getDbName(), + recordingListDb->getField("STATE")->getDbName()); + selectRecordingForEvent->bindCmp(0, "TITLE", 0, "like", " and "); + selectRecordingForEvent->bindCmp(0, "SHORTTEXT", 0, "like", " and "); + + status += selectRecordingForEvent->prepare(); + + // select * + // from recordinglist where + // (state <> 'D' or state is null) + // and epglvr(title, ?) < 50 +// // order by lv + + selectRecordingForEventByLv = new cDbStatement(recordingListDb); + + selectRecordingForEventByLv->build("select "); + selectRecordingForEventByLv->bindAllOut(); + selectRecordingForEventByLv->build(" from %s where ", recordingListDb->TableName()); + selectRecordingForEventByLv->build(" (%s <> 'D' or %s is null)", + recordingListDb->getField("STATE")->getDbName(), + recordingListDb->getField("STATE")->getDbName()); + selectRecordingForEventByLv->bindTextFree("and epglvr(title, ?) < 50", recordingListDb->getValue("TITLE"), cDBS::bndIn); + + status += selectRecordingForEventByLv->prepare(); + + // search timer stuff + + if (!status) + { + if ((status = search->initDb()) != success) + return status; + } + + // --------------------------------- + // prepare vdr-list for EditStraItem + + int i = 0; + + vdrDb->countWhere("svdrp > 0", vdrCount); + vdrList = new char*[vdrCount]; + vdrUuidList = new char*[vdrCount];; + + vdrDb->clear(); + + for (int f = selectAllVdrs->find(); f; f = selectAllVdrs->fetch()) + { + asprintf(&vdrList[i], "%s%c", vdrDb->getStrValue("NAME"), vdrDb->hasValue("STATE", "attached") ? '*' : ' '); + vdrUuidList[i] = strdup(vdrDb->getStrValue("UUID")); + i++; + } + + selectAllVdrs->freeResult(); + + // some parameters + + user = "@"; + getParameter("webif", "needLogin", webLoginEnabled); + + if (webLoginEnabled) + user += Epg2VdrConfig.user; + + getParameter(user.c_str(), "startWithSched", startWithSched); + + // prepare User Times (quickTimes) + + initUserTimes(); + + // ------ + + dbInitialized = yes; + + return status; +} + +int cMenuDb::exitDb() +{ + dbInitialized = no; + + search->exitDb(); + + if (connection) + { + cParameters::exitDb(); + + delete timerDb; timerDb = 0; + delete vdrDb; vdrDb = 0; + delete timerDoneDb; timerDoneDb = 0; + delete userDb; userDb = 0; + delete searchtimerDb; searchtimerDb = 0; + delete recordingListDb; recordingListDb = 0; + delete useeventsDb; useeventsDb = 0; + + delete selectTimers; selectTimers = 0; + delete selectMaxUpdSp; selectMaxUpdSp = 0; + delete selectTimerById; selectTimerById = 0; + delete selectActiveVdrs; selectActiveVdrs = 0; + delete selectAllVdrs; selectAllVdrs = 0; + delete selectAllUser; selectAllUser = 0; + delete selectSearchTimers; selectSearchTimers = 0; + delete selectSearchTimerByName; selectSearchTimerByName = 0; + + delete selectDoneTimerByStateTitleOrder; selectDoneTimerByStateTitleOrder = 0; + delete selectDoneTimerByStateTimeOrder; selectDoneTimerByStateTimeOrder = 0; + delete selectRecordingForEvent; selectRecordingForEvent = 0; + delete selectRecordingForEventByLv; selectRecordingForEventByLv = 0; + + delete connection; connection = 0; + + for (int i = 0; i < vdrCount; i++) + { + free(vdrList[i]); + free(vdrUuidList[i]); + } + + delete vdrList; + delete vdrUuidList; + } + + return done; +} + +//*************************************************************************** +// Init Users List +//*************************************************************************** + +int cMenuDb::initUserList(char**& userList, int& count, long int& loginEnabled) +{ + int i = 0; + count = 0; + + if (!dbConnected()) + return fail; + + getParameter("webif", "needLogin", loginEnabled); + + userDb->countWhere("1 = 1", count); + userList = new char*[count]; + memset(userList, 0, count*sizeof(char*)); + + userDb->clear(); + + for (int f = selectAllUser->find(); f; f = selectAllUser->fetch()) + { + if (userDb->getIntValue("ACTIVE") > 0) + asprintf(&userList[i++], "%s", userDb->getStrValue("USER")); + } + + return success; +} + +//*************************************************************************** +// Init Times +//*************************************************************************** + +int cMenuDb::initUserTimes() +{ + char* p; + char times[500+TB] = ""; + + userTimes->clear(); + getParameter(user.c_str(), "quickTimes", times); + + tell(3, "DEBUG: got quickTimes [%s] for user '%s'", times, user.c_str()); + + // parse string like: + // PrimeTime=20:15~EarlyNight=22:20~MidNight=00:00~LateNight=02:00 + + p = strtok(times, "~"); + + while (p) + { + char* time; + + if (time = strchr(p, '=')) + *time++ = 0; + + if (!isEmpty(time)) + userTimes->add(time, p); + else + tell(0, "Error: Ignoring invalid quickTime '%s'", p); + + p = strtok(0, "~"); + } + + return done; +} + +//*************************************************************************** +// Lookup Timer +//*************************************************************************** + +int cMenuDb::lookupTimer(const cEvent* event, int& timerMatch, int& remote, int force) +{ + int maxSp = 0; + + timerMatch = tmNone; + remote = no; + + if (!event) + return 0; + + // on change create lookup cache + + timerDb->clear(); + selectMaxUpdSp->execute(); + maxSp = timerDb->getIntValue("UPDSP"); + selectMaxUpdSp->freeResult(); + + if (maxSp != timersCacheMaxUpdsp) + { + tell(1, "DEBUG: Update timer lookup cache %s", force ? "(forced)": ""); + + timersCacheMaxUpdsp = maxSp; + timers.Clear(); + timerDb->clear(); + timerState.setValue("P,R,u"); + timerAction.setValue("C,M,A,F,a"); + + for (int f = selectTimers->find(); f && dbConnected(); f = selectTimers->fetch()) + { + cTimerInfo* info = new cTimerInfo; + + info->timerid = timerDb->getValue("ID")->getIntValue(); + info->state = timerDb->getValue("STATE")->getCharValue(); + info->starttime = valueStartTime.getIntValue(); + info->eventid = timerDb->getIntValue("EVENTID"); + strcpy(info->channelid, timerDb->getStrValue("CHANNELID")); + strcpy(info->uuid, timerDb->getStrValue("VDRUUID")); + + timers.Add(info); + } + + selectTimers->freeResult(); + } + + // lookup timer in cache + + for (const cTimerInfo* ifo = timers.First(); ifo; ifo = timers.Next(ifo)) + { + if (ifo->eventid == event->EventID()) + { + remote = strcmp(ifo->uuid, Epg2VdrConfig.uuid) != 0; + timerMatch = tmFull; + return ifo->timerid; + } + + else if (strcmp(ifo->channelid, event->ChannelID().ToString()) == 0 + && ifo->starttime == event->StartTime()) // #TODO around + { + remote = strcmp(ifo->uuid, Epg2VdrConfig.uuid) != 0; + timerMatch = tmFull; + return ifo->timerid; + } + } + + return 0; +} + +//*************************************************************************** +// Modify Timer +// +// - timerRow contain the actual vdrUuid +// - for move destUuid is set +//*************************************************************************** + +int cMenuDb::modifyTimer(cDbRow* timerRow, const char* destUuid) +{ + int knownTimer = !timerRow->hasValue("ID", (long)na); + int move = knownTimer && destUuid && !timerRow->hasValue("VDRUUID", destUuid); + int timerid = timerDb->getIntValue("ID"); + + // lookup known (existing) timer + + connection->startTransaction(); + + if (knownTimer) + { + timerDb->clear(); + timerDb->copyValues(timerRow, cDBS::ftPrimary); + + if (!timerDb->find()) + { + connection->commit(); + + tell(0, "Timer (%d) at vdr '%s' not found, aborting modify request!", + timerid, timerDb->getStrValue("VDRUUID")); + + return fail; + } + + // found and all values are loaded! + } + + if (move) + { + // request 'D'elete of 'old' timer + + timerDb->setValue("ACTION", "D"); // = taDelete + timerDb->setValue("SOURCE", Epg2VdrConfig.uuid); + timerDb->update(); + + // create new on other vdr + + timerDb->copyValues(timerRow, cDBS::ftData); // takeover all data (can be modified by user) + timerDb->setValue("ID", 0); // don't care on insert! + timerDb->setValue("VDRUUID", destUuid); + timerDb->setValue("ACTION", "C"); // = taCreate + timerDb->setValue("SOURCE", Epg2VdrConfig.uuid); + timerDb->insert(); + + tell(0, "Created 'move' request for timer (%d) at vdr '%s'", + timerid, timerDb->getStrValue("VDRUUID")); + } + else + { + // create 'C'reate oder 'M'odify request ... + + timerDb->copyValues(timerRow, cDBS::ftData); + + timerDb->setCharValue("ACTION", knownTimer ? taModify : taCreate); + timerDb->getValue("STATE")->setNull(); + timerDb->setValue("SOURCE", Epg2VdrConfig.uuid); + + if (!knownTimer) + timerDb->setValue("NAMINGMODE", tnmAuto); + + if (knownTimer) + timerDb->update(); + else + timerDb->insert(); + + tell(0, "Created '%s' request for timer (%d) at vdr '%s'", + knownTimer ? "modify" : "create", + timerid, timerDb->getStrValue("VDRUUID")); + } + + connection->commit(); + triggerVdrs("TIMERJOB"); // trigger all! + + return success; +} + +//*************************************************************************** +// Create Timer +//*************************************************************************** + +int cMenuDb::createTimer(cDbRow* timerRow, const char* destUuid) +{ + // Timer 'C'reate request ... + + timerDb->clear(); + timerDb->copyValues(timerRow, cDBS::ftData); + + timerDb->setValue("VDRUUID", destUuid); + timerDb->setValue("ACTION", "C"); // taCreate + timerDb->setValue("SOURCE", Epg2VdrConfig.uuid); + timerDb->setValue("NAMINGMODE", tnmAuto); + + timerDb->insert(); + triggerVdrs("TIMERJOB", destUuid); + + tell(0, "Created 'create' request for event '%ld' at vdr '%s'", + timerDb->getIntValue("EVENTID"), timerDb->getStrValue("VDRUUID")); + + return done; +} + +//*************************************************************************** +// Delete Timer +//*************************************************************************** + +int cMenuDb::deleteTimer(long timerid) +{ + timerDb->clear(); + timerDb->setValue("ID", timerid); + + if (!selectTimerById->find()) + { + selectTimerById->freeResult(); + tell(0, "Timer (%ld) not found, maybe deleted from other user, aborting", timerid); + Skins.Message(mtInfo, tr("Timer not found, maybe deleted from other user")); + + return fail; + } + + selectTimerById->freeResult(); + + timerDb->setValue("ACTION", "D"); // = taDelete + timerDb->setValue("SOURCE", Epg2VdrConfig.uuid); + timerDb->update(); + + tell(0, "Created delete request for timer (%ld)", timerid); + + triggerVdrs("TIMERJOB", timerDb->getStrValue("VDRUUID")); + + return success; +} + +//*************************************************************************** +// Trigger VDRs +//*************************************************************************** + +int cMenuDb::triggerVdrs(const char* trg, const char* uuid, const char* options) +{ + int count = 0; + + if (!dbConnected()) + return 0; + + if (!isEmpty(uuid)) + { + if (triggerVdr(uuid, trg, options) == success) + count++; + + return count; + } + + vdrDb->clear(); + + for (int f = selectActiveVdrs->find(); f; f = selectActiveVdrs->fetch()) + { + char* uuid = strdup(vdrDb->getStrValue("UUID")); + tell(1, "Trigger VDR '%s' with '%s'", uuid, trg); + + if (triggerVdr(uuid, trg, options) == success) + count++; + + free(uuid); + } + + selectActiveVdrs->freeResult(); + + return count; +} + +int cMenuDb::triggerVdr(const char* uuid, const char* trg, const char* options) +{ + const char* plgs[] = { "epg2vdr", 0 }; // "scraper2vdr" + + // myself - SVDR to myself will lock vdr!! + + if (strcmp(uuid, Epg2VdrConfig.uuid) == 0) + { + if (strcasecmp(trg, "TIMERJOB") == 0) + { + tell(2, "Trigger myself to update timer-jobs"); + oUpdate->triggerTimerJobs(); + } + + return success; + } + + if (!dbConnected()) + { + tell(0, "Error: Lost database connection, aborting trigger"); + return fail; + } + + vdrDb->clear(); + vdrDb->setValue("UUID", uuid); + + if (!vdrDb->find()) + { + tell(0, "Error: Can't lookup VDR with uuid '%s', skipping trigger", uuid); + return fail; + } + + const char* ip = vdrDb->getStrValue("IP"); + unsigned int port = vdrDb->getIntValue("SVDRP"); + + vdrDb->reset(); + + // svdrp connection + + cSvdrpClient cl(ip, port); + + // open tcp connection + + if (cl.open() == success) + { + for (int i = 0; plgs[i]; i++) + { + char* command = 0; + cList<cLine> result; + + asprintf(&command, "PLUG %s %s %s", plgs[i], trg, !isEmpty(options) ? options : ""); + + tell(0, "Send '%s' to '%s' at '%s:%d'", command, plgs[i], ip, port); + + if (!cl.send(command)) + tell(0, "Error: Send '%s' to '%s' at '%s:%d' failed!", + command, plgs[i], ip, port); + else + cl.receive(&result, 2); + + free(command); + } + + cl.close(no); + } + + return success; +} @@ -0,0 +1,522 @@ +/* + * menu.h: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __EPG2VDR_MENU_H +#define __EPG2VDR_MENU_H + +#include <vdr/osdbase.h> +#include <vdr/menuitems.h> + +#include "lib/common.h" +#include "lib/db.h" +#include "lib/epgservice.h" +#include "lib/searchtimer.h" + +#include "menu.h" +#include "ttools.h" +#include "parameters.h" + +class cMenuEpgEditTimer; +class cTimerData; + +//*************************************************************************** +// +//*************************************************************************** + +#define NEWTIMERLIMIT 60 // seconds until the start time of a new timer created from the Schedule menu, + // within which it will go directly into the "Edit timer" menu to allow + // further parameter settings + +#define MAXCHNAMWIDTH 16 // maximum number of characters of channels' short names shown in schedules menus + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) +# define CHNUMWIDTH (numdigits(cChannels::MaxNumber()) + 1) +# define CHNAMWIDTH (std::min(MAXCHNAMWIDTH, cChannels::MaxShortChannelNameLength() + 1)) +#else +# define CHNUMWIDTH (numdigits(Channels.MaxNumber()) + 1) +# define CHNAMWIDTH (std::min(MAXCHNAMWIDTH, Channels.MaxShortChannelNameLength() + 1)) +#endif + +//*************************************************************************** +// Class cMenuDb +//*************************************************************************** + +class cMenuDb : public cParameters +{ + friend class cMenuEpgEvent; + friend class cMenuEpgEditTimer; + friend class cMenuEpgWhatsOn; + friend class cMenuEpgSchedule; + friend class cMenuEpgTimers; + friend class cMenuEpgSearchTimers; + friend class cEpgMenuDones; + friend class cEpgMenuSearchTimerEdit; + friend class cMenuEpgMatchRecordings; + friend class cEpgMenuSearchResult; + friend class cMenuSetupEPG2VDR; + + public: + + cMenuDb(); + virtual ~cMenuDb(); + + int initDb(); + int exitDb(); + + int initUserTimes(); + int initUserList(char**& userList, int& count, long int& loginEnabled); + int lookupTimer(const cEvent* event, int& timerMatch, int& remote, int force = no); + + // timer db + + int createTimer(cDbRow* timerRow, const char* destUuid); + int modifyTimer(cDbRow* timerRow, const char* destUuid = 0); + int deleteTimer(long timerid); + + // + + int triggerVdrs(const char* trg, const char* uuid = "", const char* options = ""); + int triggerVdr(const char* uuid, const char* trg, const char* options = ""); + + protected: + + virtual int dbConnected(int force = no) + { + return dbInitialized + && connection + && (!force || connection->check() == success); + } + + // data + + long webLoginEnabled; + std::string user; + long startWithSched; + + cUserTimes* userTimes; + char** vdrList; + char** vdrUuidList; + int vdrCount; + + // database + + int dbInitialized; + cDbConnection* connection; + + cDbTable* timerDb; + cDbTable* vdrDb; + cDbTable* timerDoneDb; + cDbTable* userDb; + cDbTable* searchtimerDb; + cDbTable* recordingListDb; + cDbTable* useeventsDb; + + cDbStatement* selectTimers; + cDbStatement* selectMaxUpdSp; + cDbStatement* selectTimerById; + cDbStatement* selectActiveVdrs; + cDbStatement* selectAllVdrs; + cDbStatement* selectDoneTimerByState; + cDbStatement* selectAllUser; + cDbStatement* selectSearchTimers; + cDbStatement* selectSearchTimerByName; + cDbStatement* selectDoneTimerByStateTitleOrder; + cDbStatement* selectDoneTimerByStateTimeOrder; + cDbStatement* selectRecordingForEvent; + cDbStatement* selectRecordingForEventByLv; + + cSearchTimer* search; + + cDbValue valueStartTime; + cDbValue timerState; + cDbValue timerAction; + + class cTimerInfo : public cListObject + { + public: + + uint timerid; + char state; + time_t starttime; + uint eventid; + char channelid[100]; + char uuid[sizeUuid+TB]; + }; + + cList<cTimerInfo> timers; + int timersCacheMaxUpdsp; +}; + +//*************************************************************************** +// Class cMenuEditTimer +//*************************************************************************** + +class cMenuEpgEditTimer : public cOsdMenu +{ + public: + + cMenuEpgEditTimer(cMenuDb* db, cEpgTimer* Timer, bool New = no); + virtual ~cMenuEpgEditTimer(); + + virtual eOSState ProcessKey(eKeys Key); + + protected: + + eOSState SetFolder(); + void SetFirstDayItem(); + void SetHelpKeys(); + + public: + + class cTimerData + { + public: + + cTimerData(cMenuDb* p) { memset(this, 0, sizeof(cTimerData)); db = p; } + ~cTimerData() { free(aux); } + + const char* getVdrName() { return db->vdrList[vdrIndex]; } + const char* getVdrUuid() { return db->vdrUuidList[vdrIndex]; } + const char* getLastVdrUuid() { return lastVdrUuid; } + long TimerId() { return timerid; } + + //************************************************** + // From Timer + //************************************************** + + void fromTimer(cEpgTimer* Timer, bool New = no) + { + weekdays = Timer->WeekDays(); + start = Timer->Start(); + stop = Timer->Stop(); + priority = Timer->Priority(); + lifetime = Timer->Lifetime(); + flags = Timer->Flags(); + channel = Timer->Channel(); + day = Timer->Day(); + strcpy(file, Timer->File()); + vdrIndex = 0; + free(aux); aux = 0; + + if (New) + flags |= tfActive; + + if (!isEmpty(Timer->Aux())) + aux = strdup(Timer->Aux()); + + timerid = Timer->TimerId(); + eventid = Timer->EventId(); + state = Timer->State(); + sprintf(stateInfo, "%.300s", Timer->StateInfo()); + action = Timer->Action(); + strcpy(lastVdrUuid, Timer->VdrUuid()); + + for (int i = 0; i < db->vdrCount; i++) + if (strncmp(db->vdrList[i], Timer->VdrName(), strlen(db->vdrList[i])-1) == 0) + vdrIndex = i; + +#ifdef WITH_PIN + fskProtection = Timer->FskProtection(); +#endif + } + + int toRow(cDbRow* timerRow) + { + timerRow->clear(); + + timerRow->setValue("ID", timerid); + timerRow->setValue("VDRUUID", lastVdrUuid); + timerRow->setValue("ACTIVE", (int)flags & tfActive); + timerRow->setValue("CHILDLOCK", fskProtection); + timerRow->setValue("CHANNELID", channel->GetChannelID().ToString()); + timerRow->setValue("FILE", file); + timerRow->setValue("VPS", (int)flags & tfVps); + timerRow->setValue("DAY", day); + timerRow->setValue("WEEKDAYS", weekdays); + timerRow->setValue("STARTTIME", start); + timerRow->setValue("ENDTIME", stop); + timerRow->setValue("PRIORITY", priority); + timerRow->setValue("LIFETIME", lifetime); + timerRow->setValue("EVENTID", eventid); + + return done; + } + + long timerid; + long eventid; + int weekdays; // bitmask, lowest bits: SSFTWTM (the 'M' is the LSB) + int start; + int stop; + int priority; + int fskProtection; + int lifetime; + uint flags; + char state; + char stateInfo[300+TB]; + char action; + int vdrIndex; + const cChannel* channel; + mutable time_t day; + mutable char file[NAME_MAX*2 + TB]; // *2 to be able to hold 'title' and 'episode', which can each be up to 255 characters long + char* aux; + + cMenuDb* db; + char lastVdrUuid[sizeUuid+TB]; + }; + + protected: + + cMenuDb* menuDb; + cTimerData data; + int channelNr; + cMenuEditStrItem* file; + cMenuEditDateItem* day; + cMenuEditDateItem* firstday; +}; + +//*************************************************************************** +// class cEpgMenuTextItem +//*************************************************************************** + +class cEpgMenuTextItem : public cOsdItem +{ + public: + + cEpgMenuTextItem(long aId, const char* text) + { + cid = 0; + id = aId; + SetText(text); + } + + cEpgMenuTextItem(const char* aId, const char* text) + { + cid = strdup(aId); + SetText(text); + } + + virtual ~cEpgMenuTextItem() { free(cid); } + + long getId() { return id; } + const char* getCharId() { return cid; } + + protected: + + long id; + char* cid; +}; + +//*************************************************************************** +// Class cMenuEpgTimers +//*************************************************************************** + +class cMenuEpgTimers : public cOsdMenu +{ + public: + + cMenuEpgTimers(); + virtual ~cMenuEpgTimers(); + + virtual eOSState ProcessKey(eKeys Key); + int refresh(); + + private: + + eOSState edit(); + eOSState create(); + eOSState remove(); + eOSState toggleState(); + eOSState info(); + cEpgTimer* currentTimer(); + void SetHelpKeys(); + + cMenuDb* menuDb; + int helpKeys; + int timersMaxUpdsp; +}; + +//*************************************************************************** +// Class cMenuEpgSearchTimers +//*************************************************************************** + +class cMenuEpgSearchTimers : public cOsdMenu +{ + public: + + cMenuEpgSearchTimers(); + virtual ~cMenuEpgSearchTimers(); + + virtual eOSState ProcessKey(eKeys Key); + int refresh(); + void setHelpKeys(); + + private: + + cMenuDb* menuDb; + int helpKeys; +}; + +//*************************************************************************** +// Class cMenuEpgEvent +//*************************************************************************** + +class cMenuEpgEvent : public cOsdMenu +{ + public: + + cMenuEpgEvent(cMenuDb* db, const cEvent* Event, const cSchedules* schedules, + int timerMatch, bool DispSchedule, bool CanSwitch = false); + virtual void Display(void); + virtual eOSState ProcessKey(eKeys Key); + virtual const char* MenuKind() { return "MenuEvent"; } + + private: + + const cEvent* getNextPrevEvent(const cEvent* event, int step); + int setEvent(const cEvent* Event, int timerMatch); + + cMenuDb* menuDb; + const cSchedules* schedules; + const cEvent* event; + bool dispSchedule; + bool canSwitch; + + std::string prevTime; + std::string nextTime; +}; + +//*************************************************************************** +// Class cMenuEpgScheduleItem +//*************************************************************************** + +class cMenuEpgScheduleItem : public cOsdItem +{ + public: + + enum eScheduleSortMode { ssmAllThis, ssmThisThis, ssmThisAll, ssmAllAll }; // "which event(s) on which channel(s)" + + cMenuEpgScheduleItem(cMenuDb* db, const cEvent* Event, const cChannel* Channel = 0, bool WithDate = no); + + static void SetSortMode(eScheduleSortMode SortMode) { sortMode = SortMode; } + static void IncSortMode() { sortMode = eScheduleSortMode((sortMode == ssmAllAll) ? ssmAllThis : sortMode + 1); } + static eScheduleSortMode SortMode() { return sortMode; } + + virtual int Compare(const cListObject &ListObject) const; + virtual bool Update(bool Force = false); + virtual void SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable); + + const cEvent* event; + const cChannel* channel; + bool withDate; + int timerMatch; + + private: + + cMenuDb* menuDb; + static eScheduleSortMode sortMode; +}; + +//*************************************************************************** +// Class cMenuEpgWhatsOn +//*************************************************************************** + +class cMenuEpgWhatsOn : public cOsdMenu +{ + public: + + cMenuEpgWhatsOn(const cEvent* aSearchEvent = 0); + ~cMenuEpgWhatsOn(); + + static int CurrentChannel() { return currentChannel; } + static void SetCurrentChannel(int ChannelNr) { currentChannel = ChannelNr; } + virtual eOSState ProcessKey(eKeys Key); + virtual void Display(); + + private: + + int LoadAt(); + int LoadSearch(const cUserTimes::UserTime* userTime); + int LoadSchedule(); + int LoadQuery(const cEvent* searchEvent); + + bool canSwitch; + int helpKeys; + time_t helpKeyTime; + int helpKeyTimeMode; + int timerState; + eOSState Record(); + eOSState Switch(); + bool Update(); + void SetHelpKeys(); + + cMenuDb* menuDb; + const cSchedules* schedules; +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cStateKey schedulesKey; +#else + cSchedulesLock* schedulesLock; +#endif + const cEvent* searchEvent; + int dispSchedule; + + static int currentChannel; + static const cEvent* scheduleEvent; +}; + +//*************************************************************************** +// class cEpgMenuDones +//*************************************************************************** + +class cEpgMenuDones : public cOsdMenu +{ + public: + + enum JournalFilter + { + jfAll = 0, + + jfFirst = 1, + jfRecorded = jfFirst, + jfCreated, + jfFailed, + jfDeleted, + + jfCount + }; + + cEpgMenuDones(); + virtual ~cEpgMenuDones(); + virtual eOSState ProcessKey(eKeys Key); + + protected: + + int refresh(); + void setHelp(); + + // data + + cMenuDb* menuDb; + int journalFilter; + int order; +}; + +//*************************************************************************** +// class cMenuEpgMatchRecordings +//*************************************************************************** + +class cMenuEpgMatchRecordings : public cOsdMenu +{ + public: + + cMenuEpgMatchRecordings(cMenuDb* db, const cEvent* event); + virtual ~cMenuEpgMatchRecordings() {}; + virtual eOSState ProcessKey(eKeys Key); +}; + +//*************************************************************************** + +#endif // __EPG2VDR_MENU_H diff --git a/menudone.c b/menudone.c new file mode 100644 index 0000000..be04ca8 --- /dev/null +++ b/menudone.c @@ -0,0 +1,158 @@ +/* + * menudone.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <vdr/interface.h> + +#include "epg2vdr.h" +#include "update.h" +#include "menu.h" + +#include "svdrpclient.h" + +//*************************************************************************** +// Object +//*************************************************************************** + +cEpgMenuDones::cEpgMenuDones() + : cOsdMenu("", 2, 20, 35) +{ + journalFilter = jfAll; + order = 0; + + menuDb = new cMenuDb; + + refresh(); +} + +cEpgMenuDones::~cEpgMenuDones() +{ + delete menuDb; +} + +//*************************************************************************** +// Set Help +//*************************************************************************** + +void cEpgMenuDones::setHelp() +{ + const char* btGreen = ""; + + switch (journalFilter) + { + case jfAll: btGreen = tr("Recorded"); break; + case jfRecorded: btGreen = tr("Created"); break; + case jfCreated: btGreen = tr("Failed"); break; + case jfFailed: btGreen = tr("Deleted"); break; + case jfDeleted: btGreen = tr("Recorded"); break; + + } + + SetHelp(tr("All"), btGreen, tr("Delete"), 0); +} + +//*************************************************************************** +// Refresh +//*************************************************************************** + +int cEpgMenuDones::refresh() +{ + int current = Current(); + const char* state = "%"; + + cDbStatement* select = order ? menuDb->selectDoneTimerByStateTimeOrder : menuDb->selectDoneTimerByStateTitleOrder; + + Clear(); + + switch (journalFilter) + { + case jfAll: state = "%"; SetTitle(tr("Timer journal")); break; + case jfRecorded: state = "R"; SetTitle(tr("Timer journal - Recorded")); break; + case jfCreated: state = "C"; SetTitle(tr("Timer journal - Created")); break; + case jfFailed: state = "F"; SetTitle(tr("Timer journal - Failed")); break; + case jfDeleted: state = "D"; SetTitle(tr("Timer journal - Delete")); break; + } + + // fill menu .. + + menuDb->timerDoneDb->clear(); + menuDb->timerDoneDb->setValue("STATE", state); + + for (int f = select->find(); f && menuDb->dbConnected(); f = select->fetch()) + { + char* buf; + asprintf(&buf, "%s\t%s\t%s\t%s", + menuDb->timerDoneDb->getStrValue("STATE"), + l2pTime(menuDb->timerDoneDb->getIntValue("STARTTIME"), "%d.%m.%y %H:%M").c_str(), + menuDb->timerDoneDb->getStrValue("TITLE"), + menuDb->timerDoneDb->getStrValue("SHORTTEXT")); + Add(new cEpgMenuTextItem(menuDb->timerDoneDb->getIntValue("ID"), buf)); + free(buf); + } + + select->freeResult(); + + // init .. + + SetCurrent(current >= 0 ? Get(current) : First()); + setHelp(); + Display(); + + return success; +} + +//*************************************************************************** +// ProcessKey +//*************************************************************************** + +eOSState cEpgMenuDones::ProcessKey(eKeys Key) +{ + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osUnknown) + { + switch (Key) + { + case k0: + { + order = !order; + refresh(); + break; + } + case kRed: + { + journalFilter = jfAll; + refresh(); + return osContinue; + + } + case kGreen: + { + if (++journalFilter >= jfCount) + journalFilter = jfFirst; + + refresh(); + return osContinue; + } + case kYellow: + { + cEpgMenuTextItem* item = (cEpgMenuTextItem*)Get(Current()); + + if (item && Interface->Confirm(tr("Delete timer from journal?"))) + { + menuDb->timerDoneDb->deleteWhere("id = %ld", item->getId()); + refresh(); + } + + return osContinue; + } + + default: break; + } + } + + return state; +} diff --git a/menusched.c b/menusched.c new file mode 100644 index 0000000..452c7b4 --- /dev/null +++ b/menusched.c @@ -0,0 +1,1204 @@ +/* + * menusched.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#define __STL_CONFIG_H + +#include <string> + +#include <vdr/menuitems.h> +#include <vdr/status.h> +#include <vdr/menu.h> + +#include "plgconfig.h" +#include "menu.h" +#include "ttools.h" + +class cMenuEpgScheduleItem; + +//*************************************************************************** +// Class cEpgCommandMenu +//*************************************************************************** + +class cEpgCommandMenu : public cOsdMenu +{ + public: + + cEpgCommandMenu(const char* title, cMenuDb* db, const cMenuEpgScheduleItem* aItem); + virtual ~cEpgCommandMenu() { }; + + virtual eOSState ProcessKey(eKeys key); + + protected: + + const cMenuEpgScheduleItem* item; + cMenuDb* menuDb; +}; + +cEpgCommandMenu::cEpgCommandMenu(const char* title, cMenuDb* db, const cMenuEpgScheduleItem* aItem) + : cOsdMenu(title) +{ + item = aItem; + menuDb = db; + + SetMenuCategory(mcMain); + SetHasHotkeys(yes); + Clear(); + + cOsdMenu::Add(new cOsdItem(hk(tr("Search matching Events")), osUser1)); + cOsdMenu::Add(new cOsdItem(hk(tr("Search matching Recordings")), osUser2)); + cOsdMenu::Add(new cOsdItem(hk(tr("Searchtimers")), osUser3)); + + SetHelp(0, 0, 0, 0); + + Display(); +} + +eOSState cEpgCommandMenu::ProcessKey(eKeys key) +{ + eOSState state = cOsdMenu::ProcessKey(key); + + switch (state) + { + case osUser1: + { + if (item) + return AddSubMenu(new cMenuEpgWhatsOn(item->event)); + + break; + } + + case osUser2: + { + if (item) + return AddSubMenu(new cMenuEpgMatchRecordings(menuDb, item->event)); + + break; + } + + case osUser3: return AddSubMenu(new cMenuEpgSearchTimers()); + + default: ; + } + + return state; +} + +//*************************************************************************** +// Class cMenuEpgMatchRecordings +//*************************************************************************** + +cMenuEpgMatchRecordings::cMenuEpgMatchRecordings(cMenuDb* db, const cEvent* event) + : cOsdMenu(tr("Matching recordings"), 30, 30, 30, 40) +{ + cMenuDb* menuDb = db; + + Add(new cOsdItem(tr("Direct Matches:"))); + cList<cOsdItem>::Last()->SetSelectable(no); + + tell(0, "Search recordings for '%s' '%s'", event->Title(), event->ShortText()); + + menuDb->recordingListDb->clear(); + menuDb->recordingListDb->setValue("TITLE", event->Title()); + menuDb->recordingListDb->setValue("SHORTTEXT", event->ShortText()); + + for (int f = menuDb->selectRecordingForEvent->find(); f; f = menuDb->selectRecordingForEvent->fetch()) + { + const char* vdr = 0; + +#ifdef WITH_PIN + if (!cOsd::pinValid && menuDb->recordingListDb->getIntValue("FSK")) + continue; +#endif + + if (!menuDb->recordingListDb->getValue("OWNER")->isEmpty()) + { + menuDb->vdrDb->clear(); + menuDb->vdrDb->setValue("UUID", menuDb->recordingListDb->getStrValue("OWNER")); + + if (menuDb->vdrDb->find()) + vdr = menuDb->vdrDb->getStrValue("NAME"); + } + + Add(new cEpgMenuTextItem(menuDb->recordingListDb->getStrValue("MD5PATH"), + cString::sprintf("%s%s%s\t%s\t%s/%s", vdr ? vdr : "", vdr ? "\t" : "", + menuDb->recordingListDb->getStrValue("TITLE"), + menuDb->recordingListDb->getStrValue("SHORTTEXT"), + menuDb->recordingListDb->getStrValue("FOLDER"), + menuDb->recordingListDb->getStrValue("NAME")))); + } + + menuDb->selectRecordingForEvent->freeResult(); + + Add(new cOsdItem(tr("All Matches:"))); + cList<cOsdItem>::Last()->SetSelectable(no); + + menuDb->recordingListDb->clear(); + menuDb->recordingListDb->setValue("TITLE", event->Title()); + + for (int f = menuDb->selectRecordingForEventByLv->find(); f; f = menuDb->selectRecordingForEventByLv->fetch()) + { + const char* vdr = 0; + +#ifdef WITH_PIN + if (!cOsd::pinValid && menuDb->recordingListDb->getIntValue("FSK")) + continue; +#endif + + if (!menuDb->recordingListDb->getValue("OWNER")->isEmpty()) + { + menuDb->vdrDb->clear(); + menuDb->vdrDb->setValue("UUID", menuDb->recordingListDb->getStrValue("OWNER")); + + if (menuDb->vdrDb->find()) + vdr = menuDb->vdrDb->getStrValue("NAME"); + } + + Add(new cEpgMenuTextItem(menuDb->recordingListDb->getStrValue("MD5PATH"), + cString::sprintf("%s%s%s\t%s\t%s/%s", vdr ? vdr : "", vdr ? "\t" : "", + menuDb->recordingListDb->getStrValue("TITLE"), + menuDb->recordingListDb->getStrValue("SHORTTEXT"), + menuDb->recordingListDb->getStrValue("FOLDER"), + menuDb->recordingListDb->getStrValue("NAME")))); + } + + menuDb->selectRecordingForEventByLv->freeResult(); + + Display(); +} + +eOSState cMenuEpgMatchRecordings::ProcessKey(eKeys Key) +{ + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osUnknown) + { + return osContinue; + } + + return state; +} + +//*************************************************************************** +//*************************************************************************** +// Class cMenuEpgScheduleItem +//*************************************************************************** + +cMenuEpgScheduleItem::eScheduleSortMode cMenuEpgScheduleItem::sortMode = ssmAllThis; + +//*************************************************************************** +// Object +//*************************************************************************** + +cMenuEpgScheduleItem::cMenuEpgScheduleItem(cMenuDb* db, const cEvent* Event, const cChannel* Channel, bool WithDate) +{ + menuDb = db; + + event = Event; + channel = Channel; + withDate = WithDate; + timerMatch = tmNone; + Update(yes); +} + +//*************************************************************************** +// Compare +//*************************************************************************** + +int cMenuEpgScheduleItem::Compare(const cListObject &ListObject) const +{ + cMenuEpgScheduleItem *p = (cMenuEpgScheduleItem *)&ListObject; + int r = -1; + + if (sortMode != ssmAllThis) + r = strcoll(event->Title(), p->event->Title()); + + if (sortMode == ssmAllThis || r == 0) + r = event->StartTime() - p->event->StartTime(); + + return r; +} + +//*************************************************************************** +// Update +//*************************************************************************** + +bool cMenuEpgScheduleItem::Update(bool Force) +{ + static const char* TimerMatchChars = " tT"; + static const char* TimerMatchCharsRemote = " sS"; + + bool result = false; + int oldTimerMatch = timerMatch; + int remote = no; + + if (event) + menuDb->lookupTimer(event, timerMatch, remote); // -> loads timerDb, vdrDb and set ther timerMatch + + if (Force || timerMatch != oldTimerMatch) + { + if (timerMatch && event) + tell(0, "Timer match for '%s'", event->Title()); + + cString buffer; + char t = !remote ? TimerMatchChars[timerMatch] : TimerMatchCharsRemote[timerMatch]; + char v = !event ? ' ' : event->Vps() && (event->Vps() - event->StartTime()) ? 'V' : ' '; + char r = !event ? ' ' : event->SeenWithin(30) && event->IsRunning() ? '*' : ' '; + const char* csn = channel ? channel->ShortName(true) : 0; + cString eds = !event ? "" : event->GetDateString(); + + if (channel && withDate) + buffer = cString::sprintf("%d\t%.*s\t%.*s\t%s\t%c%c%c\t%s", channel->Number(), + Utf8SymChars(csn, 999), csn, Utf8SymChars(eds, 6), + *eds, + !event ? "" : *event->GetTimeString(), + t, v, r, + !event ? "" : event->Title()); + else if (channel) + buffer = cString::sprintf("%d\t%.*s\t%s\t%c%c%c\t%s", channel->Number(), + Utf8SymChars(csn, 999), csn, + !event ? "" : *event->GetTimeString(), + t, v, r, + !event ? "" : event->Title()); + else + buffer = cString::sprintf("%.*s\t%s\t%c%c%c\t%s", Utf8SymChars(eds, 6), *eds, + !event ? "" : *event->GetTimeString(), + t, v, r, !event ? "" : + !event ? "" : event->Title()); + + SetText(buffer); + result = true; + } + + return result; +} + +//*************************************************************************** +// SetMenuItem +//*************************************************************************** + +void cMenuEpgScheduleItem::SetMenuItem(cSkinDisplayMenu* DisplayMenu, + int Index, bool Current, bool Selectable) +{ + if (!DisplayMenu->SetItemEvent(event, Index, Current, Selectable, channel, + withDate, (eTimerMatch)timerMatch)) + { + DisplayMenu->SetItem(Text(), Index, Current, Selectable); + } +} + +//*************************************************************************** +//*************************************************************************** +// cMenuEpgScheduleSepItem +//*************************************************************************** + +class cMenuEpgScheduleSepItem : public cOsdItem +{ + public: + + cMenuEpgScheduleSepItem(const cChannel* Channel, const cEvent* Event); + ~cMenuEpgScheduleSepItem(); + + virtual bool Update(bool Force = false); + virtual void SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable); + + private: + + const cChannel* channel; + const cEvent* event; + cEvent* tmpEvent; +}; + +cMenuEpgScheduleSepItem::cMenuEpgScheduleSepItem(const cChannel* Channel, const cEvent* Event) +{ + channel = Channel; + event = Event; + tmpEvent = 0; + + SetSelectable(false); + Update(true); +} + +cMenuEpgScheduleSepItem::~cMenuEpgScheduleSepItem() +{ + delete tmpEvent; +} + +bool cMenuEpgScheduleSepItem::Update(bool Force) +{ + if (channel) + { + SetText(cString::sprintf("-----\t %s -----", channel ? channel->Name() : *event->GetDateString())); + } + else if (event) + { + tmpEvent = new cEvent(0); + tmpEvent->SetTitle(cString::sprintf("-----\t %s -----", *event->GetDateString())); + SetText(tmpEvent->Title()); + } + + return true; +} + +void cMenuEpgScheduleSepItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable) +{ + if (!DisplayMenu->SetItemEvent(tmpEvent, Index, Current, Selectable, channel, no, tmNone)) + DisplayMenu->SetItem(Text(), Index, Current, Selectable); +} + +//*************************************************************************** +// cMenuEpgEvent +//*************************************************************************** + +cMenuEpgEvent::cMenuEpgEvent(cMenuDb* db, const cEvent* Event, const cSchedules* Schedules, + int timerMatch, bool DispSchedule, bool CanSwitch) + : cOsdMenu(tr("Event")) +{ + menuDb = db; + dispSchedule = DispSchedule; + schedules = Schedules; + canSwitch = CanSwitch; + SetMenuCategory(mcEvent); + setEvent(Event, timerMatch); +} + +//*************************************************************************** +// Set Event +//*************************************************************************** + +int cMenuEpgEvent::setEvent(const cEvent* Event, int timerMatch) +{ + if (Event) + { + event = Event; + +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; + const cChannel* channel = channels->GetByChannelID(event->ChannelID(), true); +#else + cChannels* channels = &Channels; + cChannel* channel = channels->GetByChannelID(event->ChannelID(), true); +#endif + + if (channel) + { + const cChannel* prevChannel = 0; + const cChannel* nextChannel = 0; + + prevTime = ""; + nextTime = ""; + + SetTitle(channel->Name()); + + if (menuDb->userTimes->current()->getMode() != cUserTimes::mSearch) + { + const cEvent* e; + + if (e = getNextPrevEvent(event, -1)) + { + if (dispSchedule) + prevTime = l2pTime(e->StartTime(), "%H:%M"); + else + prevChannel = channels->GetByChannelID(e->ChannelID(), true); + } + + if (e = getNextPrevEvent(event, +1)) + { + if (dispSchedule) + nextTime = l2pTime(e->StartTime(), "%H:%M"); + else + nextChannel = channels->GetByChannelID(e->ChannelID(), true); + } + } + + SetHelp(timerMatch == tmFull ? tr("Button$Timer") : tr("Button$Record"), + dispSchedule ? prevTime.c_str() : (prevChannel ? prevChannel->Name() : 0), + dispSchedule ? nextTime.c_str() : (nextChannel ? nextChannel->Name() : 0), + canSwitch ? tr("Button$Switch") : 0); + } + + Display(); + } + + return success; +} + +void cMenuEpgEvent::Display() +{ + cOsdMenu::Display(); + DisplayMenu()->SetEvent(event); + +#ifdef WITH_GTFT + cStatus::MsgOsdSetEvent(event); +#endif + + if (event->Description()) + cStatus::MsgOsdTextItem(event->Description()); +} + +//*************************************************************************** +// Get Next Prev Event (related to current) +//*************************************************************************** + +const cEvent* cMenuEpgEvent::getNextPrevEvent(const cEvent* event, int step) +{ +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; + const cChannel* channel = channels->GetByChannelID(event->ChannelID(), true); +#else + cChannels* channels = &Channels; + cChannel* channel = channels->GetByChannelID(event->ChannelID(), true); +#endif + + const cEvent* e = 0; + + if (dispSchedule) + { + const cSchedule* schedule = schedules->GetSchedule(channel); + + if (schedule && step == -1) + e = schedule->Events()->Prev(event); + else if (schedule && step == +1) + e = schedule->Events()->Next(event); + } + else + { + while (!e) + { + int cn = channel->Number() + step; + + if (cn < 1) + cn = channels->MaxNumber(); + else if (cn > channels->MaxNumber()) + cn = 1; + + if (!(channel = channels->GetByNumber(cn))) + return 0; + + const cSchedule* schedule = schedules->GetSchedule(channel); + + if (schedule) + { + cUserTimes::UserTime* userTime = menuDb->userTimes->current(); + + switch (userTime->getMode()) + { + case cUserTimes::mNow: e = schedule->GetPresentEvent(); break; + case cUserTimes::mNext: e = schedule->GetFollowingEvent(); break; + case cUserTimes::mTime: e = schedule->GetEventAround(userTime->getTime()); break; + } + } + } + } + + return e; +} + +//*************************************************************************** +// Process Key +//*************************************************************************** + +eOSState cMenuEpgEvent::ProcessKey(eKeys Key) +{ + switch (int(Key)) + { + case kUp|k_Repeat: + case kUp: + case kDown|k_Repeat: + case kDown: + case kLeft|k_Repeat: + case kLeft: + case kRight|k_Repeat: + case kRight: + { + DisplayMenu()->Scroll(NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft, NORMALKEY(Key) == kLeft || NORMALKEY(Key) == kRight); + cStatus::MsgOsdTextItem(NULL, NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft); + return osContinue; + } + + case kInfo: return osBack; + default: break; + } + + if (Key == kGreen || Key == kYellow) + { + if (menuDb->userTimes->current()->getMode() == cUserTimes::mSearch) + return osContinue; + + const cEvent* e = getNextPrevEvent(event, Key == kGreen ? -1 : +1); + int remote = no; + int timerMatch = tmNone; + + // get remote and timerMatch info + + menuDb->lookupTimer(e, timerMatch, remote); + setEvent(e, timerMatch); + + return osContinue; + } + + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osUnknown) + { + switch (Key) + { + case kOk: return osBack; + default: break; + } + } + + return state; +} + +//*************************************************************************** +//*************************************************************************** +// cMenuEpgWhatsOn +//*************************************************************************** + +int cMenuEpgWhatsOn::currentChannel = 0; +const cEvent* cMenuEpgWhatsOn::scheduleEvent = 0; + +//*************************************************************************** +// Object +//*************************************************************************** + +cMenuEpgWhatsOn::cMenuEpgWhatsOn(const cEvent* aSearchEvent) + : cOsdMenu("", CHNUMWIDTH, CHNAMWIDTH, 6, 4) +{ +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) +#else + schedulesLock = 0; +#endif + schedules = 0; + dispSchedule = no; + canSwitch = no; + helpKeys = 0; + helpKeyTime = 0; + helpKeyTimeMode = na; +// timerState = 0; + searchEvent = aSearchEvent; + menuDb = new cMenuDb; + +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; + const cChannel* channel = channels->GetByNumber(cDevice::CurrentChannel()); +#else + cChannels* channels = &Channels; + const cChannel* channel = channels->GetByNumber(cDevice::CurrentChannel()); +#endif + + if (channel) + { + currentChannel = channel->Number(); +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + schedules = cSchedules::GetSchedulesRead(schedulesKey); +#else + schedulesLock = new cSchedulesLock(false); + schedules = (cSchedules*)cSchedules::Schedules(*schedulesLock); +#endif + } + +// Timers.Modified(timerState); + + if (menuDb->dbConnected()) + { + menuDb->userTimes->first(); // 'whats on now' should be the first + + if (searchEvent) + LoadQuery(searchEvent); + else if (menuDb->startWithSched) + LoadSchedule(); + else + LoadAt(); + } +} + +cMenuEpgWhatsOn::~cMenuEpgWhatsOn() +{ + delete menuDb; + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + if (schedules) + schedulesKey.Remove(); +#else + tell(3, "LOCK free (cMenuEpgWhatsOn)"); + delete schedulesLock; +#endif +} + +//*************************************************************************** +// Load At +//*************************************************************************** + +int cMenuEpgWhatsOn::LoadAt() +{ + dispSchedule = no; + cUserTimes::UserTime* userTime = menuDb->userTimes->current(); + cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current()); + + if (item) + { + scheduleEvent = item->event; + + if (item->channel) + currentChannel = item->channel->Number(); + } + + if (userTime->getMode() == cUserTimes::mSearch) + return LoadSearch(userTime); + + Clear(); +// Timers.Modified(timerState); + + SetMenuCategory(userTime->getMode() == cUserTimes::mNow ? mcScheduleNow : mcScheduleNext); + + if (isEmpty(userTime->getHHMMStr())) + SetTitle(cString::sprintf(tr("Overview - %s"), userTime->getTitle())); + else + SetTitle(cString::sprintf(tr("Overview - %s (%s)"), userTime->getTitle(), userTime->getHHMMStr())); + + tell(2, "DEBUG: Loading events for '%s' %s", userTime->getTitle(), !isEmpty(userTime->getHHMMStr()) ? userTime->getHHMMStr() : ""); + +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; +#else + const cChannels* channels = &Channels; +#endif + + // events + + for (const cChannel* Channel = channels->First(); Channel; Channel = channels->Next(Channel)) + { + if (!Channel->GroupSep()) + { + const cSchedule* Schedule = schedules->GetSchedule(Channel); + const cEvent* Event = 0; + + if (Schedule) + { + switch (userTime->getMode()) + { + case cUserTimes::mNow: Event = Schedule->GetPresentEvent(); break; + case cUserTimes::mNext: Event = Schedule->GetFollowingEvent(); break; + case cUserTimes::mTime: Event = Schedule->GetEventAround(userTime->getTime()); break; + } + } + + // #TODO + // hier ein cEpgEvent für das Event aus der Row erzeugen und dieses anstelle von cEvent für das Menü verwenden + + if (!Event && !Epg2VdrConfig.showEmptyChannels) + continue; + + Add(new cMenuEpgScheduleItem(menuDb, Event, Channel), Channel->Number() == currentChannel); + } + else + { + if (strlen(Channel->Name())) + Add(new cMenuEpgScheduleSepItem(Channel, 0)); + } + } + + Display(); + SetHelpKeys(); + + return done; +} + +//*************************************************************************** +// Load Search +//*************************************************************************** + +int cMenuEpgWhatsOn::LoadSearch(const cUserTimes::UserTime* userTime) +{ + cDbStatement* select = 0; + + Clear(); + SetMenuCategory(mcSchedule); + SetTitle(cString::sprintf(tr("Overview - %s"), userTime->getTitle())); + tell(2, "DEBUG: Loading events for search '%s'", userTime->getSearch()); + + menuDb->searchtimerDb->clear(); + menuDb->searchtimerDb->setValue("NAME", userTime->getSearch()); + + if (!menuDb->selectSearchTimerByName->find()) + return done; + + if (!(select = menuDb->search->prepareSearchStatement(menuDb->searchtimerDb->getRow(), menuDb->useeventsDb))) + return fail; + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cChannelsLock channelsLock(false); + const cChannels* channels = channelsLock.Channels(); +#else + cChannels* channels = &Channels; +#endif + + menuDb->useeventsDb->clear(); + + for (int res = select->find(); res; res = select->fetch()) + { + menuDb->useeventsDb->find(); // get all fields .. + + if (!menuDb->search->matchCriterias(menuDb->searchtimerDb->getRow(), menuDb->useeventsDb->getRow())) + continue; + + // + + const char* strChannelId = menuDb->useeventsDb->getStrValue("CHANNELID"); + const cChannel* channel = channels->GetByChannelID(tChannelID::FromString(strChannelId)); + const cSchedule* schedule = schedules->GetSchedule(channel); + const cEvent* event = !schedule ? 0 : schedule->GetEvent(menuDb->useeventsDb->getIntValue("USEID")); + + if (event) + Add(new cMenuEpgScheduleItem(menuDb, event, channel, yes)); + } + + select->freeResult(); + menuDb->selectSearchTimerByName->freeResult(); + + Display(); + SetHelpKeys(); + + return Count(); +} + +//*************************************************************************** +// Load Schedule +//*************************************************************************** + +int cMenuEpgWhatsOn::LoadSchedule() +{ + Clear(); + SetCols(7, 6, 4); + dispSchedule = yes; + +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; + const cChannel* channel = channels->GetByNumber(currentChannel); +#else + cChannels* channels = &Channels; + const cChannel* channel = channels->GetByNumber(currentChannel); +#endif + + cMenuEpgScheduleItem::SetSortMode(cMenuEpgScheduleItem::ssmAllThis); + SetMenuCategory(mcSchedule); + SetTitle(cString::sprintf(tr("Schedule - %s"), channel->Name())); + + if (schedules && channel) + { + const cSchedule* Schedule = schedules->GetSchedule(channel); + + if (Schedule) + { + int lastDay = 0; + const cEvent* PresentEvent = scheduleEvent ? scheduleEvent : Schedule->GetPresentEvent(); + time_t now = time(0) - Setup.EPGLinger * 60; + + for (const cEvent* ev = Schedule->Events()->First(); ev; ev = Schedule->Events()->Next(ev)) + { + time_t stime = ev->StartTime(); + struct tm tmSTime; + localtime_r(&stime, &tmSTime); + + if (lastDay != 0 && tmSTime.tm_mday != lastDay) + Add(new cMenuEpgScheduleSepItem(0, ev)); + + if (ev->EndTime() > now || ev == PresentEvent) + Add(new cMenuEpgScheduleItem(menuDb, ev, channel), ev == PresentEvent); + + lastDay = tmSTime.tm_mday; + } + } + } + + Display(); + SetHelpKeys(); + + return done; +} + +//*************************************************************************** +// Load Query +//*************************************************************************** + +int cMenuEpgWhatsOn::LoadQuery(const cEvent* searchEvent) +{ + Clear(); + SetMenuCategory(mcSchedule); + cMenuEpgScheduleItem::SetSortMode(cMenuEpgScheduleItem::ssmAllThis); + +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; +#else + const cChannels* channels = &Channels; +#endif + + tell(1, "Lookup events with title '%s'", searchEvent->Title()); + + for (const cChannel* channel = channels->First(); channel; channel = channels->Next(channel)) + { + const cSchedule* schedule = schedules->GetSchedule(channel); + + if (schedule) + { + for (const cEvent* event = schedule->Events()->First(); event; event = schedule->Events()->Next(event)) + { + if (strcasecmp(event->Title(), searchEvent->Title()) == 0 && + event->StartTime() > time(0)) + Add(new cMenuEpgScheduleItem(menuDb, event, channel, yes)); + } + } + } + + Sort(); + SetTitle(cString::sprintf("%d %s - %s", Count(), tr("Search results"), searchEvent->Title())); + + Display(); + SetHelpKeys(); + + return Count(); +} + +//*************************************************************************** +// Display +//*************************************************************************** + +void cMenuEpgWhatsOn::Display() +{ + cOsdMenu::Display(); + +#ifdef WITH_GTFT + if (Count() > 0) + { + int ni = 0; + + for (cOsdItem *item = First(); item; item = Next(item)) + cStatus::MsgOsdEventItem(((cMenuEpgScheduleItem*)item)->event, item->Text(), ni++, Count()); + } +#endif +} + +//*************************************************************************** +// Update +//*************************************************************************** + +bool cMenuEpgWhatsOn::Update() +{ + bool result = false; + + for (cOsdItem* item = First(); item; item = Next(item)) + { + if (((cMenuEpgScheduleItem*)item)->Update()) + result = true; + } + + return result; +} + +//*************************************************************************** +// SetHelpKeys +//*************************************************************************** + +void cMenuEpgWhatsOn::SetHelpKeys() +{ + cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem *)Get(Current()); + canSwitch = no; + int NewHelpKeys = 0; + + if (item) + { + if (item->timerMatch == tmFull) + NewHelpKeys |= 0x02; // "Timer" + else + NewHelpKeys |= 0x01; // "Record" + + if (!dispSchedule) + NewHelpKeys |= 0x04; // "Schedule" + + if (item->channel) + { + if (item->channel->Number() != cDevice::CurrentChannel()) + { + NewHelpKeys |= 0x10; // "Switch" + canSwitch = yes; + } + } + } + + cUserTimes::UserTime* userTimeGreen = !dispSchedule ? menuDb->userTimes->getNext() : menuDb->userTimes->current(); + + if (NewHelpKeys != helpKeys || helpKeyTime != userTimeGreen->getTime() || helpKeyTimeMode != userTimeGreen->getMode()) + { + const char* Red[] = { 0, tr("Button$Record"), tr("Button$Timer") }; + + if (searchEvent) + { + helpKeys = 0; + helpKeyTime = 0; + helpKeyTimeMode = na; + + SetHelp(Red[NewHelpKeys & 0x03], 0, 0, 0); + } + else + { + SetHelp(Red[NewHelpKeys & 0x03], // red + userTimeGreen->getHelpKey(), // green + !dispSchedule + ? tr("Button$Schedule") // yellow + : menuDb->userTimes->current() == menuDb->userTimes->getFirst() + ? tr(menuDb->userTimes->getNext()->getHelpKey()) // yellow + : tr(menuDb->userTimes->getFirst()->getHelpKey()), // yellow + Epg2VdrConfig.xchgOkBlue ? tr("Button$Info") : canSwitch ? tr("Button$Switch") : 0); // blue + + helpKeyTime = userTimeGreen->getTime(); + helpKeyTimeMode = userTimeGreen->getMode(); + helpKeys = NewHelpKeys; + } + } +} + +//*************************************************************************** +// Switch +//*************************************************************************** + +eOSState cMenuEpgWhatsOn::Switch() +{ + cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current()); + + if (cDevice::PrimaryDevice()->SwitchChannel(item->channel, true)) + return osEnd; + + Skins.Message(mtError, tr("Can't switch channel!")); + + return osContinue; +} + +//*************************************************************************** +// Record +//*************************************************************************** + +eOSState cMenuEpgWhatsOn::Record() +{ + int timerMatch = tmNone; + cEpgTimer* timer = 0; + long timerid = 0; + int remote = no; + cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current()); + + if (!item) + return osContinue; + + if (timerid = menuDb->lookupTimer(item->event, timerMatch, remote)) // -> loads timerDb and vdrDb + { + menuDb->timerDb->clear(); + menuDb->timerDb->setValue("ID", timerid); + + if (!menuDb->selectTimerById->find()) + { + tell(0, "Fatal, Can't find timer (%ld)", timerid); + return osContinue; + } + + timer = newTimerObjectFromRow(menuDb->timerDb->getRow(), menuDb->vdrDb->getRow()); + } + + if (timer) + return AddSubMenu(new cMenuEpgEditTimer(menuDb, timer)); + + // neuen Timer anlegen + + cDbRow* timerRow = newTimerRowFromEvent(item->event); + char timerDefaultVDRuuid[150+TB] = ""; + + menuDb->getParameter(menuDb->user.c_str(), "timerDefaultVDRuuid", timerDefaultVDRuuid); + + // Menü bei 'aktuellem' Event Timer Dialog öffen -> #TODO + + if (timer && timer->Event() && timer->Event()->StartTime() < time(0) + NEWTIMERLIMIT) + { + // timer = newTimerObjectFromRow(timerRow, xxxxx); + // return AddSubMenu(new cMenuEpgEditTimer(menuDb, timer)); + } + + // ansonsten direkt anlegen + + menuDb->createTimer(timerRow, isEmpty(timerDefaultVDRuuid) || Epg2VdrConfig.createTimerLocal ? Epg2VdrConfig.uuid : timerDefaultVDRuuid); + delete timerRow; + + if (HasSubMenu()) + CloseSubMenu(); + + sleep(1); + + if (Update()) + Display(); + + SetHelpKeys(); + + return osContinue; +} + +//*************************************************************************** +// ProcessKey +//*************************************************************************** + +eOSState cMenuEpgWhatsOn::ProcessKey(eKeys Key) +{ + bool HadSubMenu = HasSubMenu(); + eOSState state = cOsdMenu::ProcessKey(Key); + + if (!menuDb->dbConnected()) + return state; + + if (state == osUnknown) + { + if (searchEvent && Key != kOk && Key != kRed && Key != kRecord) + return osContinue; + + if (Epg2VdrConfig.xchgOkBlue && !HasSubMenu()) + { + if (Key == kBlue) Key = kOk; + else if (Key == kOk) Key = kBlue; + } + + switch (Key) + { + case k0: // command menu + { + cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current()); + + return AddSubMenu(new cEpgCommandMenu("Commands", menuDb, item)); + } + + case k1: // search repeat of event + { + if (Get(Current())) + { + cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current()); + + if (item) + return AddSubMenu(new cMenuEpgWhatsOn(item->event)); + } + + break; + } + + case k2: // search matching recordings + { + if (Get(Current())) + { + cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current()); + + if (item) + return AddSubMenu(new cMenuEpgMatchRecordings(menuDb, item->event)); + } + + break; + } + + case k3: // search timer dialog + { + return AddSubMenu(new cMenuEpgSearchTimers()); + } + + case k4: // Umschalt Timer erstellen + { + break; + } + + case kRecord: + case kRed: + return Record(); + + case kYellow: + { + if (!dispSchedule && Get(Current())) + { + cMenuEpgScheduleItem* mi = (cMenuEpgScheduleItem*)Get(Current()); + + if (mi) + { + scheduleEvent = mi->event; + currentChannel = mi->channel->Number(); + LoadSchedule(); + } + } + else + { + if (menuDb->userTimes->current() == menuDb->userTimes->getFirst()) + menuDb->userTimes->next(); + else + menuDb->userTimes->first(); + + LoadAt(); + } + + return osContinue; + } + case kGreen: + { + if (!dispSchedule) + menuDb->userTimes->next(); + + LoadAt(); + + return osContinue; + } + + case kBlue: + { + if (canSwitch) + return Switch(); + + break; + } + + case kInfo: + case kOk: + { + if (Get(Current())) + { + cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current()); + + if (item) + { + const cEvent* event = item->event; + + if (Count() && event) + return AddSubMenu(new cMenuEpgEvent(menuDb, event, schedules, + item->timerMatch, dispSchedule, canSwitch)); + } + } + + break; + } + + default: + break; + } + } + + if (!HasSubMenu()) + { + if (HadSubMenu && Update()) + Display(); + + if (Key != kNone) + SetHelpKeys(); + } + + return state; +} + +//*************************************************************************** diff --git a/menusearchtimer.c b/menusearchtimer.c new file mode 100644 index 0000000..dd59c1b --- /dev/null +++ b/menusearchtimer.c @@ -0,0 +1,381 @@ +/* + * menusearchtimer.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <vdr/interface.h> + +#include "lib/searchtimer.h" +#include "menu.h" + +//*************************************************************************** +// Class cEpgMenuSearchTimerEdit +//*************************************************************************** + +class cEpgMenuSearchTimerEdit : public cOsdMenu +{ + public: + + cEpgMenuSearchTimerEdit(cMenuDb* db, long id, bool New = no); + virtual ~cEpgMenuSearchTimerEdit(); + + virtual eOSState ProcessKey(eKeys Key); + + protected: + + cMenuDb* menuDb; +}; + +cEpgMenuSearchTimerEdit::cEpgMenuSearchTimerEdit(cMenuDb* db, long id, bool New) + : cOsdMenu(tr("Edit Search Timer"), 12) +{ + menuDb = db; + menuDb->searchtimerDb->clear(); + menuDb->searchtimerDb->setValue("ID", id); + + if (!menuDb->searchtimerDb->find()) + return; + + cDbTableDef* def = menuDb->searchtimerDb->getTableDef(); + + for (int i = 0; i < def->fieldCount(); i++) + { + cDbValue* value = menuDb->searchtimerDb->getValue(def->getField(i)->getName()); + const char* name = def->getField(i)->getDescription(); + + if (!value || def->getField(i)->hasType(cDBS::ftMeta)) + continue; + + if (isEmpty(name)) + name = def->getField(i)->getName(); + + if (def->getField(i)->hasFormat(cDBS::ffAscii)) + Add(new cMenuEditStrItem(tr(name), (char*)value->getStrValueRef(), def->getField(i)->getSize())); + + else if (def->getField(i)->hasFormat(cDBS::ffInt)) + Add(new cMenuEditIntItem(tr(name), (int*)value->getIntValueRef())); + } + + Display(); +} + +cEpgMenuSearchTimerEdit::~cEpgMenuSearchTimerEdit() +{ +} + +eOSState cEpgMenuSearchTimerEdit::ProcessKey(eKeys Key) +{ + eOSState state = cOsdMenu::ProcessKey(Key); + + return state; +} + +//*************************************************************************** +// Class cEpgMenuSearchTimerItem +//*************************************************************************** + +class cEpgMenuSearchTimerItem : public cOsdItem +{ + public: + + cEpgMenuSearchTimerItem(long aId, int aActive, const char* text) + { + id = aId; + active = aActive; + SetText(text); + } + + ~cEpgMenuSearchTimerItem() { } + + long getId() { return id; } + int isActive() { return active; } + + protected: + + long id; + int active; +}; + +//*************************************************************************** +//*************************************************************************** +// Class cEpgMenuSearchResult +//*************************************************************************** + +class cEpgMenuSearchResult : public cOsdMenu +{ + public: + + cEpgMenuSearchResult(cMenuDb* db, long id); + virtual ~cEpgMenuSearchResult(); + virtual eOSState ProcessKey(eKeys Key); + + protected: + + int refresh(long id); + + cMenuDb* menuDb; +}; + +cEpgMenuSearchResult::cEpgMenuSearchResult(cMenuDb* db, long id) + : cOsdMenu(tr("Edit Search Timer"), 17, CHNAMWIDTH, 3, 30) +{ + menuDb = db; + refresh(id); +} + +cEpgMenuSearchResult::~cEpgMenuSearchResult() +{ +} + +//*************************************************************************** +// Refresh +//*************************************************************************** + +int cEpgMenuSearchResult::refresh(long id) +{ + cDbStatement* select = 0; + + menuDb->searchtimerDb->setValue("ID", id); + + if (!menuDb->searchtimerDb->find()) + return done; + + if (!(select = menuDb->search->prepareSearchStatement(menuDb->searchtimerDb->getRow(), menuDb->useeventsDb))) + return fail; + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cChannelsLock channelsLock(false); + const cChannels* channels = channelsLock.Channels(); +#else + cChannels* channels = &Channels; +#endif + + menuDb->useeventsDb->clear(); + + for (int res = select->find(); res; res = select->fetch()) + { + int cnt = 0; + cDbStatement* selectDones = 0; + + menuDb->useeventsDb->find(); // get all fields .. + + if (!menuDb->search->matchCriterias(menuDb->searchtimerDb->getRow(), menuDb->useeventsDb->getRow())) + continue; + + // dones ... + + if (menuDb->search->prepareDoneSelect(menuDb->useeventsDb->getRow(), + menuDb->searchtimerDb->getIntValue("REPEATFIELDS"), + selectDones) == success + && selectDones) + { + // first only count - #TODO show them in a sub-menu oder let delete by kYellow? + + for (int f = selectDones->find(); f; f = selectDones->fetch()) + cnt++; + + selectDones->freeResult(); + } + + // + + const char* strChannelId = menuDb->useeventsDb->getStrValue("CHANNELID"); + const cChannel* channel = channels->GetByChannelID(tChannelID::FromString(strChannelId)); + + Add(new cEpgMenuTextItem(menuDb->useeventsDb->getIntValue("USEID"), + cString::sprintf("%s\t%s\t%s\t%s\t%s", + l2pTime(menuDb->useeventsDb->getIntValue("STARTTIME")).c_str(), + channel->Name(), + cnt ? num2Str(cnt).c_str() : "", + menuDb->useeventsDb->getStrValue("TITLE"), + menuDb->useeventsDb->getStrValue("SHORTTEXT")))); + } + + select->freeResult(); + menuDb->searchtimerDb->reset(); + + SetHelp(0, 0, "Delete dones", 0); + Display(); + + return success; +} + +//*************************************************************************** +// Process Key +//*************************************************************************** + +eOSState cEpgMenuSearchResult::ProcessKey(eKeys Key) +{ + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osUnknown) + { + switch (Key) + { + case kYellow: + { + cDbStatement* selectDones = 0; + cEpgMenuTextItem* item = (cEpgMenuTextItem*)Get(Current()); + + if (item && Interface->Confirm(tr("Remove all done entries of this event?"))) + { + menuDb->useeventsDb->clear(); + menuDb->useeventsDb->setValue("USEID", item->getId()); + + if (menuDb->useeventsDb->find()) + { + if (menuDb->search->prepareDoneSelect(menuDb->useeventsDb->getRow(), + menuDb->searchtimerDb->getIntValue("REPEATFIELDS"), + selectDones) == success + && selectDones) + { + for (int f = selectDones->find(); f; f = selectDones->fetch()) + selectDones->getTable()->deleteWhere("id = %ld", selectDones->getTable()->getIntValue("ID")); + + selectDones->freeResult(); + } + } + } + } + + default: break; + } + } + + return state; +} + +//*************************************************************************** +// Class cMenuEpgSearchTimers +//*************************************************************************** +//*************************************************************************** +// Object +//*************************************************************************** + +cMenuEpgSearchTimers::cMenuEpgSearchTimers() + : cOsdMenu(tr("Search Timers"), 2, 6) +{ + menuDb = new cMenuDb; + refresh(); +} + +cMenuEpgSearchTimers::~cMenuEpgSearchTimers() +{ + delete menuDb; +} + +int cMenuEpgSearchTimers::refresh() +{ + int current = Current(); + + menuDb->searchtimerDb->clear(); + + Clear(); + + for (int f = menuDb->selectSearchTimers->find(); f && menuDb->dbConnected(); f = menuDb->selectSearchTimers->fetch()) + { + char* buf; + asprintf(&buf, "%c\t%ld\t%s", + menuDb->searchtimerDb->getIntValue("ACTIVE") ? '>' : ' ', + menuDb->searchtimerDb->getIntValue("HITS"), + menuDb->searchtimerDb->getStrValue("EXPRESSION")); + Add(new cEpgMenuSearchTimerItem(menuDb->searchtimerDb->getIntValue("ID"), + menuDb->searchtimerDb->getIntValue("ACTIVE"), buf)); + free(buf); + } + + menuDb->selectSearchTimers->freeResult(); + + Sort(); + SetCurrent(current >= 0 ? Get(current) : First()); + + setHelpKeys(); + Display(); + + return done; +} + +void cMenuEpgSearchTimers::setHelpKeys() +{ + cEpgMenuSearchTimerItem* item = (cEpgMenuSearchTimerItem*)Get(Current()); + + if (item) + SetHelp(item->isActive() ? tr("Deactivate") : tr("Activate"), + tr("Test"), + tr("Delete"), + 0); + else + SetHelp(0, 0, 0, 0); + + return ; +} + +//*************************************************************************** +// Process Key +//*************************************************************************** + +eOSState cMenuEpgSearchTimers::ProcessKey(eKeys Key) +{ + eOSState state = cOsdMenu::ProcessKey(Key); + cEpgMenuSearchTimerItem* item = (cEpgMenuSearchTimerItem*)Get(Current()); + + if (state == osUnknown) + { + switch (Key) + { + case kOk: + { + if (HasSubMenu() || Count() == 0) + return osContinue; + + return osContinue; // #TODO + return AddSubMenu(new cEpgMenuSearchTimerEdit(menuDb, item->getId())); + } + + case kRed: + { + menuDb->searchtimerDb->setValue("ID", item->getId()); + + if (menuDb->searchtimerDb->find()) + { + menuDb->searchtimerDb->setValue("ACTIVE", !menuDb->searchtimerDb->getIntValue("ACTIVE")); + menuDb->searchtimerDb->update(); + refresh(); + } + + return osContinue; + } + + case kGreen: + { + return AddSubMenu(new cEpgMenuSearchResult(menuDb, item->getId())); + } + + case kYellow: + { + if (item && Interface->Confirm(tr("Delete search timer?"))) + { + menuDb->searchtimerDb->setValue("ID", item->getId()); + + if (menuDb->searchtimerDb->find()) + { + menuDb->searchtimerDb->setCharValue("STATE", 'D'); + menuDb->searchtimerDb->update(); + refresh(); + } + } + + return osContinue; + } + + default: break; + } + } + + if (!HasSubMenu() && Key != kNone) + setHelpKeys(); + + return state; +} diff --git a/menutimers.c b/menutimers.c new file mode 100644 index 0000000..011cc39 --- /dev/null +++ b/menutimers.c @@ -0,0 +1,572 @@ +/* + * menutimers.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <vdr/menu.h> +#include <vdr/interface.h> + +#include "plgconfig.h" +#include "menu.h" + +//*************************************************************************** +// cMenuEpgEditTimer +//*************************************************************************** + +cMenuEpgEditTimer::cMenuEpgEditTimer(cMenuDb* db, cEpgTimer* Timer, bool New) + : cOsdMenu(tr("Edit Timer"), 12), + data(db) +{ + SetMenuCategory(mcTimerEdit); + file = 0; + day = firstday = 0; + menuDb = db; + + if (Timer) + { + data.fromTimer(Timer); + + channelNr = data.channel->Number(); + Add(new cMenuEditBitItem(tr("Active"), &data.flags, tfActive)); + + if (menuDb->vdrCount) + Add(new cMenuEditStraItem(tr("VDR"), &data.vdrIndex, menuDb->vdrCount, menuDb->vdrList)); + + Add(new cMenuEditChanItem(tr("Channel"), &channelNr)); + Add(day = new cMenuEditDateItem(tr("Day"), &data.day, &data.weekdays)); + Add(new cMenuEditTimeItem(tr("Start"), &data.start)); + Add(new cMenuEditTimeItem(tr("Stop"), &data.stop)); + Add(new cMenuEditBitItem(tr("VPS"), &data.flags, tfVps)); + Add(new cMenuEditIntItem(tr("Priority"), &data.priority, 0, MAXPRIORITY)); + Add(new cMenuEditIntItem(tr("Lifetime"), &data.lifetime, 0, MAXLIFETIME)); + +#ifdef WITH_PIN + if (cOsd::pinValid || !data.fskProtection) + Add(new cMenuEditBoolItem(tr("Childlock"), &data.fskProtection)); + else + Add(new cOsdItem(cString::sprintf("%s\t%s", tr("Childlock"), data.fskProtection ? trVDR("yes") : trVDR("no")))); +#endif + + Add(file = new cMenuEditStrItem(tr("File"), data.file, sizeof(data.file))); + SetFirstDayItem(); + + Add(new cOsdItem(cString::sprintf("------- %s ----------", tr("Information")))); + cList<cOsdItem>::Last()->SetSelectable(false); + Add(new cOsdItem(cString::sprintf("Status:\t%s", toName((TimerState)data.state)))); + + if (!isEmpty(data.stateInfo)) + Add(new cOsdItem(cString::sprintf("\t%s", data.stateInfo))); + + Add(new cOsdItem(cString::sprintf("Pending action:\t%s", toName((TimerAction)data.action, yes)))); + } + + SetHelpKeys(); +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) +#else + Timers.IncBeingEdited(); +#endif +} + +cMenuEpgEditTimer::~cMenuEpgEditTimer() +{ +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) +#else + Timers.DecBeingEdited(); +#endif +} + +void cMenuEpgEditTimer::SetHelpKeys(void) +{ + SetHelp(tr("Button$Folder"), data.weekdays ? tr("Button$Single") : tr("Button$Repeating")); +} + +void cMenuEpgEditTimer::SetFirstDayItem(void) +{ + if (!firstday && data.weekdays) + { + Add(firstday = new cMenuEditDateItem(tr("First day"), &data.day)); + Display(); + } + else if (firstday && !data.weekdays) + { + Del(firstday->Index()); + firstday = 0; + Display(); + } +} + +eOSState cMenuEpgEditTimer::SetFolder(void) +{ + if (cMenuFolder* mf = dynamic_cast<cMenuFolder*>(SubMenu())) + { + cString Folder = mf->GetFolder(); + const char *p = strrchr(data.file, FOLDERDELIMCHAR); + + if (p) + p++; + else + p = data.file; + + if (!isempty(*Folder)) + strn0cpy(data.file, cString::sprintf("%s%c%s", *Folder, FOLDERDELIMCHAR, p), sizeof(data.file)); + else if (p != data.file) + memmove(data.file, p, strlen(p) + 1); + + SetCurrent(file); + Display(); + } + + return CloseSubMenu(); +} + +eOSState cMenuEpgEditTimer::ProcessKey(eKeys Key) +{ + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osUnknown) + { + switch (Key) + { + case kOk: + { + int recording = no; + +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannel* ch = Channels->GetByNumber(channelNr); +#else + cChannel* ch = Channels.GetByNumber(channelNr); +#endif + + if (!ch) + { + Skins.Message(mtError, tr("*** Invalid Channel ***")); + break; + } + + data.channel = ch; + + if (!*data.file) + strcpy(data.file, data.channel->ShortName(true)); + + cDbRow timerRow("timers"); + + data.toRow(&timerRow); + + // get actual timer state from database + + menuDb->timerDb->clear(); + menuDb->timerDb->setValue("ID", data.TimerId()); + menuDb->timerDb->setValue("VDRUUID", data.getLastVdrUuid()); + + if (menuDb->timerDb->find() && menuDb->timerDb->hasCharValue("STATE", tsRunning)) + recording = yes; + + menuDb->timerDb->reset(); + + // ask for confirmation if timer running! + + if (recording && strcmp(data.getVdrUuid(), data.getLastVdrUuid()) != 0) + { + if (!Interface->Confirm(tr("Timer still recording - really move to other VDR?"))) + return osContinue; + } + + menuDb->modifyTimer(&timerRow, data.getVdrUuid()); + + return osBack; + } + + case kRed: + return AddSubMenu(new cMenuFolder(tr("Select folder"), &Folders, data.file)); + + case kGreen: + { + if (day) + { + day->ToggleRepeating(); + SetCurrent(day); + SetFirstDayItem(); + SetHelpKeys(); + Display(); + } + + return osContinue; + } + + case kYellow: + case kBlue: return osContinue; + + default: break; + } + } + else if (state == osEnd && HasSubMenu()) + state = SetFolder(); + + if (Key != kNone) + SetFirstDayItem(); + + return state; +} + +//*************************************************************************** +// cMenuEpgTimerItem +//*************************************************************************** + +class cMenuEpgTimerItem : public cOsdItem +{ + public: + + cMenuEpgTimerItem(cEpgTimer* Timer); + ~cMenuEpgTimerItem(); + + virtual int Compare(const cListObject &ListObject) const; + virtual void Set(); + cEpgTimer* Timer() { return timer; } + virtual void SetMenuItem(cSkinDisplayMenu* DisplayMenu, int Index, + bool Current, bool Selectable); + + private: + + cEpgTimer* timer; +}; + +cMenuEpgTimerItem::cMenuEpgTimerItem(cEpgTimer* Timer) +{ + timer = Timer; + Set(); +} + +cMenuEpgTimerItem::~cMenuEpgTimerItem() +{ + delete timer; +} + +int cMenuEpgTimerItem::Compare(const cListObject &ListObject) const +{ + return timer->Compare(*((cMenuEpgTimerItem*)&ListObject)->timer); +} + +void cMenuEpgTimerItem::Set() +{ + cString day, dayName(""); + + if (timer->WeekDays()) + { + day = timer->PrintDay(0, timer->WeekDays(), false); + } + else if (timer->Day() - time(0) < 28 * SECSINDAY) + { + day = itoa(timer->GetMDay(timer->Day())); + dayName = WeekDayName(timer->Day()); + } + else + { + struct tm tm_r; + time_t Day = timer->Day(); + localtime_r(&Day, &tm_r); + char buffer[16]; + strftime(buffer, sizeof(buffer), "%Y%m%d", &tm_r); + day = buffer; + } + + const char* vdr = timer->VdrName(); + const char* File = Setup.FoldersInTimerMenu ? 0 : strrchr(timer->File(), FOLDERDELIMCHAR); + + if (File && strcmp(File + 1, TIMERMACRO_TITLE) && strcmp(File + 1, TIMERMACRO_EPISODE)) + File++; + else + File = timer->File(); + + SetText(cString::sprintf("%c\t%s%c\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s", + !(timer->HasFlags(tfActive)) ? ' ' : timer->FirstDay() ? '!' : timer->Recording() ? '#' : '>', + vdr, timer->isVdrRunning() ? '*' : ' ', + timer->Channel() ? timer->Channel()->Number() : na, + *dayName, *dayName && **dayName ? " " : "", *day, + timer->Start() / 100, timer->Start() % 100, + timer->Stop() / 100, timer->Stop() % 100, + File)); +} + +void cMenuEpgTimerItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable) +{ + if (!DisplayMenu->SetItemTimer(timer, Index, Current, Selectable)) + DisplayMenu->SetItem(Text(), Index, Current, Selectable); +} + +//*************************************************************************** +// Class cMenuEpgTimers +//*************************************************************************** +//*************************************************************************** +// Object +//*************************************************************************** + +cMenuEpgTimers::cMenuEpgTimers() + : cOsdMenu(tr("Timers"), 2, CHNUMWIDTH, 10, 6, 6) +{ + helpKeys = -1; + timersMaxUpdsp = 0; + + menuDb = new cMenuDb; + + SetMenuCategory(mcTimer); + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) +#else + Timers.IncBeingEdited(); +#endif + + refresh(); +} + +cMenuEpgTimers::~cMenuEpgTimers() +{ + delete menuDb; + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) +#else + Timers.DecBeingEdited(); +#endif +} + +//*************************************************************************** +// Refresh +//*************************************************************************** + +int cMenuEpgTimers::refresh() +{ + int current = Current(); + + menuDb->timerDb->clear(); + menuDb->vdrDb->clear(); + + menuDb->timerDb->clear(); + menuDb->selectMaxUpdSp->execute(); + timersMaxUpdsp = menuDb->timerDb->getIntValue("UPDSP"); + menuDb->selectMaxUpdSp->freeResult(); + + Clear(); + + menuDb->timerState.setValue("P,R,u"); + menuDb->timerAction.setValue("C,M,A,F,a"); + + for (int f = menuDb->selectTimers->find(); f && menuDb->dbConnected(); f = menuDb->selectTimers->fetch()) + { + cEpgTimer* t = newTimerObjectFromRow(menuDb->timerDb->getRow(), menuDb->vdrDb->getRow()); + + if (t) + Add(new cMenuEpgTimerItem(t)); + } + + menuDb->selectTimers->freeResult(); + + Sort(); + SetCurrent(current >= 0 ? Get(current) : First()); + SetHelpKeys(); + Display(); + + return done; +} + +cEpgTimer* cMenuEpgTimers::currentTimer() +{ + cMenuEpgTimerItem* item = (cMenuEpgTimerItem*)Get(Current()); + return item ? item->Timer() : 0; +} + +void cMenuEpgTimers::SetHelpKeys() +{ + int newHelpKeys = 0; + cEpgTimer* timer = currentTimer(); + + if (timer) + { + if (timer->Event()) + newHelpKeys = 2; + else + newHelpKeys = 1; + } + + if (newHelpKeys != helpKeys) + { + helpKeys = newHelpKeys; + SetHelp(helpKeys > 0 ? tr("Button$On/Off") : 0, + tr("Button$New"), + helpKeys > 0 ? tr("Button$Delete") : 0, + helpKeys == 2 ? tr("Button$Info") : 0); + } +} + +eOSState cMenuEpgTimers::toggleState() +{ + if (HasSubMenu()) + return osContinue; + + cEpgTimer* timer = currentTimer(); + + if (timer) + { + menuDb->timerDb->clear(); + menuDb->timerDb->setValue("ID", timer->TimerId()); + menuDb->timerDb->setValue("VDRUUID", timer->VdrUuid()); + + if (menuDb->timerDb->find()) + { + menuDb->timerDb->setCharValue("ACTION", taModify); + menuDb->timerDb->getValue("STATE")->setNull(); + menuDb->timerDb->setValue("SOURCE", Epg2VdrConfig.uuid); + menuDb->timerDb->setValue("ACTIVE", !menuDb->timerDb->getIntValue("ACTIVE")); + menuDb->timerDb->update(); + menuDb->triggerVdrs("TIMERJOB"); // trigger all! + + tell(0, "Timer %s %sactivated", *timer->ToDescr(), menuDb->timerDb->getIntValue("ACTIVE") ? "" : "de"); + } + } + + return osUser1; +} + +eOSState cMenuEpgTimers::edit() +{ + if (HasSubMenu() || Count() == 0) + return osContinue; + + tell(0, "Editing timer %s", *currentTimer()->ToDescr()); + + return AddSubMenu(new cMenuEpgEditTimer(menuDb, currentTimer())); +} + +eOSState cMenuEpgTimers::create() +{ + if (HasSubMenu()) + return osContinue; + + return AddSubMenu(new cMenuEpgEditTimer(menuDb, new cEpgTimer, yes)); +} + +eOSState cMenuEpgTimers::remove() +{ + int recording = no; + cEpgTimer* ti = currentTimer(); + + if (!ti) + return osContinue; + + if (!Interface->Confirm(tr("Delete timer?"))) + return osContinue; + + // get actual state from database + + menuDb->timerDb->clear(); + menuDb->timerDb->setValue("ID", ti->TimerId()); + menuDb->timerDb->setValue("VDRUUID", ti->VdrUuid()); + + if (menuDb->timerDb->find() && menuDb->timerDb->hasCharValue("STATE", tsRunning)) + recording = yes; + + menuDb->timerDb->reset(); + + // check + + if (recording && !Interface->Confirm(tr("Timer still recording - really delete?"))) + return osContinue; + + // request timer deletion + + tell(0, "Deleting timer %s", *ti->ToDescr()); + + menuDb->deleteTimer(currentTimer()->TimerId()); + + return osUser1; +} + +eOSState cMenuEpgTimers::info() +{ + if (HasSubMenu() || !Count()) + return osContinue; + + cEpgTimer* ti = currentTimer(); + + if (ti && ti->Event()) + { +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + LOCK_TIMERS_READ; + LOCK_CHANNELS_READ; + return AddSubMenu(new cMenuEvent(Timers, Channels, ti->Event())); +#else + return AddSubMenu(new cMenuEvent(ti->Event())); +#endif + } + + return osContinue; +} + +eOSState cMenuEpgTimers::ProcessKey(eKeys Key) +{ + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osUnknown) + { + switch (Key) + { + case kInfo: + case kBlue: return info(); + + case kOk: state = edit(); break; + case kGreen: state = create(); break; + case kYellow: state = remove(); break; + case kRed: state = toggleState(); break; + + case k1: // search repetition of event + { + cEpgTimer* timer = currentTimer(); + + if (timer->Event()) + return AddSubMenu(new cMenuEpgWhatsOn(timer->Event())); + + break; + } + + default: break; + } + } + + if (!HasSubMenu() && Key != kNone && Key != kUp && Key != kDown) + { + int updSp = na; + + SetHelpKeys(); + + if (state != osUser1) + { + menuDb->timerDb->clear(); + menuDb->selectMaxUpdSp->execute(); + updSp = menuDb->timerDb->getIntValue("UPDSP"); + menuDb->selectMaxUpdSp->freeResult(); + } + else + { + usleep(300000); // give VDRs a chance to process the last request + } + + // new timer created or deleted ... ? + + if (state == osUser1 || (updSp != na && timersMaxUpdsp != updSp)) + { + if (updSp != na) + timersMaxUpdsp = updSp; + + refresh(); + + tell(2, "DEBUG: maxUpdSp = %d; osUser1 = '%s'", timersMaxUpdsp, + state == osUser1 ? "Y" : "N"); + } + + if (state == osUser1) + state = osContinue; + } + + return state; +} diff --git a/parameters.c b/parameters.c new file mode 100644 index 0000000..05b0eba --- /dev/null +++ b/parameters.c @@ -0,0 +1,253 @@ +/* + * parameters.c + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "plgconfig.h" +#include "parameters.h" + +//*************************************************************************** +// Parameters +//*************************************************************************** + +cParameters::Parameter cParameters::parameters[] = +{ + // owner, name, type, default, regexp, readonly, visible + +#ifndef VDR_PLUGIN + // -------------------------------------------- + // epgd / epghttpd + + { "epgd", "manualTimer2Done", ptBool, "1", "^[01]$", no, yes }, + { "epgd", "timerJobFailedHistory", ptNum, "10", "^[0-9]{1,3}$", no, yes }, + + { "epgd", "logoUpperCase", ptBool, "0", "^[01]$", no, yes }, + { "epgd", "logoById", ptBool, "0", "^[01]$", no, yes }, + { "epgd", "logoSuffix", ptAscii, "png", "^.{1,5}$", no, yes }, + + { "epgd", "mailScript", ptAscii, BINDEST"/epgd-sendmail", "^.{0,150}$", no, yes }, + + { "epgd", "mergeStart", ptTime, "0", "^[0-9]{1,20}$", yes, no }, + { "epgd", "lastMergeAt", ptTime, "0", "^[0-9]{1,20}$", yes, no }, + { "epgd", "lastScrRefUpdate", ptTime, "0", "^[0-9]{1,20}$", yes, no }, + { "epgd", "lastEpisodeRun", ptTime, "0", "^[0-9]{1,20}$", yes, no }, + { "epgd", "lastEpisodeFullRun", ptTime, "0", "^[0-9]{1,20}$", yes, no }, + + { "epgd", "maxEventTime", ptTime, "0", "^[0-9]{1,20}$", yes, yes }, + { "epgd", "minEventTime", ptTime, "0", "^[0-9]{1,20}$", yes, yes }, + + { "epgd", "md5", ptAscii, "", "^.{0,150}$", yes, no }, + +#endif + + // -------------------------------------------- + // epg2vdr / scraper2vdr + + { "uuid", "lastEventsUpdateAt", ptNum, NULL, "^[0-9]{1,20}$", yes, yes }, + + // -------------------------------------------- + // webif + + { "webif", "needLogin", ptBool, "0", "^[01]$", no, yes }, + + // -------------------------------------------- + // user + + { "@", "chFormat", ptAscii, "", "^.{0,150}$", no, yes }, + { "@", "defaultVDRuuid", ptAscii, "", "^.{0,150}$", no, yes }, + { "@", "startPage", ptAscii, "menu_magazine", "^.{0,150}$", no, yes }, + { "@", "timerDefaultVDRuuid", ptAscii, "", "^.{0,150}$", no, yes }, + { "@", "quickTimes", ptAscii, "Now=@Now~Next=@Next~PrimeTime=20:15~LateNight=00:00", "^(~?[^=]+=(([0-1]?[0-9]|2[0-4]):[0-5]?[0-9]|@Now|@Next))*$", no, yes }, + { "@", "startWithSched", ptBool, "0", "^[01]$", no, yes }, + { "@", "searchAdv", ptBool, "1", "^[01]$", no, yes }, + { "@", "namingModeSerie", ptNum, "1", "^[0-5]$", no, yes }, + { "@", "namingModeSearchSerie", ptNum, "1", "^[0-5]$", no, yes }, + { "@", "namingModeMovie", ptNum, "1", "^[0-5]$", no, yes }, + { "@", "namingModeSearchMovie", ptNum, "1", "^[0-5]$", no, yes }, + { "@", "pickerFirstDay", ptNum, "1", "^[0-6]$", no, yes }, + { "@", "sendTCC", ptBool, "1", "^[01]$", no, yes }, + { "@", "constabelLoginPath", ptAscii, "", "^.{0,150}$", no, yes }, + { "@", "mailReceiver", ptAscii, "", "^.{0,150}$", no, yes }, + + { 0, 0, 0, 0, 0, 0, 0 } +}; + +//*************************************************************************** +// Get Definition +//*************************************************************************** + +cParameters::Parameter* cParameters::getDefinition(const char* owner, const char* name) +{ + if (isEmpty(owner) || isEmpty(name)) + return 0; + + // some specials for dynamic owner or name + + if (owner[0] == '@') + owner = "@"; + + if (strcmp(owner, "epgd") == 0 && strstr(name, ".md5")) + name = "md5"; + + // lookup + + for (int i = 0; parameters[i].name != 0; i++) + { + if (strcmp(parameters[i].owner, owner) == 0 && strcasecmp(parameters[i].name, name) == 0) + return ¶meters[i]; + } + + tell(0, "Warning: Requested parameter '%s/%s' not known, ignoring", owner, name); + + return 0; +} + +//*************************************************************************** +// Object / Init / Exit +//*************************************************************************** + +cParameters::cParameters() +{ + parametersDb = 0; + selectParameters = 0; +} + +int cParameters::initDb(cDbConnection* connection) +{ + int status = success; + + parametersDb = new cDbTable(connection, "parameters"); + if (parametersDb->open() != success) return fail; + + // ---------- + // select * from parameters + // where owner = ? + + selectParameters = new cDbStatement(parametersDb); + + selectParameters->build("select "); + selectParameters->bindAllOut(); + selectParameters->build(" from %s", parametersDb->TableName()); + + status += selectParameters->prepare(); + + return status; +} + +int cParameters::exitDb() +{ + delete parametersDb; parametersDb = 0; + delete selectParameters; selectParameters = 0; + + return done; +} + +//*************************************************************************** +// Get String Parameter +//*************************************************************************** + +int cParameters::getParameter(const char* owner, const char* name, char* value) +{ + int found; + Parameter* definition = getDefinition(owner, name); + + if (value) + *value = 0; + + if (!definition) + return no; + + if (strcasecmp(owner, "uuid") == 0) + owner = Epg2VdrConfig.uuid; + + parametersDb->clear(); + parametersDb->setValue("OWNER", owner); + parametersDb->setValue("NAME", name); + + if (found = parametersDb->find()) + { + if (value) + sprintf(value, "%s", parametersDb->getStrValue("Value")); + } + else if (value && definition->def) + { + sprintf(value, "%s", definition->def); + found = yes; + setParameter(owner, name, value); + } + + parametersDb->reset(); + + return found; +} + +//*************************************************************************** +// Get Integer Parameter +//*************************************************************************** + +int cParameters::getParameter(const char* owner, const char* name, long int& value) +{ + char txt[100]; *txt = 0; + int found; + + found = getParameter(owner, name, txt); + + if (!isEmpty(txt)) + value = atol(txt); + else + value = 0; + + return found; +} + +//*************************************************************************** +// Set String Parameter +//*************************************************************************** + +int cParameters::setParameter(const char* owner, const char* name, const char* value) +{ + Parameter* definition = getDefinition(owner, name); + + if (!definition) + return fail; + + if (!value) + return fail; + + if (strcasecmp(owner, "uuid") == 0) + owner = Epg2VdrConfig.uuid; + + tell(2, "Storing '%s' for '%s' with value '%s'", name, owner, value); + + // validate parameter + + if (definition->regexp) + { + if (rep(value, definition->regexp) != success) + { + tell(0, "Ignoring '%s' for parameter '%s/%s' don't match expression '%s'", + value, owner, name, definition->regexp); + + return fail; + } + } + + parametersDb->clear(); + parametersDb->setValue("OWNER", owner); + parametersDb->setValue("NAME", name); + parametersDb->setValue("VALUE", value); + + return parametersDb->store(); +} + +int cParameters::setParameter(const char* owner, const char* name, long int value) +{ + char txt[16]; + + snprintf(txt, sizeof(txt), "%ld", value); + + return setParameter(owner, name, txt); +} + diff --git a/parameters.h b/parameters.h new file mode 100644 index 0000000..7d45b0b --- /dev/null +++ b/parameters.h @@ -0,0 +1,59 @@ +/* + * parameters.h + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __PARAMETERS_H +#define __PARAMETERS_H + +#include "lib/db.h" + +//*************************************************************************** +// Parameters +//*************************************************************************** + +class cParameters +{ + public: + + enum Type + { + ptNum, + ptTime, // unix time + ptBool, + ptAscii + }; + + struct Parameter + { + const char* owner; + const char* name; + int type; + const char* def; + const char* regexp; + int readonly; + int visible; + }; + + cParameters(); + + int initDb(cDbConnection* connection); + int exitDb(); + + int getParameter(const char* owner, const char* name, char* value = 0); + int getParameter(const char* owner, const char* name, long int& value); + int setParameter(const char* owner, const char* name, const char* value); + int setParameter(const char* owner, const char* name, long int value); + + protected: + + cDbTable* parametersDb; + cDbStatement* selectParameters; + + static Parameter parameters[]; + static Parameter* getDefinition(const char* owner, const char* name); +}; + +#endif // __PARAMETERS_H diff --git a/patches/pre-vdr-2.1.x--epghandler-segment-transfer.patch b/patches/pre-vdr-2.1.x--epghandler-segment-transfer.patch new file mode 100644 index 0000000..8374a66 --- /dev/null +++ b/patches/pre-vdr-2.1.x--epghandler-segment-transfer.patch @@ -0,0 +1,65 @@ +--- ../vdr-2.0.2.plain//eit.c 2012-12-04 12:10:10.000000000 +0100 ++++ eit.c 2013-05-22 16:49:37.635027462 +0200 +@@ -46,6 +46,8 @@ + return; + } + ++ EpgHandlers.BeginSegmentTransfer(channel, OnlyRunningStatus); ++ + bool handledExternally = EpgHandlers.HandledExternally(channel); + cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(channel, true); + +@@ -310,6 +312,7 @@ + Schedules->SetModified(pSchedule); + } + Channels.Unlock(); ++ EpgHandlers.EndSegmentTransfer(Modified, OnlyRunningStatus); + } + + // --- cTDT ------------------------------------------------------------------ +--- ../vdr-2.0.2.plain//epg.c 2013-02-17 15:12:07.000000000 +0100 ++++ epg.c 2013-05-22 16:50:29.043029281 +0200 +@@ -1537,3 +1537,19 @@ + } + Schedule->DropOutdated(SegmentStart, SegmentEnd, TableID, Version); + } ++ ++void cEpgHandlers::BeginSegmentTransfer(const cChannel *Channel, bool OnlyRunningStatus) ++{ ++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { ++ if (eh->BeginSegmentTransfer(Channel, OnlyRunningStatus)) ++ return; ++ } ++} ++ ++void cEpgHandlers::EndSegmentTransfer(bool Modified, bool OnlyRunningStatus) ++{ ++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { ++ if (eh->EndSegmentTransfer(Modified, OnlyRunningStatus)) ++ return; ++ } ++} +--- ../vdr-2.0.2.plain//epg.h 2012-09-24 14:53:53.000000000 +0200 ++++ epg.h 2013-05-22 16:50:16.867028850 +0200 +@@ -273,6 +273,12 @@ + virtual bool DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version) { return false; } + ///< Takes a look at all EPG events between SegmentStart and SegmentEnd and + ///< drops outdated events. ++ virtual bool BeginSegmentTransfer(const cChannel *Channel, bool OnlyRunningStatus) { return false; } ++ ///< called directly after IgnoreChannel before any other handler method called ++ ///< designed to give handlers the ossibility to prepare a transaction ++ virtual bool EndSegmentTransfer(bool Modified, bool OnlyRunningStatus) { return false; } ++ ///< called at last after the segment data is processed ++ ///< at this oint handlers should close/commt/rollback their transactions + }; + + class cEpgHandlers : public cList<cEpgHandler> { +@@ -295,6 +301,8 @@ + void HandleEvent(cEvent *Event); + void SortSchedule(cSchedule *Schedule); + void DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version); ++ void BeginSegmentTransfer(const cChannel *Channel, bool OnlyRunningStatus); ++ void EndSegmentTransfer(bool Modified, bool OnlyRunningStatus); + }; + + extern cEpgHandlers EpgHandlers; diff --git a/patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch b/patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch new file mode 100644 index 0000000..7be1802 --- /dev/null +++ b/patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch @@ -0,0 +1,219 @@ +--- /home/wendel/vdr-1.7.27.plain//eit.c 2012-03-14 11:11:15.000000000 +0100 ++++ eit.c 2012-10-01 09:38:51.526839349 +0200 +@@ -45,6 +45,7 @@ + return; + } + ++ bool handledExternally = EpgHandlers.HandledExternally(channel); + cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(channel, true); + + bool Empty = true; +@@ -70,14 +71,18 @@ + cEvent *newEvent = NULL; + cEvent *rEvent = NULL; + cEvent *pEvent = (cEvent *)pSchedule->GetEvent(SiEitEvent.getEventId(), StartTime); +- if (!pEvent) { ++ if (!pEvent || handledExternally) { + if (OnlyRunningStatus) + continue; ++ if (handledExternally) ++ if (!EpgHandlers.IsUpdate(SiEitEvent.getEventId(), StartTime, Tid, getVersionNumber())) ++ continue; + // If we don't have that event yet, we create a new one. + // Otherwise we copy the information into the existing event anyway, because the data might have changed. + pEvent = newEvent = new cEvent(SiEitEvent.getEventId()); + newEvent->SetStartTime(StartTime); + newEvent->SetDuration(Duration); ++ if (!handledExternally) + pSchedule->AddEvent(newEvent); + } + else { +@@ -290,6 +295,9 @@ + channel->SetLinkChannels(LinkChannels); + Modified = true; + EpgHandlers.HandleEvent(pEvent); ++ ++ if (handledExternally) ++ delete pEvent; + } + if (Tid == 0x4E) { + if (Empty && getSectionNumber() == 0) +--- /home/wendel/vdr-1.7.27.plain//epg.c 2012-03-10 14:14:27.000000000 +0100 ++++ epg.c 2012-10-01 09:41:35.010845128 +0200 +@@ -18,6 +18,7 @@ + #include "timers.h" + + #define RUNNINGSTATUSTIMEOUT 30 // seconds before the running status is considered unknown ++#define EPGDATAWRITEDELTA 600 // seconds between writing the epg.data file + + // --- tComponent ------------------------------------------------------------ + +@@ -1109,6 +1110,47 @@ + return false; + } + ++// --- cEpgDataWriter --------------------------------------------------------- ++ ++class cEpgDataWriter : public cThread { ++private: ++ cMutex mutex; ++protected: ++ virtual void Action(void); ++public: ++ cEpgDataWriter(void); ++ void Perform(void); ++ }; ++ ++cEpgDataWriter::cEpgDataWriter(void) ++:cThread("epg data writer") ++{ ++} ++ ++void cEpgDataWriter::Action(void) ++{ ++ SetPriority(19); ++ SetIOPriority(7); ++ Perform(); ++} ++ ++void cEpgDataWriter::Perform(void) ++{ ++ cMutexLock MutexLock(&mutex); // to make sure fore- and background calls don't cause parellel dumps! ++ { ++ cSchedulesLock SchedulesLock(true, 1000); ++ cSchedules *s = (cSchedules *)cSchedules::Schedules(SchedulesLock); ++ if (s) { ++ time_t now = time(NULL); ++ for (cSchedule *p = s->First(); p; p = s->Next(p)) ++ p->Cleanup(now); ++ } ++ } ++ cSchedules::Dump(); ++} ++ ++static cEpgDataWriter EpgDataWriter; ++ + // --- cSchedulesLock -------------------------------------------------------- + + cSchedulesLock::cSchedulesLock(bool WriteLock, int TimeoutMs) +@@ -1152,28 +1194,13 @@ + if (Force) + lastDump = 0; + time_t now = time(NULL); +- struct tm tm_r; +- struct tm *ptm = localtime_r(&now, &tm_r); +- if (now - lastCleanup > 3600) { +- isyslog("cleaning up schedules data"); +- cSchedulesLock SchedulesLock(true, 1000); +- cSchedules *s = (cSchedules *)Schedules(SchedulesLock); +- if (s) { +- for (cSchedule *p = s->First(); p; p = s->Next(p)) +- p->Cleanup(now); +- } +- lastCleanup = now; +- if (ptm->tm_hour == 5) +- ReportEpgBugFixStats(true); +- } +- if (epgDataFileName && now - lastDump > 600) { +- cSafeFile f(epgDataFileName); +- if (f.Open()) { +- Dump(f); +- f.Close(); ++ if (now - lastDump > EPGDATAWRITEDELTA) { ++ if (epgDataFileName) { ++ if (Force) ++ EpgDataWriter.Perform(); ++ else if (!EpgDataWriter.Active()) ++ EpgDataWriter.Start(); + } +- else +- LOG_ERROR; + lastDump = now; + } + } +@@ -1207,8 +1234,23 @@ + cSchedulesLock SchedulesLock; + cSchedules *s = (cSchedules *)Schedules(SchedulesLock); + if (s) { ++ cSafeFile *sf = NULL; ++ if (!f) { ++ sf = new cSafeFile(epgDataFileName); ++ if (sf->Open()) ++ f = *sf; ++ else { ++ LOG_ERROR; ++ delete sf; ++ return false; ++ } ++ } + for (cSchedule *p = s->First(); p; p = s->Next(p)) + p->Dump(f, Prefix, DumpMode, AtTime); ++ if (sf) { ++ sf->Close(); ++ delete sf; ++ } + return true; + } + return false; +@@ -1329,6 +1371,24 @@ + return true; + } + return false; ++} ++ ++bool cEpgHandlers::HandledExternally(const cChannel *Channel) ++{ ++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { ++ if (eh->HandledExternally(Channel)) ++ return true; ++ } ++ return false; ++} ++ ++bool cEpgHandlers::IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) ++{ ++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { ++ if (eh->IsUpdate(EventID, StartTime, TableID, Version)) ++ return true; ++ } ++ return false; + } + + void cEpgHandlers::SetEventID(cEvent *Event, tEventID EventID) +--- /home/wendel/vdr-1.7.27.plain//epg.h 2012-03-10 14:50:10.000000000 +0100 ++++ epg.h 2012-10-01 09:43:28.162849134 +0200 +@@ -207,7 +207,7 @@ + static void Cleanup(bool Force = false); + static void ResetVersions(void); + static bool ClearAll(void); +- static bool Dump(FILE *f, const char *Prefix = "", eDumpMode DumpMode = dmAll, time_t AtTime = 0); ++ static bool Dump(FILE *f = NULL, const char *Prefix = "", eDumpMode DumpMode = dmAll, time_t AtTime = 0); + static bool Read(FILE *f = NULL); + cSchedule *AddSchedule(tChannelID ChannelID); + const cSchedule *GetSchedule(tChannelID ChannelID) const; +@@ -244,6 +244,16 @@ + ///< EPG handlers are queried to see if any of them would like to do the + ///< complete processing by itself. TableID and Version are from the + ///< incoming section data. ++ virtual bool HandledExternally(const cChannel *Channel) { return false; } ++ ///< If any EPG handler returns true in this function, it is assumed that ++ ///< the EPG for the given Channel is handled completely from some external ++ ///< source. Incoming EIT data is processed as usual, but any new EPG event ++ ///< will not be added to the respective schedule. It's up to the EPG ++ ///< handler to take care of this. ++ virtual bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) { return false; } ++ ///< VDR can't perform the update check (version, tid) for external handled events ++ ///< therefore the handle have to take care. Otherwise the parsing of 'non' updates will ++ ///< take a lot of resources + virtual bool SetEventID(cEvent *Event, tEventID EventID) { return false; } + virtual bool SetTitle(cEvent *Event, const char *Title) { return false; } + virtual bool SetShortText(cEvent *Event, const char *ShortText) { return false; } +@@ -269,6 +279,8 @@ + public: + bool IgnoreChannel(const cChannel *Channel); + bool HandleEitEvent(cSchedule *Schedule, const SI::EIT::Event *EitEvent, uchar TableID, uchar Version); ++ bool HandledExternally(const cChannel *Channel); ++ bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version); + void SetEventID(cEvent *Event, tEventID EventID); + void SetTitle(cEvent *Event, const char *Title); + void SetShortText(cEvent *Event, const char *ShortText); diff --git a/patches/vdr-1.7.28-epghandledexternally.diff b/patches/vdr-1.7.28-epghandledexternally.diff new file mode 100644 index 0000000..52dfab6 --- /dev/null +++ b/patches/vdr-1.7.28-epghandledexternally.diff @@ -0,0 +1,118 @@ +--- ./eit.c 2012/06/02 14:05:22 2.17 ++++ ./eit.c 2012/06/04 10:10:11 +@@ -45,6 +45,7 @@ + return; + } + ++ bool handledExternally = EpgHandlers.HandledExternally(channel); + cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(channel, true); + + bool Empty = true; +@@ -70,7 +71,7 @@ + cEvent *newEvent = NULL; + cEvent *rEvent = NULL; + cEvent *pEvent = (cEvent *)pSchedule->GetEvent(SiEitEvent.getEventId(), StartTime); +- if (!pEvent) { ++ if (!pEvent || handledExternally) { + if (OnlyRunningStatus) + continue; + // If we don't have that event yet, we create a new one. +@@ -78,7 +79,8 @@ + pEvent = newEvent = new cEvent(SiEitEvent.getEventId()); + newEvent->SetStartTime(StartTime); + newEvent->SetDuration(Duration); +- pSchedule->AddEvent(newEvent); ++ if (!handledExternally) ++ pSchedule->AddEvent(newEvent); + } + else { + // We have found an existing event, either through its event ID or its start time. +@@ -290,11 +292,8 @@ + channel->SetLinkChannels(LinkChannels); + Modified = true; + EpgHandlers.HandleEvent(pEvent); +- +- if (EpgHandlers.DeleteEvent(pEvent)) { +- pSchedule->DelEvent(pEvent); +- pEvent = NULL; +- } ++ if (handledExternally) ++ delete pEvent; + } + if (Tid == 0x4E) { + if (Empty && getSectionNumber() == 0) +--- ./epg.c 2012/06/02 14:08:12 2.14 ++++ ./epg.c 2012/06/04 10:06:22 +@@ -1331,6 +1331,15 @@ + return false; + } + ++bool cEpgHandlers::HandledExternally(const cChannel *Channel) ++{ ++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { ++ if (eh->HandledExternally(Channel)) ++ return true; ++ } ++ return false; ++} ++ + void cEpgHandlers::SetEventID(cEvent *Event, tEventID EventID) + { + for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { +@@ -1429,15 +1438,6 @@ + } + } + +-bool cEpgHandlers::DeleteEvent(const cEvent *Event) +-{ +- for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { +- if (eh->DeleteEvent(Event)) +- return true; +- } +- return false; +-} +- + void cEpgHandlers::SortSchedule(cSchedule *Schedule) + { + for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { +--- ./epg.h 2012/06/02 14:07:51 2.10 ++++ ./epg.h 2012/06/04 10:05:21 +@@ -244,6 +244,12 @@ + ///< EPG handlers are queried to see if any of them would like to do the + ///< complete processing by itself. TableID and Version are from the + ///< incoming section data. ++ virtual bool HandledExternally(const cChannel *Channel) { return false; } ++ ///< If any EPG handler returns true in this function, it is assumed that ++ ///< the EPG for the given Channel is handled completely from some external ++ ///< source. Incoming EIT data is processed as usual, but any new EPG event ++ ///< will not be added to the respective schedule. It's up to the EPG ++ ///< handler to take care of this. + virtual bool SetEventID(cEvent *Event, tEventID EventID) { return false; } + virtual bool SetTitle(cEvent *Event, const char *Title) { return false; } + virtual bool SetShortText(cEvent *Event, const char *ShortText) { return false; } +@@ -258,9 +264,6 @@ + virtual bool HandleEvent(cEvent *Event) { return false; } + ///< After all modifications of the Event have been done, the EPG handler + ///< can take a final look at it. +- virtual bool DeleteEvent(const cEvent *Event) { return false; } +- ///< After the complete processing of the Event is finished, an EPG handler +- ///< can decide that this Event shall be deleted from its schedule. + virtual bool SortSchedule(cSchedule *Schedule) { return false; } + ///< Sorts the Schedule after the complete table has been processed. + virtual bool DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version) { return false; } +@@ -272,6 +275,7 @@ + public: + bool IgnoreChannel(const cChannel *Channel); + bool HandleEitEvent(cSchedule *Schedule, const SI::EIT::Event *EitEvent, uchar TableID, uchar Version); ++ bool HandledExternally(const cChannel *Channel); + void SetEventID(cEvent *Event, tEventID EventID); + void SetTitle(cEvent *Event, const char *Title); + void SetShortText(cEvent *Event, const char *ShortText); +@@ -283,7 +287,6 @@ + void SetVps(cEvent *Event, time_t Vps); + void FixEpgBugs(cEvent *Event); + void HandleEvent(cEvent *Event); +- bool DeleteEvent(const cEvent *Event); + void SortSchedule(cSchedule *Schedule); + void DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version); + }; diff --git a/patches/vdr-1.7.29-epgIsUpdate.diff b/patches/vdr-1.7.29-epgIsUpdate.diff new file mode 100644 index 0000000..61549ca --- /dev/null +++ b/patches/vdr-1.7.29-epgIsUpdate.diff @@ -0,0 +1,52 @@ +--- ../vdr-1.7.29.plain//eit.c 2012-06-04 12:26:10.000000000 +0200 ++++ eit.c 2012-07-30 10:19:34.841894485 +0200 +@@ -74,6 +74,9 @@ + if (!pEvent || handledExternally) { + if (OnlyRunningStatus) + continue; ++ if (handledExternally) ++ if (!EpgHandlers.IsUpdate(SiEitEvent.getEventId(), StartTime, Tid, getVersionNumber())) ++ continue; + // If we don't have that event yet, we create a new one. + // Otherwise we copy the information into the existing event anyway, because the data might have changed. + pEvent = newEvent = new cEvent(SiEitEvent.getEventId()); +--- ../vdr-1.7.29.plain//epg.c 2012-06-04 12:26:10.000000000 +0200 ++++ epg.c 2012-07-30 10:21:51.153899306 +0200 +@@ -1340,6 +1340,15 @@ + return false; + } + ++bool cEpgHandlers::IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) ++{ ++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { ++ if (eh->IsUpdate(EventID, StartTime, TableID, Version)) ++ return true; ++ } ++ return false; ++} ++ + void cEpgHandlers::SetEventID(cEvent *Event, tEventID EventID) + { + for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { +--- ../vdr-1.7.29.plain//epg.h 2012-06-04 12:26:10.000000000 +0200 ++++ epg.h 2012-07-30 10:20:15.705895929 +0200 +@@ -250,6 +250,10 @@ + ///< source. Incoming EIT data is processed as usual, but any new EPG event + ///< will not be added to the respective schedule. It's up to the EPG + ///< handler to take care of this. ++ virtual bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) { return false; } ++ ///< VDR can't perform the update check (version, tid) for external handled events ++ ///< therefore the handle have to take care. Otherwise the parsing of 'non' updates will ++ ///< take a lot of resources + virtual bool SetEventID(cEvent *Event, tEventID EventID) { return false; } + virtual bool SetTitle(cEvent *Event, const char *Title) { return false; } + virtual bool SetShortText(cEvent *Event, const char *ShortText) { return false; } +@@ -277,6 +281,7 @@ + bool IgnoreChannel(const cChannel *Channel); + bool HandleEitEvent(cSchedule *Schedule, const SI::EIT::Event *EitEvent, uchar TableID, uchar Version); + bool HandledExternally(const cChannel *Channel); ++ bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version); + void SetEventID(cEvent *Event, tEventID EventID); + void SetTitle(cEvent *Event, const char *Title); + void SetShortText(cEvent *Event, const char *ShortText); + diff --git a/patches/vdr-2.3.1.patch b/patches/vdr-2.3.1.patch new file mode 100644 index 0000000..c37bb54 --- /dev/null +++ b/patches/vdr-2.3.1.patch @@ -0,0 +1,11 @@ +--- ../vdr-2.3.2.plain/./epg.h 2015-08-09 13:25:04.000000000 +0200 ++++ ./epg.h 2017-02-08 14:42:26.304063928 +0100 +@@ -66,7 +66,7 @@ + + class cSchedule; + +-typedef u_int16_t tEventID; ++typedef u_int32_t tEventID; + + class cEvent : public cListObject { + friend class cSchedule; diff --git a/patches/vdr-2.3.2.patch b/patches/vdr-2.3.2.patch new file mode 100644 index 0000000..102b362 --- /dev/null +++ b/patches/vdr-2.3.2.patch @@ -0,0 +1,56 @@ +--- ../vdr-2.3.2.plain//./epg.c 2015-09-10 12:58:19.000000000 +0200 ++++ ./epg.c 2017-02-09 18:40:29.597671711 +0100 +@@ -1527,12 +1527,13 @@ + Schedule->DropOutdated(SegmentStart, SegmentEnd, TableID, Version); + } + +-void cEpgHandlers::BeginSegmentTransfer(const cChannel *Channel) ++bool cEpgHandlers::BeginSegmentTransfer(const cChannel *Channel) + { + for (cEpgHandler *eh = First(); eh; eh = Next(eh)) { +- if (eh->BeginSegmentTransfer(Channel, false)) +- return; ++ if (!eh->BeginSegmentTransfer(Channel, false)) ++ return false; + } ++ return true; + } + + void cEpgHandlers::EndSegmentTransfer(bool Modified) +--- ../vdr-2.3.2.plain//./eit.c 2015-08-23 12:43:36.000000000 +0200 ++++ ./eit.c 2017-02-09 18:40:29.597671711 +0100 +@@ -67,8 +67,13 @@ + return; + } + ++ if (!EpgHandlers.BeginSegmentTransfer(Channel)) { ++ SchedulesStateKey.Remove(false); ++ ChannelsStateKey.Remove(false); ++ return; ++ } ++ + bool ChannelsModified = false; +- EpgHandlers.BeginSegmentTransfer(Channel); + bool handledExternally = EpgHandlers.HandledExternally(Channel); + cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(Channel, true); + +--- ../vdr-2.3.2.plain//./epg.h 2015-08-09 13:25:04.000000000 +0200 ++++ ./epg.h 2017-02-09 18:40:29.601671655 +0100 +@@ -66,7 +66,7 @@ + + class cSchedule; + +-typedef u_int16_t tEventID; ++typedef u_int32_t tEventID; + + class cEvent : public cListObject { + friend class cSchedule; +@@ -311,7 +311,7 @@ + void HandleEvent(cEvent *Event); + void SortSchedule(cSchedule *Schedule); + void DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version); +- void BeginSegmentTransfer(const cChannel *Channel); ++ bool BeginSegmentTransfer(const cChannel *Channel); + void EndSegmentTransfer(bool Modified); + }; + diff --git a/plgconfig.c b/plgconfig.c new file mode 100644 index 0000000..4dda3d2 --- /dev/null +++ b/plgconfig.c @@ -0,0 +1,36 @@ +/* + * config.c: + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "plgconfig.h" + +cEpg2VdrConfig Epg2VdrConfig; + +//*************************************************************************** +// cEpg2VdrConfig +//*************************************************************************** + +cEpg2VdrConfig::cEpg2VdrConfig() + : cEpgConfig() +{ + mainmenuVisible = yes; + mainmenuFullupdate = 0; + blacklist = no; + + activeOnEpgd = no; + scheduleBoot = no; + masterMode = 0; + shareInWeb = yes; + createTimerLocal = no; + useCommonRecFolder = yes; + xchgOkBlue = no; + + replaceScheduleMenu = no; + replaceTimerMenu = no; + userIndex = 0; + *user = 0; + showEmptyChannels = no; +} diff --git a/plgconfig.h b/plgconfig.h new file mode 100644 index 0000000..fce7b0d --- /dev/null +++ b/plgconfig.h @@ -0,0 +1,44 @@ +/* + * plgconfig.h: + * + * See the README file for copyright information and how to reach the author. + * + * $Id: config.h,v 1.2 2012/10/26 08:44:13 wendel Exp $ + */ + +#ifndef __EPG2VDR_CONFIG_H +#define __EPG2VDR_CONFIG_H + +#include "lib/config.h" + +//*************************************************************************** +// Config +//*************************************************************************** + +struct cEpg2VdrConfig : public cEpgConfig +{ + public: + + cEpg2VdrConfig(); + + int mainmenuVisible; + int mainmenuFullupdate; + int activeOnEpgd; + int scheduleBoot; + int blacklist; // to enable noepg feature + int masterMode; + int shareInWeb; // 'am verbund teilnehmen' + int createTimerLocal; + int useCommonRecFolder; // NAS + int xchgOkBlue; + + int replaceScheduleMenu; + int replaceTimerMenu; + int userIndex; + char user[100+TB]; + int showEmptyChannels; +}; + +extern cEpg2VdrConfig Epg2VdrConfig; + +#endif // __EPG2VDR_CONFIG_H diff --git a/po/de_DE.po b/po/de_DE.po new file mode 100644 index 0000000..9a1eb62 --- /dev/null +++ b/po/de_DE.po @@ -0,0 +1,355 @@ +# VDR plugin language source file. +# Copyright (C) 2007 Klaus Schmidinger <kls@cadsoft.de> +# This file is distributed under the same license as the VDR package. +# Klaus Schmidinger <kls@cadsoft.de>, 2000 +# +msgid "" +msgstr "" +"Project-Id-Version: VDR 1.5.7\n" +"Report-Msgid-Bugs-To: <vdr@jwendel.de>\n" +"POT-Creation-Date: 2017-02-14 12:24+0100\n" +"PO-Revision-Date: 2009-08-27 21:40+0200\n" +"Last-Translator: Klaus Schmidinger <kls@cadsoft.de>\n" +"Language-Team: <vdr@linuxtv.org>\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Timer" +msgstr "" + +msgid "Search Timer" +msgstr "Suchtimer" + +msgid "Timer journal" +msgstr "Timerhistorie" + +msgid "Program" +msgstr "Programm" + +#, fuzzy +msgid "Update" +msgstr "Automatisches Update" + +msgid "Reload" +msgstr "Neu laden" + +#, fuzzy +msgid "Update EPG" +msgstr "Automatisches Update" + +msgid "Reload EPG" +msgstr "EPG komplett neu einlesen" + +msgid "auto" +msgstr "" + +msgid "yes" +msgstr "ja" + +msgid "no" +msgstr "nein" + +#, fuzzy +msgid "EPG Update" +msgstr "Automatisches Update" + +msgid "Update DVB EPG Database" +msgstr "DVB/EPG in Datenbank eintragen" + +msgid "Load Images" +msgstr "Bilder laden" + +msgid "Prohibit Shutdown On Busy 'epgd'" +msgstr "Herunterfahren verhindern, wenn EPGD aktiv" + +msgid "Schedule Boot For Update" +msgstr "Booten für geplante Updates" + +msgid "Blacklist not configured Channels" +msgstr "Nicht konfigurierte Kanäle 'blacklisten'" + +msgid "Menu" +msgstr "Menü" + +msgid "Show In Main Menu" +msgstr "Im Hauptmenü anzeigen" + +msgid "Replace Program Menu" +msgstr "Programmübersicht ersetzen" + +msgid "Replace Timer Menu" +msgstr "Timerübersicht ersetzen" + +msgid "XChange Key Ok/Blue" +msgstr "Tausche Taste OK/Blau" + +msgid "Show Channels without EPG" +msgstr "Zeige Kanäle ohne EPG" + +msgid "Web" +msgstr "Netzwerk" + +msgid "Share in Web" +msgstr "Im Netz teilen" + +msgid "Create Timer Local" +msgstr "Erstelle Timer lokal" + +msgid "Have Common Recordings Folder (NAS)" +msgstr "Zentrale Aufnahmeablage (NAS)" + +msgid "SVDRP Interface" +msgstr "" + +msgid "User" +msgstr "Benutzer" + +msgid "MySQL" +msgstr "" + +msgid "Host" +msgstr "" + +msgid "Port" +msgstr "" + +msgid "Database Name" +msgstr "Datenbankname" + +msgid "Password" +msgstr "Passwort" + +msgid "Technical Stuff" +msgstr "Technisches" + +msgid "Log level" +msgstr "Protokollierungsstufe" + +msgid "EPG2VDR Waiting on epgd" +msgstr "EPG2VDR wartet auf EPGD" + +msgid "Timer not found, maybe deleted from other user" +msgstr "Timer nicht gefunden, evtl. von anderem Benutzer gelöscht" + +msgid "Recorded" +msgstr "Aufgenommen" + +msgid "Created" +msgstr "Angelegt" + +msgid "Failed" +msgstr "Fehlgeschlagen" + +msgid "Deleted" +msgstr "Gelöscht" + +msgid "All" +msgstr "Alle" + +msgid "Delete" +msgstr "Löschen" + +msgid "Timer journal - Recorded" +msgstr "Timerhistorie - Aufgenommen" + +msgid "Timer journal - Created" +msgstr "Timerhistorie - Erzeugt" + +msgid "Timer journal - Failed" +msgstr "Timerhistorie - Fehlgeschlagen" + +msgid "Timer journal - Delete" +msgstr "Timerhistorie - Gelöscht" + +msgid "Delete timer from journal?" +msgstr "Eintrag aus Timerhistorie löschen?" + +msgid "Search matching Events" +msgstr "" + +msgid "Search matching Recordings" +msgstr "" + +msgid "Searchtimers" +msgstr "" + +msgid "Matching recordings" +msgstr "Vergleiche Aufnahmen" + +msgid "Direct Matches:" +msgstr "Direkte Ãœbereinstimmungen:" + +msgid "All Matches:" +msgstr "Alle Ãœbereinstimmungen:" + +msgid "Event" +msgstr "" + +msgid "Button$Timer" +msgstr "Timer" + +msgid "Button$Record" +msgstr "Aufnehmen" + +msgid "Button$Switch" +msgstr "Umschalten" + +#, c-format +msgid "Overview - %s" +msgstr "Ãœbersicht - %s" + +#, c-format +msgid "Overview - %s (%s)" +msgstr "Ãœbersicht - %s (%s)" + +#, c-format +msgid "Schedule - %s" +msgstr "Programm - %s" + +msgid "Search results" +msgstr "Suchergebnisse" + +msgid "Button$Schedule" +msgstr "Programm" + +msgid "Button$Info" +msgstr "Information" + +msgid "Can't switch channel!" +msgstr "Umschalten nicht möglich!" + +msgid "Edit Search Timer" +msgstr "Suchtimer bearbeiten" + +msgid "Remove all done entries of this event?" +msgstr "Alle erledigten Einträge dieses Ereignisses entfernen?" + +msgid "Search Timers" +msgstr "Suchtimer" + +msgid "Deactivate" +msgstr "Deaktivieren" + +msgid "Activate" +msgstr "Aktivieren" + +msgid "Test" +msgstr "" + +msgid "Delete search timer?" +msgstr "Suchtimer löschen?" + +msgid "Edit Timer" +msgstr "Timer bearbeiten" + +msgid "Active" +msgstr "Aktiv" + +msgid "VDR" +msgstr "" + +msgid "Channel" +msgstr "Kanal" + +msgid "Day" +msgstr "Tag" + +msgid "Start" +msgstr "Start" + +msgid "Stop" +msgstr "Stop" + +msgid "VPS" +msgstr "" + +msgid "Priority" +msgstr "Priorität" + +msgid "Lifetime" +msgstr "Lebensdauer" + +msgid "Childlock" +msgstr "Kindersperre" + +msgid "File" +msgstr "Datei" + +msgid "Information" +msgstr "" + +msgid "Button$Folder" +msgstr "Ordner" + +msgid "Button$Single" +msgstr "Einzeln" + +msgid "Button$Repeating" +msgstr "Wiederholung" + +msgid "First day" +msgstr "Erster Tag" + +msgid "*** Invalid Channel ***" +msgstr "*** ungültiger Kanal ***" + +msgid "Timer still recording - really move to other VDR?" +msgstr "Timer zeichnet auf, dennoch verschieben?" + +msgid "Select folder" +msgstr "Verzeichnis wählen" + +msgid "Timers" +msgstr "Timer" + +msgid "Button$On/Off" +msgstr "An/Aus" + +msgid "Button$New" +msgstr "Neu" + +msgid "Button$Delete" +msgstr "Löschen" + +msgid "Delete timer?" +msgstr "Timer löschen" + +msgid "Timer still recording - really delete?" +msgstr "Timer zeichnet auf, dennoch löschen?" + +#, c-format +msgid "Skipping '%s' request, epgd busy. Trying again later" +msgstr "" + +msgid "reload" +msgstr "neu laden" + +msgid "update" +msgstr "aktualisieren" + +#, c-format +msgid "EPG '%s' done" +msgstr "EPG '%s' abgeschlossen" + +#~ msgid "Now" +#~ msgstr "Jetzt" + +#~ msgid "Next" +#~ msgstr "Nächste" + +#~ msgid "This event - %s" +#~ msgstr "Dieses Ereignis - %s" + +#~ msgid "This event - all channels" +#~ msgstr "Dieses Ereignis - alle Kanäle" + +#~ msgid "All events - all channels" +#~ msgstr "Alle Ereignisse - alle Kanäle" + +#~ msgid "Button$Now" +#~ msgstr "Jetzt" + +#~ msgid "Button$Next" +#~ msgstr "Nächste" diff --git a/po/it_IT.po b/po/it_IT.po new file mode 100644 index 0000000..356928e --- /dev/null +++ b/po/it_IT.po @@ -0,0 +1,343 @@ +# VDR plugin language source file. +# Copyright (C) 2007 Klaus Schmidinger <kls@cadsoft.de> +# This file is distributed under the same license as the VDR package. +# Alberto Carraro <bertocar@tin.it>, 2001 +# Antonio Ospite <ospite@studenti.unina.it>, 2003 +# Sean Carlos <seanc@libero.it>, 2005 +# +msgid "" +msgstr "" +"Project-Id-Version: VDR 1.5.7\n" +"Report-Msgid-Bugs-To: <vdr@jwendel.de>\n" +"POT-Creation-Date: 2017-02-14 12:24+0100\n" +"PO-Revision-Date: 2009-08-27 21:45+0100\n" +"Last-Translator: Diego Pierotto <vdr-italian@tiscali.it>\n" +"Language-Team: <vdr@linuxtv.org>\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Language: Italian\n" +"X-Poedit-Country: ITALY\n" +"X-Poedit-SourceCharset: utf-8\n" + +msgid "Timer" +msgstr "" + +msgid "Search Timer" +msgstr "" + +msgid "Timer journal" +msgstr "" + +msgid "Program" +msgstr "" + +#, fuzzy +msgid "Update" +msgstr "Aggiorn. automatico" + +msgid "Reload" +msgstr "" + +#, fuzzy +msgid "Update EPG" +msgstr "Aggiorn. automatico" + +msgid "Reload EPG" +msgstr "" + +msgid "auto" +msgstr "" + +msgid "yes" +msgstr "" + +msgid "no" +msgstr "" + +#, fuzzy +msgid "EPG Update" +msgstr "Aggiorn. automatico" + +msgid "Update DVB EPG Database" +msgstr "" + +msgid "Load Images" +msgstr "" + +msgid "Prohibit Shutdown On Busy 'epgd'" +msgstr "" + +msgid "Schedule Boot For Update" +msgstr "" + +msgid "Blacklist not configured Channels" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Show In Main Menu" +msgstr "Mostra nel menu principale" + +msgid "Replace Program Menu" +msgstr "" + +msgid "Replace Timer Menu" +msgstr "" + +msgid "XChange Key Ok/Blue" +msgstr "" + +msgid "Show Channels without EPG" +msgstr "" + +msgid "Web" +msgstr "" + +msgid "Share in Web" +msgstr "" + +msgid "Create Timer Local" +msgstr "" + +msgid "Have Common Recordings Folder (NAS)" +msgstr "" + +msgid "SVDRP Interface" +msgstr "" + +msgid "User" +msgstr "" + +msgid "MySQL" +msgstr "" + +msgid "Host" +msgstr "" + +msgid "Port" +msgstr "" + +msgid "Database Name" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Technical Stuff" +msgstr "" + +msgid "Log level" +msgstr "xxx #AS# " + +msgid "EPG2VDR Waiting on epgd" +msgstr "" + +msgid "Timer not found, maybe deleted from other user" +msgstr "" + +msgid "Recorded" +msgstr "" + +msgid "Created" +msgstr "" + +msgid "Failed" +msgstr "" + +msgid "Deleted" +msgstr "" + +msgid "All" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Timer journal - Recorded" +msgstr "" + +msgid "Timer journal - Created" +msgstr "" + +msgid "Timer journal - Failed" +msgstr "" + +msgid "Timer journal - Delete" +msgstr "" + +msgid "Delete timer from journal?" +msgstr "" + +msgid "Search matching Events" +msgstr "" + +msgid "Search matching Recordings" +msgstr "" + +msgid "Searchtimers" +msgstr "" + +msgid "Matching recordings" +msgstr "" + +msgid "Direct Matches:" +msgstr "" + +msgid "All Matches:" +msgstr "" + +msgid "Event" +msgstr "" + +msgid "Button$Timer" +msgstr "" + +msgid "Button$Record" +msgstr "" + +msgid "Button$Switch" +msgstr "" + +#, c-format +msgid "Overview - %s" +msgstr "" + +#, c-format +msgid "Overview - %s (%s)" +msgstr "" + +#, c-format +msgid "Schedule - %s" +msgstr "" + +msgid "Search results" +msgstr "" + +msgid "Button$Schedule" +msgstr "" + +msgid "Button$Info" +msgstr "" + +msgid "Can't switch channel!" +msgstr "" + +msgid "Edit Search Timer" +msgstr "" + +msgid "Remove all done entries of this event?" +msgstr "" + +msgid "Search Timers" +msgstr "" + +msgid "Deactivate" +msgstr "" + +msgid "Activate" +msgstr "" + +msgid "Test" +msgstr "" + +msgid "Delete search timer?" +msgstr "" + +msgid "Edit Timer" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "VDR" +msgstr "" + +msgid "Channel" +msgstr "" + +msgid "Day" +msgstr "" + +msgid "Start" +msgstr "" + +msgid "Stop" +msgstr "" + +msgid "VPS" +msgstr "" + +msgid "Priority" +msgstr "" + +msgid "Lifetime" +msgstr "" + +msgid "Childlock" +msgstr "" + +msgid "File" +msgstr "" + +msgid "Information" +msgstr "" + +msgid "Button$Folder" +msgstr "" + +msgid "Button$Single" +msgstr "" + +msgid "Button$Repeating" +msgstr "" + +msgid "First day" +msgstr "" + +msgid "*** Invalid Channel ***" +msgstr "" + +msgid "Timer still recording - really move to other VDR?" +msgstr "" + +msgid "Select folder" +msgstr "" + +msgid "Timers" +msgstr "" + +msgid "Button$On/Off" +msgstr "" + +msgid "Button$New" +msgstr "" + +msgid "Button$Delete" +msgstr "" + +msgid "Delete timer?" +msgstr "" + +msgid "Timer still recording - really delete?" +msgstr "" + +#, c-format +msgid "Skipping '%s' request, epgd busy. Trying again later" +msgstr "" + +msgid "reload" +msgstr "" + +msgid "update" +msgstr "" + +#, c-format +msgid "EPG '%s' done" +msgstr "" + +#, fuzzy +#~ msgid "Updatetime (hours)" +#~ msgstr "Tempo aggiorn. (min)" diff --git a/recinfofile.c b/recinfofile.c new file mode 100644 index 0000000..cc957da --- /dev/null +++ b/recinfofile.c @@ -0,0 +1,245 @@ +/* + * recinfofile.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "update.h" + +//*************************************************************************** +// Class Event Details +//*************************************************************************** + +const char* cEventDetails::fields[] = +{ + "ACTOR", + "AUDIO", + "CATEGORY", + "COUNTRY", + "DIRECTOR", + "FLAGS", + "GENRE", + "MUSIC", + "PRODUCER", + "SCREENPLAY", + "SHORTREVIEW", + "TIPP", + "TOPIC", + "YEAR", + "RATING", + "NUMRATING", + "MODERATOR", + "OTHER", + "GUEST", + "CAMERA", + + "SCRSERIESID", + "SCRSERIESEPISODE", + "SCRMOVIEID", + + // just in recordinglist, not in events or useevents row + + "CHANNELNAME", + "SCRINFOMOVIEID", + "SCRINFOSERIESID", + "SCRINFOEPISODEID", + + 0 +}; + +//*************************************************************************** +// Set Value +//*************************************************************************** + +void cEventDetails::setValue(const char* name, const char* value) +{ + std::map<std::string,std::string>::iterator it; + + it = values.find(name); + + if (it == values.end() || it->first != value) + { + changes++; + values[name] = value; + } +} + +void cEventDetails::setValue(const char* name, int value) +{ + setValue(name, num2Str(value).c_str()); +} + +//*************************************************************************** +// Update By Row +//*************************************************************************** + +int cEventDetails::updateByRow(cDbRow* row) +{ + std::map<std::string,std::string>::iterator it; + + for (int i = 0; fields[i]; i++) + { + if (!row->getTableDef()->getField(fields[i], /*silent*/ yes)) + continue; // skip silent! + + cDbValue* value = row->getValue(fields[i]); + + if (!value) + { + tell(2, "Warning: Field '%s' not found", fields[i]); // only for debug + continue; + } + + if (!value->isNull()) + { + std::string v; + + if (value->getField()->isString()) + v = row->getStrValue(fields[i]); + else if (value->getField()->isInt()) + v = num2Str(row->getIntValue(fields[i])); + else + { + tell(0, "Info: Field '%s' unhandled for info.epg2vdr", fields[i]); + continue; + } + + it = values.find(fields[i]); + + if (it == values.end() || it->second != v) + { + changes++; + values[fields[i]] = v; + } + } + } + + return success; +} + +//*************************************************************************** +// Update To Row +//*************************************************************************** + +int cEventDetails::updateToRow(cDbRow* row) +{ + std::map<std::string, std::string>::iterator it; + + for (it = values.begin(); it != values.end(); it++) + { + cDbValue* value = row->getValue(it->first.c_str()); + + if (!value) + { + tell(0, "Warning: Field '%s' not found", it->first.c_str()); + continue; + } + + if (!it->first.length()) + continue; + + if (value->getField()->isString()) + value->setValue(it->second.c_str()); + else if (value->getField()->isInt()) + value->setValue(atoi(it->second.c_str())); + else + { + tell(0, "Info: Field '%s' unhandled for info.epg2vdr", it->first.c_str()); + continue; + } + } + + return success; +} + +//*************************************************************************** +// Store To Fs +//*************************************************************************** + +int cEventDetails::storeToFs(const char* path) +{ + FILE* f; + char* fileName = 0; + std::map<std::string,std::string>::iterator it; + + asprintf(&fileName, "%s/info.epg2vdr", path); + + if (!(f = fopen(fileName, "w"))) + { + tell(0, "Error opening file '%s' failed, %s", fileName, strerror(errno)); + free(fileName); + + return fail; + } + + tell(0, "Storing event details to '%s'", fileName); + + // store fields + + for (it = values.begin(); it != values.end(); it++) + fprintf(f, "%s: %s\n", it->first.c_str(), it->second.c_str()); + + free(fileName); + fclose(f); + + return success; +} + +//*************************************************************************** +// Load From Fs +//*************************************************************************** + +int cEventDetails::loadFromFs(const char* path) +{ + FILE* f; + char* fileName = 0; + std::map<std::string,std::string>::iterator it; + + values.clear(); + changes = 0; + + asprintf(&fileName, "%s/info.epg2vdr", path); + + if (!fileExists(fileName)) + { + free(fileName); + return done; + } + + if (!(f = fopen(fileName, "r"))) + { + tell(0, "Error opening file '%s' failed, %s", fileName, strerror(errno)); + free(fileName); + + return fail; + } + + tell(3, "Loading event details from '%s'", fileName); + + // load fields + + char* p; + char* s; + cReadLine readLine; + + while (s = readLine.Read(f)) + { + if (!(p = strchr(s, ':'))) + { + tell(0, " "); + continue; + } + + *(p++) = 0; + p = skipspace(rTrim(p)); + + if (!isEmpty(p)) + values[s] = p; + } + + free(fileName); + fclose(f); + + return success; +} diff --git a/recording.c b/recording.c new file mode 100644 index 0000000..c3d5ac4 --- /dev/null +++ b/recording.c @@ -0,0 +1,533 @@ +/* + * recording.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <set> + +#include <vdr/videodir.h> + +#include "lib/common.h" +#include "update.h" + +//*************************************************************************** +// Is Protected +//*************************************************************************** + +int isProtected(const char* path) +{ + char* dir; + int fsk = no; + char* file = 0; + char* p = 0; + + const char* videoDir = cVideoDirectory::Name(); + + if (strncmp(path, videoDir, strlen(videoDir)) != 0) + { + tell(0, "Fatal: Unexpected video dir in '%s'", path); + return no; + } + + dir = strdup(path + strlen(videoDir)); + + do + { + if (p) *p = 0; + + asprintf(&file, "%s/%s/protection.fsk", videoDir, dir); + fsk = fileExists(file); + free(file); + + } while (!fsk && (p = strrchr(dir, '/'))); + + tell(3, "'%s' is %sprotected", path, fsk ? "" : "not "); + free(dir); + + return fsk; +} + +//*************************************************************************** +// Cleanup Deleted Recordings from Table +//*************************************************************************** + +int cUpdate::cleanupDeletedRecordings(int force) +{ + int delCnt = 0; + md5Buf md5path; + std::set<std::string> recMd5List; + + // -------------------------- + // get recordings lock + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + LOCK_RECORDINGS_READ; + const cRecordings* recordings = Recordings; +#else + const cRecordings* recordings = &Recordings; +#endif + + if (recordings->Count() == lastRecordingCount && !force) + return done; + + tell(0, "Cleanup deleted recordings at database%s", force ? " (forced)" : ""); + + // create set of existing recordings (to improve speed) + + for (const cRecording* rec = recordings->First(); rec; rec = recordings->Next(rec)) + { + int pathOffset = 0; + + if (strncmp(rec->FileName(), videoBasePath, strlen(videoBasePath)) == 0) + { + pathOffset = strlen(videoBasePath); + + if (*(rec->FileName()+pathOffset) == '/') + pathOffset++; + } + + createMd5(rec->FileName()+pathOffset, md5path); + + tell(5, "DEBUG: Recording: '%s' [%s]", rec->Title(), rec->FileName()); + recMd5List.insert(md5path); + } + + connection->startTransaction(); + + recordingListDb->clear(); + recordingListDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + + for (int f = selectRecordings->find(); f && dbConnected(); f = selectRecordings->fetch()) + { + // bin 'ich' zuständig? + + if (!recordingListDb->hasValue("VDRUUID", Epg2VdrConfig.uuid)) + { + if (!recordingListDb->hasValue("OWNER", "") || !Epg2VdrConfig.useCommonRecFolder) + continue; + + // sonderlocke, das mounted ggf. nicht jeder überall + + if (recordingListDb->getIntValue("FSK")) + continue; + } + + if (recMd5List.find(recordingListDb->getStrValue("MD5PATH")) == recMd5List.end()) + { + // not found, mark as deleted + + delCnt++; + recordingListDb->setValue("STATE", "D"); + recordingListDb->update(); + } + } + + selectRecordings->freeResult(); + + connection->commit(); + lastRecordingCount = recordings->Count(); + + tell(0, "Info: Marked %d recordings as deleted", delCnt); + + return success; +} + +//*************************************************************************** +// Update Pending Recording Info Files +//*************************************************************************** + +int cUpdate::updatePendingRecordingInfoFiles(const cRecordings* recordings) +{ + cEventDetails evd; + int count = 0; + + if (pendingNewRecordings.empty()) + return done; + + tell(1, "Updating recording info in info.epg2vdr"); + + // iterate over pending recordings + + while (!pendingNewRecordings.empty()) + { + std::string path = pendingNewRecordings.front(); + const cRecording* rec = ((cRecordings*)recordings)->GetByName(path.c_str()); + + pendingNewRecordings.pop(); + + if (!rec || !rec->Info() || !rec->Info()->GetEvent()) + { + tell(0, "Warning: Recording '%s' not found or missin recording info," + " can't create info.epg2vdr", path.c_str()); + continue; + } + + useeventsDb->clear(); + useeventsDb->setValue("USEID", (int)rec->Info()->GetEvent()->EventID()); + + if (selectEventById->find()) + { + evd.loadFromFs(path.c_str()); + evd.updateByRow(useeventsDb->getRow()); + + if (evd.getChanges()) + { + count++; + evd.storeToFs(path.c_str()); + + tell(1, "Updated/Created recording info for %d in info.epg2vdr with %d changes", + rec->Info()->GetEvent()->EventID(), evd.getChanges()); + } + } + else + tell(0, "Warning: Event %d not found in table", rec->Info()->GetEvent()->EventID()); + + selectEventById->freeResult(); + } + + tell(1, "Updated %d pending info.epg2vdr files", count); + + return done; +} + +//*************************************************************************** +// Store All Recording Info Files (only used by manual trigger) +//*************************************************************************** + +int cUpdate::storeAllRecordingInfoFiles() +{ + cEventDetails evd; + int count = 0; + + tell(1, "Store info.epg2vdr for all recordings"); + + connection->startTransaction(); + recordingListDb->clear(); + recordingListDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + + for (int f = selectRecordings->find(); f && dbConnected(); f = selectRecordings->fetch()) + { + char* path; + + // bin 'ich' zuständig? + + if (!recordingListDb->hasValue("VDRUUID", Epg2VdrConfig.uuid)) + { + // nicht zuständig wenn OWNER nicht leer oder ich nicht am NAS Verbund teilnehme + + if (!recordingListDb->hasValue("OWNER", "") || !Epg2VdrConfig.useCommonRecFolder) + continue; + } + + asprintf(&path, "%s/%s", videoBasePath, recordingListDb->getStrValue("PATH")); + + evd.loadFromFs(path); + evd.updateByRow(recordingListDb->getRow()); + + if (evd.getChanges()) + { + count++; + evd.storeToFs(path); + + tell(2, "Store recording info for %ld in info.epg2vdr with %d changes", + recordingListDb->getIntValue("EVENTID"), evd.getChanges()); + } + + time_t sp = time(0); + recordingListDb->setValue("LASTIFOUPD", sp); + recordingListDb->update(sp); + + free(path); + } + + connection->commit(); + selectRecordings->freeResult(); + storeAllRecordingInfoFilesTrigger = no; + + tell(1, "Stored %d info.epg2vdr files", count); + + return done; +} + +//*************************************************************************** +// Update Recording Info Files +//*************************************************************************** + +int cUpdate::updateRecordingInfoFiles() +{ + cEventDetails evd; + int count = 0; + + tell(1, "Update info.epg2vdr recordings"); + + connection->startTransaction(); + recordingListDb->clear(); + + for (int f = selectRecForInfoUpdate->find(); f && dbConnected(); f = selectRecForInfoUpdate->fetch()) + { + char* path; + + asprintf(&path, "%s/%s", videoBasePath, recordingListDb->getStrValue("PATH")); + + // only update if this VDR can access the recording folder ... + + if (folderExists(path)) + { + evd.loadFromFs(path); + evd.updateByRow(recordingListDb->getRow()); + + if (evd.getChanges()) + { + count++; + evd.storeToFs(path); + + tell(1, "Update recording info for %ld in info.epg2vdr with %d changes", + recordingListDb->getIntValue("EVENTID"), evd.getChanges()); + } + + time_t sp = time(0); + recordingListDb->setValue("LASTIFOUPD", sp); + recordingListDb->update(sp); // force same stamp! + } + else + { + tell(3, "Info: Folder '%s' not found, suppose it belong to another vdr", path); + } + + free(path); + } + + connection->commit(); + selectRecForInfoUpdate->freeResult(); + tell(1, "Updated %d info.epg2vdr files", count); + + return done; +} + +//*************************************************************************** +// Update Recording Table +//*************************************************************************** + +int cUpdate::updateRecordingTable(int fullReload) +{ + int count = 0, insCnt = 0, updCnt = 0, dirCnt = 0; + + // first cleanup, at least to adjust count in 'lastRecordingCount' + + if (fullReload) + recordingListDb->deleteWhere("vdruuid = '%s'", Epg2VdrConfig.uuid); + else + cleanupDeletedRecordings(yes); + + // get channel and recordings lock + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cChannelsLock channelsLock(false); + const cChannels* channels = channelsLock.Channels(); +#else + cChannels* channels = &Channels; +#endif + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cRecordingsLock recordingsLock(false); + const cRecordings* recordings = recordingsLock.Recordings(); +#else + const cRecordings* recordings = &Recordings; +#endif + + // update + + tell(0, "Updating recording list table"); + + connection->startTransaction(); + + recordingDirDb->deleteWhere("vdruuid = '%s'", Epg2VdrConfig.uuid); + + // ---------------- + // update ... + + for (const cRecording* rec = recordings->First(); rec; rec = recordings->Next(rec)) + { + int insert; + int fsk; + md5Buf md5path; + const char* subTitle = ""; + int eventId = 0; + std::string channelId = ""; + const char* description = ""; + const char* title = rec->Name(); + const cRecordingInfo* recInfo = rec->Info(); + int pathOffset = 0; + const cChannel* channel = 0; + int baseChanges = 0; + + // check if directory is registered + + if (rec->HierarchyLevels() > 0) + dirCnt += updateRecordingDirectory(rec); + + // check if recording is registered + + if (strncmp(rec->FileName(), videoBasePath, strlen(videoBasePath)) == 0) + { + pathOffset = strlen(videoBasePath); + + if (*(rec->FileName()+pathOffset) == '/') + pathOffset++; + } + + createMd5(rec->FileName()+pathOffset, md5path); + + if (recInfo) + { + channelId = *(recInfo->ChannelID().ToString()); + subTitle = recInfo->ShortText() ? recInfo->ShortText() : ""; + description = recInfo->Description() ? recInfo->Description() : ""; + channel = channels->GetByChannelID(recInfo->ChannelID()); + + if (recInfo->Title()) title = recInfo->Title(); + if (recInfo->GetEvent()) eventId = recInfo->GetEvent()->EventID(); + } + + fsk = isProtected(rec->FileName()); + + recordingListDb->clear(); + + recordingListDb->setValue("MD5PATH", md5path); + recordingListDb->setValue("STARTTIME", rec->Start()); + recordingListDb->setValue("OWNER", Epg2VdrConfig.useCommonRecFolder ? "" : Epg2VdrConfig.uuid); + + insert = !recordingListDb->find(); + recordingListDb->clearChanged(); + + // base data .. + + recordingListDb->setValue("STATE", rec->IsInUse() == ruTimer ? "R" : "F"); + recordingListDb->setValue("INUSE", rec->IsInUse()); + recordingListDb->setValue("PATH", rec->FileName()+pathOffset); + recordingListDb->setValue("NAME", rec->BaseName()); + recordingListDb->setValue("FOLDER", rec->Folder()); + recordingListDb->setValue("LONGDESCRIPTION", description); + recordingListDb->setValue("DURATION", rec->LengthInSeconds() > 0 ? rec->LengthInSeconds() : 0); + recordingListDb->setValue("EVENTID", eventId); + recordingListDb->setValue("CHANNELID", channelId.c_str()); + recordingListDb->setValue("FSK", fsk); + + if (channel) + recordingListDb->setValue("CHANNELNAME", channel->Name()); + + // scraping relevand data .. + + baseChanges = recordingListDb->getChanges(); + + recordingListDb->setValue("TITLE", title); + recordingListDb->setValue("SHORTTEXT", subTitle); + + // load event details + + cEventDetails evd; + evd.loadFromFs(rec->FileName()); + evd.updateToRow(recordingListDb->getRow()); + + // any scrap relevand data changed? + + if (recordingListDb->getChanges() != baseChanges) + { + int isSeries = recordingListDb->hasValue("CATEGORY", "Serie"); + int changed = no; + + if (isSeries) + { + if (recordingListDb->getValue("SCRSERIESID")->isEmpty() || + !recordingListDb->hasValue("SCRSERIESEPISODE", recordingListDb->getIntValue("SCRINFOEPISODEID")) || + !recordingListDb->hasValue("SCRSERIESID", recordingListDb->getIntValue("SCRINFOSERIESID"))) + { + changed = yes; + } + } + else + { + if (recordingListDb->getValue("SCRMOVIEID")->isEmpty() || + !recordingListDb->hasValue("SCRMOVIEID", recordingListDb->getIntValue("SCRINFOMOVIEID"))) + { + changed = yes; + } + } + + if (changed) + { + recordingListDb->setValue("SCRNEW", yes); // force scrap + recordingListDb->setValue("SCRSP", time(0)); // force load from vdr + } + } + + // don't toggle uuid if already set! + + if (recordingListDb->getValue("VDRUUID")->isNull()) + recordingListDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + + if (insert || recordingListDb->getChanges()) + { + insert ? insCnt++ : updCnt++; + tell(2, "Info: '%s' recording '%s / %s' due to %d changes [%s]", + insert ? "Insert" : "Update", title, subTitle, + recordingListDb->getChanges(), recordingListDb->getChangedFields().c_str()); + recordingListDb->store(); + } + + count++; + + recordingListDb->reset(); + } + + connection->commit(); + + tell(0, "Info: Found %d recordings; %d inserted; %d updated " + "and %d directories", count, insCnt, updCnt, dirCnt); + + // create info files for new recordings + + updatePendingRecordingInfoFiles(recordings); + + return success; +} + +//*************************************************************************** +// Update Recording Directory +//*************************************************************************** + +int cUpdate::updateRecordingDirectory(const cRecording* recording) +{ + int cnt = 0; + char* dir = strdup(recording->Name()); + char* pos = strrchr(dir, '~'); + + if (pos) + { + *pos = 0; + + for (int level = 0; level < recording->HierarchyLevels(); level++) + { + recordingDirDb->clear(); + recordingDirDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + recordingDirDb->setValue("DIRECTORY", dir); + + if (!recordingDirDb->find()) + { + cnt++; + recordingDirDb->store(); + } + + recordingDirDb->reset(); + + char* pos = strrchr(dir, '~'); + if (pos) *pos=0; + } + } + + free(dir); + + return cnt; +} diff --git a/service.c b/service.c new file mode 100644 index 0000000..20ccf14 --- /dev/null +++ b/service.c @@ -0,0 +1,71 @@ +/* + * service.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include "service.h" +#include "plgconfig.h" + +//*************************************************************************** +// Class cEpgTimer +//*************************************************************************** + +cEpgTimer::cEpgTimer(bool Instant, bool Pause, const cChannel* Channel) + : cEpgTimer_Interface_V1(Instant, Pause, Channel) +{ + timerid = na; eventid = na; + vdrName = 0; vdrUuid = 0; + vdrRunning = no; + stateInfo = 0; + local = yes; +} + +cEpgTimer::~cEpgTimer() +{ + free(vdrUuid); + free(vdrName); + free(stateInfo); +} + +void cEpgTimer::setState(char s, const char* info) +{ + state = s; + free(stateInfo); + stateInfo = 0; + + if (!isEmpty(info)) + stateInfo = strdup(info); +} + +void cEpgTimer::setAction(char a) +{ + action = a; +} + +void cEpgTimer::setVdr(const char* name, const char* uuid, int running) +{ + local = yes; // the default + free(vdrUuid); + free(vdrName); + vdrName = strdup(name); + + if (!isEmpty(uuid)) + vdrUuid = strdup(uuid); + + vdrRunning = running; + + if (!isEmpty(vdrUuid) && strcmp(vdrUuid, Epg2VdrConfig.uuid) != 0) + local = no; +} + +//*************************************************************************** +// Class cEpgEvent +//*************************************************************************** + +cEpgEvent::cEpgEvent(tEventID EventID) + : cEpgEvent_Interface_V1(EventID) +{ + +} diff --git a/service.h b/service.h new file mode 100644 index 0000000..7651316 --- /dev/null +++ b/service.h @@ -0,0 +1,127 @@ +/* + * service.h: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef _SERVICE_H_ +#define _SERVICE_H_ + +#include <vdr/timers.h> +#include <vdr/epg.h> + +#include <list> + +//*************************************************************************** +// Timer - Skin Interface +//*************************************************************************** + +class cEpgEvent_Interface_V1 : public cEvent +{ + public: + + cEpgEvent_Interface_V1(tEventID EventID) + : cEvent(EventID) {} + + // #TODO ... getter + + protected: + + // #TODO ... attributes +}; + +//*************************************************************************** +// Timer - Skin Interface +//*************************************************************************** + +class cEpgTimer_Interface_V1 : public cTimer +{ + public: + + cEpgTimer_Interface_V1(bool Instant = false, bool Pause = false, const cChannel* Channel = 0) +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + : cTimer(Instant, Pause, Channel) {} +#else + : cTimer(Instant, Pause, (cChannel*)Channel) {} +#endif + + long TimerId() { return timerid; } + long EventId() { return eventid; } + const char* VdrName() { return vdrName ? vdrName : ""; } + const char* VdrUuid() { return vdrUuid ? vdrUuid : ""; } + int isVdrRunning() { return vdrRunning; } + int isLocal() { return local; } + int isRemote() { return !isLocal(); } + + char State() { return state; } + int hasState(char s) const { return state == s; } + const char* StateInfo() { return stateInfo ? stateInfo : ""; } + char Action() { return action; } + + protected: + + long timerid; + long eventid; + + char* vdrName; + char* vdrUuid; + int local; + int vdrRunning; + + char state; + char* stateInfo; + char action; +}; + +//*************************************************************************** +// Timer - Service Interface +//*************************************************************************** + +struct cEpgTimer_Service_V1 +{ + std::list<cEpgTimer_Interface_V1*> epgTimers; +}; + +#define EPG2VDR_TIMER_UPDATED "Epg2Vdr_Timer_Updated-v1.0" +#define EPG2VDR_TIMER_SERVICE "Epg2Vdr_Timer_Service-v1.0" + +#ifdef EPG2VDR + +//*************************************************************************** +// Class cEpgEvent +//*************************************************************************** + +class cEpgEvent : public cEpgEvent_Interface_V1 +{ + public: + + cEpgEvent(tEventID EventID); + virtual ~cEpgEvent() {} + + // #TODO ... setter +}; + +//*************************************************************************** +// Class cEpgTimer +//*************************************************************************** + +class cEpgTimer : public cEpgTimer_Interface_V1 +{ + public: + + cEpgTimer(bool Instant = false, bool Pause = false, const cChannel* Channel = 0); + virtual ~cEpgTimer(); + + void setTimerId(long id) { timerid = id; } + void setEventId(long id) { eventid = id; } + void setState(char s, const char* info); + void setAction(char a); + void setVdr(const char* name, const char* uuid = 0, int running = 0); +}; + +#endif // EPG2VDR + +//*************************************************************************** + +#endif // _SERVICE_H_ diff --git a/status.c b/status.c new file mode 100644 index 0000000..e58d641 --- /dev/null +++ b/status.c @@ -0,0 +1,272 @@ +/* + * status.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <vdr/tools.h> + +#include "update.h" +#include "ttools.h" + +//*************************************************************************** +// ----> Copied from epgsearch ;-) +//*************************************************************************** + +#define LOC_INDEXFILESUFFIX "/index" + +bool IsPesRecording(const cRecording *pRecording) +{ +#if VDRVERSNUM < 10703 + return true; +#else + return pRecording && pRecording->IsPesRecording(); +#endif +} + +#if VDRVERSNUM < 10703 + +int RecLengthInSecs(const cRecording* pRecording) +{ + struct stat buf; + cString fullname = cString::sprintf("%s%s", pRecording->FileName(), "/index.vdr"); + + if (stat(fullname, &buf) == 0) + { + struct tIndex { int offset; uchar type; uchar number; short reserved; }; + int delta = buf.st_size % sizeof(tIndex); + + if (delta) + { + delta = sizeof(tIndex) - delta; + tell(0, "ERROR: invalid file size (%ld) in '%s'", buf.st_size, *fullname); + } + + return (buf.st_size + delta) / sizeof(tIndex) / SecondsToFrames(1); + } + + return -1; +} + +#else + +struct tIndexTs +{ + uint64_t offset:40; // up to 1TB per file (not using off_t here - must definitely be exactly 64 bit!) + int reserved:7; // reserved for future use + int independent:1; // marks frames that can be displayed by themselves (for trick modes) + uint16_t number:16; // up to 64K files per recording + + tIndexTs(off_t Offset, bool Independent, uint16_t Number) + { + offset = Offset; + reserved = 0; + independent = Independent; + number = Number; + } +}; + +int RecLengthInSecs(const cRecording *pRecording) +{ + struct stat buf; + cString fullname = cString::sprintf("%s%s", pRecording->FileName(), IsPesRecording(pRecording) ? LOC_INDEXFILESUFFIX ".vdr" : LOC_INDEXFILESUFFIX); + + if (pRecording->FileName() && *fullname && access(fullname, R_OK) == 0 && stat(fullname, &buf) == 0) + { + double frames = buf.st_size ? (buf.st_size - 1) / sizeof(tIndexTs) + 1 : 0; + double Seconds = 0; + modf((frames + 0.5) / pRecording->FramesPerSecond(), &Seconds); + return Seconds; + } + + return -1; +} + +#endif + +//*************************************************************************** +// Copied from epgsearch ;-) <---------- +//*************************************************************************** + +//*************************************************************************** +// Notifications from VDRs Status Interface +//*************************************************************************** +//*************************************************************************** +// Timers Change Notification +//*************************************************************************** + +void cUpdate::TimerChange(const cTimer* Timer, eTimerChange Change) +{ + if (!Epg2VdrConfig.shareInWeb) + return; + + tell(1, "Timer changed, trigger update. Action was (%d)", Change); + + timerTableUpdateTriggered = yes; + waitCondition.Broadcast(); // wakeup +} + +//*************************************************************************** +// Recording Notification +//*************************************************************************** + +void cUpdate::Recording(const cDevice* Device, const char* Name, const char* FileName, bool On) +{ + cMutexLock lock(&runningRecMutex); + const int allowedBreakDuration = 2; + + // Recording of 'Peter Hase' has 'started' [/srv/vdr/video.00/Peter_Hase/2014-10-08.11.05.18-0.rec] + // Recording of '(null)' has 'stopped' [/srv/vdr/video.00/Peter_Hase/2014-10-08.11.05.18-0.rec] + + tell(1, "Recording of '%s' has '%s' [%s]", Name, On ? "started" : "stopped", FileName); + + // at start of recording store event details to recording directory (info.epg2vdr) + + if (On) + pendingNewRecordings.push(FileName); + + // get timers lock + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cTimersLock timersLock(false); + const cTimers* timers = timersLock.Timers(); +#else + const cTimers* timers = &Timers; +#endif + + // recording started ... + + if (On && Name) + { + for (const cTimer* ti = timers->First(); ti; ti = timers->Next(ti)) + { + if (ti->Recording()) // timer nimmt gerade auf + { + cRunningRecording* recording = 0; + + // check if already known + + for (cRunningRecording* rr = runningRecordings.First(); rr; rr = runningRecordings.Next(rr)) + { + if (rr->timer == ti) + { + recording = rr; + break; + } + } + + if (recording) // already handled -> a resume?! + { + tell(1, "Info: Detected resume of '%s' on device %d", Name, Device->CardIndex()); + continue; + } + + int doneid = na; + contentOfTag(ti, "doneid", doneid); + + recording = new cRunningRecording(ti, doneid); + runningRecordings.Add(recording); + tell(1, "Info: Recording '%s' with doneid %d added to running list", Name, doneid); + } + } + } + + // recording stopped ... + + if (!On) + { + // loop over running recordings .. + + for (cRunningRecording* rr = runningRecordings.First(); rr; rr = runningRecordings.Next(rr)) + { + const cTimer* pendingTimer = 0; + int complete; + int recFraction = 100; + long timerLengthSecs = rr->timer->StopTime() - rr->timer->StartTime(); + bool vpsUsed = rr->timer->HasFlags(tfVps) && rr->timer->Event() && rr->timer->Event()->Vps(); + + // check if timer still exists + + for (pendingTimer = timers->First(); pendingTimer; pendingTimer = timers->Next(pendingTimer)) + { + if (pendingTimer == rr->timer) + break; + } + + // still recording :o ? + + if (pendingTimer && pendingTimer->Recording()) + continue; + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cStateKey stateKey; + + if (const cRecordings* recordings = cRecordings::GetRecordingsRead(stateKey)) + { + const cRecording* pRecording = recordings->GetByName(FileName); + + if (pRecording && timerLengthSecs) + { + int recLen = RecLengthInSecs(pRecording); + recFraction = double(recLen) * 100 / timerLengthSecs; + } + + stateKey.Remove(); + } +#else + const cRecording* pRecording = Recordings.GetByName(FileName); + + if (pRecording && timerLengthSecs) + { + int recLen = RecLengthInSecs(pRecording); + recFraction = double(recLen) * 100 / timerLengthSecs; + } +#endif + + // assure timer has reached it's end or at least 90% (vps) / 98% were recorded + + complete = recFraction >= (vpsUsed ? 90 : 98); + + if (complete) + tell(1, "Info: Finished: '%s'; recorded %d%%; VPS %s", + rr->timer->File(), recFraction, vpsUsed ? "Yes": "No"); + else + tell(1, "Info: Finished: '%s' (not complete! - recorded only %d%%); VPS %s", + rr->timer->File(), recFraction, vpsUsed ? "Yes": "No"); + + if (complete) + rr->lastBreak = 0; // reset break + else if (!rr->lastBreak) + rr->lastBreak = time(0); // store first break + + if (!rr->lastBreak || (time(0) - rr->lastBreak) > allowedBreakDuration) + { + char* infoTxt; + + asprintf(&infoTxt, "Recording '%s' finished - %s complete (%d%%)", + rr->timer->File(), complete ? "" : "NOT", recFraction); + + tell(1, "Info: %s", infoTxt); + + rr->finished = yes; + rr->failed = !complete; + rr->setInfo(infoTxt); + + free(infoTxt); + } + } + } + + recordingStateChangedTrigger = yes; + waitCondition.Broadcast(); // wakeup +} + +//*************************************************************************** +// Channel Switch Notification +//*************************************************************************** + +void cUpdate::ChannelSwitch(const cDevice* Device, int ChannelNumber, bool LiveView) +{ + // to be implemented ... ??? +} diff --git a/svdrpclient.c b/svdrpclient.c new file mode 100644 index 0000000..53be015 --- /dev/null +++ b/svdrpclient.c @@ -0,0 +1,749 @@ + /* + * svdrpclient.c + * + * See the README file for copyright information + * + */ + + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/select.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <sys/time.h> + +#include <unistd.h> +#include <netdb.h> +#include <fcntl.h> +#include <string.h> +#include <stdlib.h> +#include <ctype.h> +#include <errno.h> + +#include "lib/common.h" + +#include "svdrpclient.h" + +#ifndef VDR_PLUGIN + +ssize_t safe_read(int filedes, void *buffer, size_t size) +{ + for (;;) + { + ssize_t p = read(filedes, buffer, size); + + if (p < 0 && errno == EINTR) + { + tell(0, "EINTR while reading from file handle %d - retrying", filedes); + continue; + } + return p; + } +} + +ssize_t safe_write(int filedes, const void *buffer, size_t size) +{ + ssize_t p = 0; + ssize_t written = size; + const unsigned char *ptr = (const unsigned char *)buffer; + + while (size > 0) + { + p = write(filedes, ptr, size); + + if (p < 0) + { + if (errno == EINTR) + { + tell(0, "EINTR while writing to file handle %d - retrying", filedes); + continue; + } + + break; + } + ptr += p; + size -= p; + } + + return p < 0 ? p : written; +} + +// --- cListObject ----------------------------------------------------------- + +cListObject::cListObject(void) +{ + prev = next = NULL; +} + +cListObject::~cListObject() +{ +} + +void cListObject::Append(cListObject *Object) +{ + next = Object; + Object->prev = this; +} + +void cListObject::Insert(cListObject *Object) +{ + prev = Object; + Object->next = this; +} + +void cListObject::Unlink(void) +{ + if (next) + next->prev = prev; + if (prev) + prev->next = next; + next = prev = NULL; +} + +int cListObject::Index(void) const +{ + cListObject *p = prev; + int i = 0; + + while (p) { + i++; + p = p->prev; + } + return i; +} + +// --- cListBase ------------------------------------------------------------- + +cListBase::cListBase(void) +{ + objects = lastObject = NULL; + count = 0; +} + +cListBase::~cListBase() +{ + Clear(); +} + +void cListBase::Add(cListObject *Object, cListObject *After) +{ + if (After && After != lastObject) { + After->Next()->Insert(Object); + After->Append(Object); + } + else { + if (lastObject) + lastObject->Append(Object); + else + objects = Object; + lastObject = Object; + } + count++; +} + +void cListBase::Ins(cListObject *Object, cListObject *Before) +{ + if (Before && Before != objects) { + Before->Prev()->Append(Object); + Before->Insert(Object); + } + else { + if (objects) + objects->Insert(Object); + else + lastObject = Object; + objects = Object; + } + count++; +} + +void cListBase::Del(cListObject *Object, bool DeleteObject) +{ + if (Object == objects) + objects = Object->Next(); + if (Object == lastObject) + lastObject = Object->Prev(); + Object->Unlink(); + if (DeleteObject) + delete Object; + count--; +} + +void cListBase::Move(int From, int To) +{ + Move(Get(From), Get(To)); +} + +void cListBase::Move(cListObject *From, cListObject *To) +{ + if (From && To && From != To) { + if (From->Index() < To->Index()) + To = To->Next(); + if (From == objects) + objects = From->Next(); + if (From == lastObject) + lastObject = From->Prev(); + From->Unlink(); + if (To) { + if (To->Prev()) + To->Prev()->Append(From); + From->Append(To); + } + else { + lastObject->Append(From); + lastObject = From; + } + if (!From->Prev()) + objects = From; + } +} + +void cListBase::Clear(void) +{ + while (objects) { + cListObject *object = objects->Next(); + delete objects; + objects = object; + } + objects = lastObject = NULL; + count = 0; +} + +cListObject *cListBase::Get(int Index) const +{ + if (Index < 0) + return NULL; + cListObject *object = objects; + while (object && Index-- > 0) + object = object->Next(); + return object; +} + +static int CompareListObjects(const void *a, const void *b) +{ + const cListObject *la = *(const cListObject **)a; + const cListObject *lb = *(const cListObject **)b; + return la->Compare(*lb); +} + +void cListBase::Sort(void) +{ + int n = Count(); + cListObject** a; + cListObject* object = objects; + int i = 0; + + a = (cListObject**)malloc(n * sizeof(cListObject*)); + + while (object && i < n) + { + a[0] = 0; + a[i++] = object; + object = object->Next(); + } + + qsort(a, n, sizeof(cListObject*), CompareListObjects); + objects = lastObject = NULL; + + for (i = 0; i < n; i++) + { + a[i]->Unlink(); + count--; + Add(a[i]); + } + + free(a); +} + +// --- cTimeMs --------------------------------------------------------------- + +cTimeMs::cTimeMs(int Ms) +{ + if (Ms >= 0) + Set(Ms); + else + begin = 0; +} + +uint64_t cTimeMs::Now(void) +{ +#if _POSIX_TIMERS > 0 && defined(_POSIX_MONOTONIC_CLOCK) +#define MIN_RESOLUTION 5 // ms + static bool initialized = false; + static bool monotonic = false; + struct timespec tp; + if (!initialized) { + // check if monotonic timer is available and provides enough accurate resolution: + if (clock_getres(CLOCK_MONOTONIC, &tp) == 0) { + // long Resolution = tp.tv_nsec; + // require a minimum resolution: + if (tp.tv_sec == 0 && tp.tv_nsec <= MIN_RESOLUTION * 1000000) { + if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0) { + // tell(0, "cTimeMs: using monotonic clock (resolution is %ld ns)", Resolution); + monotonic = true; + } + else + tell(0, "cTimeMs: clock_gettime(CLOCK_MONOTONIC) failed"); + } + else + tell(0, "cTimeMs: not using monotonic clock - resolution is too bad (%ld s %ld ns)", tp.tv_sec, tp.tv_nsec); + } + else + tell(0, "cTimeMs: clock_getres(CLOCK_MONOTONIC) failed"); + initialized = true; + } + if (monotonic) { + if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0) + return (uint64_t(tp.tv_sec)) * 1000 + tp.tv_nsec / 1000000; + tell(0, "cTimeMs: clock_gettime(CLOCK_MONOTONIC) failed"); + monotonic = false; + // fall back to gettimeofday() + } +#else +# warning Posix monotonic clock not available +#endif + struct timeval t; + if (gettimeofday(&t, NULL) == 0) + return (uint64_t(t.tv_sec)) * 1000 + t.tv_usec / 1000; + return 0; +} + +void cTimeMs::Set(int Ms) +{ + begin = Now() + Ms; +} + +bool cTimeMs::TimedOut(void) +{ + return Now() >= begin; +} + +uint64_t cTimeMs::Elapsed(void) +{ + return Now() - begin; +} + + +// --- cFile ----------------------------------------------------------------- + +bool cFile::files[FD_SETSIZE] = { false }; +int cFile::maxFiles = 0; + +cFile::cFile(void) +{ + f = -1; +} + +cFile::~cFile() +{ + Close(); +} + +bool cFile::Open(const char *FileName, int Flags, mode_t Mode) +{ + if (!IsOpen()) + return Open(open(FileName, Flags, Mode)); + tell(0, "ERROR: attempt to re-open %s", FileName); + return false; +} + +bool cFile::Open(int FileDes) +{ + if (FileDes >= 0) { + if (!IsOpen()) { + f = FileDes; + if (f >= 0) { + if (f < FD_SETSIZE) { + if (f >= maxFiles) + maxFiles = f + 1; + if (!files[f]) + files[f] = true; + else + tell(0, "ERROR: file descriptor %d already in files[]", f); + return true; + } + else + tell(0, "ERROR: file descriptor %d is larger than FD_SETSIZE (%d)", f, FD_SETSIZE); + } + } + else + tell(0, "ERROR: attempt to re-open file descriptor %d", FileDes); + } + return false; +} + +void cFile::Close(void) +{ + if (f >= 0) { + close(f); + files[f] = false; + f = -1; + } +} + +bool cFile::Ready(bool Wait) +{ + return f >= 0 && AnyFileReady(f, Wait ? 1000 : 0); +} + +bool cFile::AnyFileReady(int FileDes, int TimeoutMs) +{ + fd_set set; + FD_ZERO(&set); + for (int i = 0; i < maxFiles; i++) { + if (files[i]) + FD_SET(i, &set); + } + if (0 <= FileDes && FileDes < FD_SETSIZE && !files[FileDes]) + FD_SET(FileDes, &set); // in case we come in with an arbitrary descriptor + if (TimeoutMs == 0) + TimeoutMs = 10; // load gets too heavy with 0 + struct timeval timeout; + timeout.tv_sec = TimeoutMs / 1000; + timeout.tv_usec = (TimeoutMs % 1000) * 1000; + return select(FD_SETSIZE, &set, NULL, NULL, &timeout) > 0 && (FileDes < 0 || FD_ISSET(FileDes, &set)); +} + +bool cFile::FileReady(int FileDes, int TimeoutMs) +{ + fd_set set; + struct timeval timeout; + FD_ZERO(&set); + FD_SET(FileDes, &set); + if (TimeoutMs >= 0) { + if (TimeoutMs < 100) + TimeoutMs = 100; + timeout.tv_sec = TimeoutMs / 1000; + timeout.tv_usec = (TimeoutMs % 1000) * 1000; + } + return select(FD_SETSIZE, &set, NULL, NULL, (TimeoutMs >= 0) ? &timeout : NULL) > 0 && FD_ISSET(FileDes, &set); +} + +bool cFile::FileReadyForWriting(int FileDes, int TimeoutMs) +{ + fd_set set; + struct timeval timeout; + FD_ZERO(&set); + FD_SET(FileDes, &set); + if (TimeoutMs < 100) + TimeoutMs = 100; + timeout.tv_sec = 0; + timeout.tv_usec = TimeoutMs * 1000; + return select(FD_SETSIZE, NULL, &set, NULL, &timeout) > 0 && FD_ISSET(FileDes, &set); +} + +#endif // VDR_PLUGIN + +//*************************************************************************** +// SVDRP Client +//*************************************************************************** + +const char* eoc = "\n"; // End Of Command + +cSvdrpClient::cSvdrpClient(const char* aIp, int aPort) +{ + port = aPort; + ip = aIp ? ::strdup(aIp) : 0; + bufSize = BUFSIZ; + buffer = (char*)malloc(bufSize); +} + +cSvdrpClient::~cSvdrpClient(void) +{ + close(); + + free(ip); + free(buffer); +} + +//*************************************************************************** +// Connect +//*************************************************************************** + +int cSvdrpClient::connect() +{ + if (!ip) + { + tell(0, "SVDRPCL: No server IP specified"); + return -1; + } + + struct hostent* hostInfo; + struct sockaddr_in server_addr; + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(port); + in_addr_t remoteAddr; + + if (!isdigit(ip[1])) + { + // map hostname to ip + + if (hostInfo = ::gethostbyname(ip)) + memcpy((char*)&remoteAddr, hostInfo->h_addr, hostInfo->h_length); + + else if ((remoteAddr = inet_addr(ip)) == INADDR_NONE) + return -1; + + memcpy(&server_addr.sin_addr, &remoteAddr, sizeof(struct in_addr)); + } + else if (!::inet_aton(ip, &server_addr.sin_addr)) + { + tell(0, "SVDRPCL: Invalid server IP '%s'", ip); + return -1; + } + + int sock = ::socket(PF_INET, SOCK_STREAM, 0); + + if (sock < 0) + { + tell(0, "SVDRPCL: Error creating socket for connection to %s: %s", ip, strerror(errno)); + return -1; + } + + // set nonblocking + + int flags = ::fcntl(sock, F_GETFL, 0); + + if (flags < 0 || ::fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) + { + tell(0, "SVDRPCL: Unable to use nonblocking I/O for %s: %s", ip, strerror(errno)); + return -1; + } + + if (::connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) + { + if (errno != EINPROGRESS) + { + tell(0, "SVDRPCL: connect to %s:%hu failed: %s", ip, port, strerror(errno)); + return -1; + } + + int result; + fd_set fds; + struct timeval tv; + cTimeMs starttime; + int timeout = 10 * 1000; + + do { + FD_ZERO(&fds); + FD_SET(sock, &fds); + tv.tv_usec = (timeout % 1000) * 1000; + tv.tv_sec = timeout / 1000; + result = ::select(sock + 1, 0, &fds, 0, &tv); + + } while (result == -1 && errno == EINTR + && (timeout = 10 * 1000 - starttime.Elapsed()) > 100); + + if (!result) // timeout + { + result = -1; + errno = ETIMEDOUT; + } + else if (result == 1) // check socket for errors + { + int error; + socklen_t size = sizeof(error); + result = ::getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &size); + if (result == 0 && error != 0) + { + result = -1; + errno = error; + } + } + + if (result != 0) + { + tell(0, "SVDRPCL: Error connecting to %s:%hu: %s", ip, port, strerror(errno)); + ::close(sock); + + return -1; + } + } + + return sock; +} + +int cSvdrpClient::open() +{ + if (file.IsOpen()) + return 0; + + int fd = connect(); + + if (fd < 0) + return -1; + + if (!file.Open(fd)) + { + ::close(fd); + return -1; + } + + // check for greeting + + cList<cLine> greeting; + + if (receive(&greeting) != 220) + { + tell(0, "SVDRPCL: did not receive greeting from %s. Closing...", ip); + abort(); + + return -1; + } + + const char* msg = 0; + + if (greeting.First() && greeting.First()->Text()) + msg = greeting.First()->Text(); + + tell(2, "SVDRPCL: connected to %s:%hu '%s'", ip, port, msg); + + return 0; +} + +void cSvdrpClient::close(int sendQuit) +{ + if (!file.IsOpen()) + return; + + if (sendQuit) + { + if (send("QUIT", false)) + receive(); + } + + file.Close(); +} + +void cSvdrpClient::abort(void) +{ + file.Close(); +} + +int cSvdrpClient::send(const char* cmd, int reconnect) +{ + if (!cmd) + return false; + + if (reconnect && !file.IsOpen()) + open(); + + if (!file.IsOpen()) + { + tell(0, "SVDRPCL: unable to send command to %s. Socket is closed", ip); + return false; + } + + int len = ::strlen(cmd); + + if (safe_write(file, cmd, len) < 0 || safe_write(file, eoc, strlen(eoc)) < 0) + { + tell(0, "SVDRPCL: error while writing to %s: %s", ip, strerror(errno)); + abort(); + + return false; + } + + return true; +} + +int cSvdrpClient::receive(cList<cLine>* list, int timeoutMs) +{ + // #TODO iconv ?! + + while (readLine(timeoutMs)) + { + char* tail; + long int code = ::strtol(buffer, &tail, 10); + + if (tail - buffer == 3 + && code >= 100 + && code <= 999 + && (*tail == ' ' || *tail == '-')) + { + const char* s = buffer + 4; + + if (code >= 451 && code <= 699) + tell(0, "Error: Got (%ld) '%s' form ", code, s); + + if (list) + list->Add(new cLine(s)); + + if (*tail == ' ') + return code; + } + else + { + tell(0, "SVDRPCL: Unexpected reply from %s '%s'", ip, buffer); + close(); + break; + } + } + + if (list) + list->Clear(); + + return 0; +} + +int cSvdrpClient::readLine(int timeoutMs) +{ + if (!file.IsOpen()) + return false; + + int tail = 0; + + while (cFile::FileReady(file, timeoutMs)) + { + unsigned char c; + int r = safe_read(file, &c, 1); + + if (r > 0) + { + if (c == *eoc || c == 0x00) + { + // line complete, make sure the string is terminated + + buffer[tail] = 0; + return true; + } + else if ((c <= 0x1F || c == 0x7F) && c != 0x09) + { + // ignore control characters + } + else + { + if (tail >= bufSize - 1) + { + bufSize += BUFSIZ; + buffer = srealloc(buffer, bufSize); + + if (!buffer) + { + tell(0, "SVDRPCL: unable to increase buffer size to %d byte", bufSize); + close(); + + return false; + } + } + + buffer[tail++] = c; + } + } + else + { + tell(0, "SVDRPCL: lost connection '%s'", ip); + buffer[0] = 0; + abort(); + + return false; + } + } + + tell(0, "SVDRPCL: timeout waiting server reply '%s'", ip); + buffer[0] = 0; + abort(); + + return false; +} diff --git a/svdrpclient.h b/svdrpclient.h new file mode 100644 index 0000000..f0f7078 --- /dev/null +++ b/svdrpclient.h @@ -0,0 +1,158 @@ +/* + * svdrpclient.h: SVDRP connection + * + * See the README file for copyright information and how to reach the author. + */ + +#ifndef __SVDRP_CLIENT_H_ +#define __SVDRP_CLIENT_H_ + +#include <sys/stat.h> +#include <sys/types.h> + +#include <stdint.h> // uint_64_t +#include <string.h> +#include <stdio.h> + +#include "lib/common.h" + +#ifdef VDR_PLUGIN +# define __STL_CONFIG_H +# include <vdr/tools.h> +#else + +//*************************************************************************** +// +//*************************************************************************** + +class cListObject { +private: + cListObject *prev, *next; +public: + cListObject(void); + virtual ~cListObject(); + virtual int Compare(const cListObject &ListObject) const { return 0; } + ///< Must return 0 if this object is equal to ListObject, a positive value + ///< if it is "greater", and a negative value if it is "smaller". + void Append(cListObject *Object); + void Insert(cListObject *Object); + void Unlink(void); + int Index(void) const; + cListObject *Prev(void) const { return prev; } + cListObject *Next(void) const { return next; } +}; + +class cListBase { +protected: + cListObject *objects, *lastObject; + cListBase(void); + int count; +public: + virtual ~cListBase(); + void Add(cListObject *Object, cListObject *After = NULL); + void Ins(cListObject *Object, cListObject *Before = NULL); + void Del(cListObject *Object, bool DeleteObject = true); + virtual void Move(int From, int To); + void Move(cListObject *From, cListObject *To); + virtual void Clear(void); + cListObject *Get(int Index) const; + int Count(void) const { return count; } + void Sort(void); +}; + +template<class T> class cList : public cListBase { +public: + T *Get(int Index) const { return (T *)cListBase::Get(Index); } + T *First(void) const { return (T *)objects; } + T *Last(void) const { return (T *)lastObject; } + T *Prev(const T *object) const { return (T *)object->cListObject::Prev(); } // need to call cListObject's members to + T *Next(const T *object) const { return (T *)object->cListObject::Next(); } // avoid ambiguities in case of a "list of lists" +}; + +class cFile { +private: + static bool files[]; + static int maxFiles; + int f; +public: + cFile(void); + ~cFile(); + operator int () { return f; } + bool Open(const char *FileName, int Flags, mode_t Mode = DEFFILEMODE); + bool Open(int FileDes); + void Close(); + bool IsOpen(void) { return f >= 0; } + bool Ready(bool Wait = true); + static bool AnyFileReady(int FileDes = -1, int TimeoutMs = 1000); + static bool FileReady(int FileDes, int TimeoutMs = 1000); + static bool FileReadyForWriting(int FileDes, int TimeoutMs = 1000); +}; + +class cTimeMs { +private: + uint64_t begin; +public: + cTimeMs(int Ms = 0); + ///< Creates a timer with ms resolution and an initial timeout of Ms. + ///< If Ms is negative the timer is not initialized with the current + ///< time. + static uint64_t Now(void); + void Set(int Ms = 0); + bool TimedOut(void); + uint64_t Elapsed(void); + }; + +#endif // VDR_PLUGIN + + +//*************************************************************************** +// Line +//*************************************************************************** + +class cLine : public cListObject +{ + public: + + cLine(const char *s) { line = s ? strdup(s) : 0; }; + virtual ~cLine() { if (line) free(line); }; + + const char* Text() { return line; } + int Length() { return strlen(line); } + + private: + + char* line; +}; + +//*************************************************************************** +// SVDRP Client +//*************************************************************************** + +class cSvdrpClient +{ + private: + + char* ip; + int port; + cFile file; + char* buffer; + int bufSize; + + int connect(); + int readLine(int timeoutMs); + + public: + + cSvdrpClient(const char *aIp, int aPort); + virtual ~cSvdrpClient(); + + int open(); + void close(int sendQuit = yes); + void abort(); + + int send(const char* cmd, int reconnect = true); + int receive(cList<cLine>* list = 0, int timeoutMs = 20 * 1000); +}; + +//*************************************************************************** +#endif // __SVDRP_CLIENT_H_ @@ -0,0 +1,679 @@ +/* + * timer.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <vdr/tools.h> +#include <vdr/menu.h> + +#include "update.h" +#include "ttools.h" +#include "service.h" + +//*************************************************************************** +// Has Timer Changed +//*************************************************************************** + +int cUpdate::timerChanged() +{ + int maxSp = 0; + int changed = no; + + selectMaxUpdSp->execute(); + maxSp = timerDb->getIntValue("UPDSP"); + + if (timersTableMaxUpdsp != maxSp) + { + timersTableMaxUpdsp = maxSp; + changed = yes; + + cPluginManager::CallAllServices(EPG2VDR_TIMER_UPDATED, &timersTableMaxUpdsp); + } + + selectMaxUpdSp->freeResult(); + + return changed; +} + +//*************************************************************************** +// Perform Timer Jobs +//*************************************************************************** + +int cUpdate::performTimerJobs() +{ + int createCount = 0; + int modifyCount = 0; + int deleteCount = 0; + uint64_t start = cTimeMs::Now(); + + tell(1, "Checking pending timer actions .."); + + // check if timer pending + { + timerDb->clear(); + timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + + if (!selectPendingTimerActions->find()) + { + selectPendingTimerActions->freeResult(); + tell(1, ".. nothing to do"); + timerJobsUpdateTriggered = no; + return done; + } + + selectPendingTimerActions->freeResult(); + } + + // get timers lock + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cTimersLock timersLock(true); + cTimers* timers = timersLock.Timers(); +#else + cTimers* timers = &Timers; +#endif + + // get schedules lock + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cStateKey schedulesKey; + const cSchedules* schedules = cSchedules::GetSchedulesRead(schedulesKey, 100/*ms*/); +#else + cSchedulesLock* schedulesLock = new cSchedulesLock(false, 100/*ms*/); + const cSchedules* schedules = (cSchedules*)cSchedules::Schedules(*schedulesLock); +#endif + + if (!schedules) + { +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + schedulesKey.Remove(); +#else + delete schedulesLock; +#endif + tell(0, "Info: Can't get lock on schedules, skipping timer update!"); + return fail; + } + + // iterate pending actions ... + + timerDb->clear(); + timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + + for (int f = selectPendingTimerActions->find(); f && dbConnected(); f = selectPendingTimerActions->fetch()) + { + int timerid = timerDb->getIntValue("ID"); + int doneid = na; + long eventid = timerDb->getValue("EVENTID")->isNull() ? na : timerDb->getIntValue("EVENTID"); + tChannelID channelId = tChannelID::FromString(timerDb->getStrValue("CHANNELID")); + const cEvent* event = 0; + char requetedAction = timerDb->getStrValue("ACTION")[0]; + cTimer* timer = getTimerById(timers, timerid); // lookup VDRs timer object + int insert = !timer; + + if (!timerDb->getValue("DONEID")->isEmpty()) + doneid = timerDb->getIntValue("DONEID"); + + tell(1, "DEBUG: Pending Action '%c' for timer (%d), event %ld, doneid %d", requetedAction, timerid, eventid, doneid); + + // -------------------------------- + // Delete timer request + + if (requetedAction == taDelete || requetedAction == taReject) + { + if (timerDb->hasValue("VDRUUID", "any")) + { + tell(0, "Error: Ignoring delete/reject request of timer (%d) without VDRUUID", timerid); + timerDb->getValue("INFO")->sPrintf("Error: Ignoring delete/reject request of timer (%d) without VDRUUID", timerid); + timerDb->setCharValue("ACTION", taFailed); + timerDb->setCharValue("STATE", tsError); + timerDb->update(); + updateTimerDone(timerid, doneid, tdsTimerCreateFailed); + continue; + } + + // delete VDRs timer + + if (timer) + { + if (timer->Recording()) + { + timer->Skip(); +#if defined (APIVERSNUM) && (APIVERSNUM >= 20302) + cRecordControls::Process(timers, time(0)); +#else + cRecordControls::Process(time(0)); +#endif + } + + timers->Del(timer); + deleteCount++; + tell(0, "Deleted timer %d", timerid); + } + else + { + tell(0, "Info: Timer (%d) not found, ignoring delete/reject request", timerid); + } + + updateTimerDone(timerid, doneid, requetedAction == taDelete ? tdsTimerDeleted : tdsTimerRejected); + timerDb->setCharValue("ACTION", taAssumed); + timerDb->setCharValue("STATE", tsDeleted); + timerDb->update(); + } + + // -------------------------------- + // Create or Modify timer request + + else if (requetedAction == taModify || requetedAction == taAdjust || requetedAction == taCreate) + { + cSchedule* s = 0; + + if (!timer && (requetedAction == taModify || requetedAction == taAdjust)) + { + tell(0, "Fatal: Timer (%d) not found, skipping modify request", timerid); + + timerDb->getValue("INFO")->sPrintf("Fatal: Timer (%d) not found, skipping modify request", timerid); + timerDb->setCharValue("ACTION", taFailed); + timerDb->setCharValue("STATE", tsError); + timerDb->update(); + updateTimerDone(timerid, doneid, tdsTimerCreateFailed); + continue; + } + + // get schedule (channel) and optional the event + + if (!(s = (cSchedule*)schedules->GetSchedule(channelId))) + { +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cChannelsLock channelsLock(false); + const cChannels* channels = channelsLock.Channels(); +#else + cChannels* channels = &Channels; +#endif + const cChannel* channel = channels->GetByChannelID(channelId); + + tell(0, "Error: Time (%d), missing channel '%s' (%s) or channel not found, ignoring request", + timerid, channel ? channel->Name() : "", timerDb->getStrValue("CHANNELID")); + + timerDb->getValue("INFO")->sPrintf("Error: Timer, (%d), missing channel '%s' (%s) or channel not found, ignoring request", + timerid, channel ? channel->Name() : "", timerDb->getStrValue("CHANNELID")); + timerDb->setCharValue("ACTION", taFailed); + timerDb->setCharValue("STATE", tsError); + timerDb->update(); + updateTimerDone(timerid, doneid, tdsTimerCreateFailed); + + // mark this channel as 'unknown' + + mapDb->clear(); + mapDb->setValue("CHANNELID", channelId.ToString()); + markUnknownChannel->execute(); + markUnknownChannel->freeResult(); + continue; + } + + if (eventid > 0 && !(event = s->GetEvent(eventid))) + { + { +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cChannelsLock channelsLock(false); + const cChannels* channels = channelsLock.Channels(); +#else + cChannels* channels = &Channels; +#endif + const cChannel* channel = channels->GetByChannelID(channelId); + + tell(0, "Error: Timer (%d), missing event '%ld' on channel '%s' (%s), ignoring request", + timerid, eventid, channel->Name(), timerDb->getStrValue("CHANNELID")); + + timerDb->getValue("INFO")->sPrintf("Error: Timer (%d), missing event '%ld' on channel '%s' (%s), ignoring request", + timerid, eventid, channel->Name(), timerDb->getStrValue("CHANNELID")); + timerDb->setCharValue("ACTION", taFailed); + timerDb->setCharValue("STATE", tsError); + timerDb->update(); + updateTimerDone(timerid, doneid, tdsTimerCreateFailed); + } + + // force reload of events + + tell(0, "Info: Trigger EPG full-reload due to missing event!"); + triggerEpgUpdate(yes); + + continue; + } + + if (insert) + { + tell(1, "DEBUG: Create of timer %d for event %ld", timerid, eventid); + + // create timer ... + + if (event) + { + // event should run at least more the 2 Minutes + + if (getTimerByEvent(timers, event)) + tell(0, "Warning: Timer for event (%d) '%s' already exist, creating additional timer due to request!", + event->EventID(), event->Title()); + + if (event->StartTime() + event->Duration() - (2 * tmeSecondsPerMinute) <= time(0)) + { + tell(0, "Info: Event '%s' finished in the past, ignoring timer request!", event->Title()); + timerDb->getValue("INFO")->sPrintf("Info: Event '%s' finished in the past, ignoring timer request!", event->Title()); + timerDb->setCharValue("ACTION", taFailed); + timerDb->setCharValue("STATE", tsError); + timerDb->update(); + updateTimerDone(timerid, doneid, tdsTimerCreateFailed); + continue; + } + + timer = new cTimer(event); + } + else + { +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; + const cChannel* channel = channels->GetByChannelID(channelId); +#else + cChannels* channels = &Channels; + cChannel* channel = channels->GetByChannelID(channelId); +#endif + // timer without a event + + timer = new cTimer(no, no, channel); + } + + // reset error message in 'reason' + + if (!timerDb->getValue("INFO")->isEmpty()) + timerDb->setValue("INFO", ""); + + tell(1, "Create timer '%d'", timerid); + + if (timerDb->hasValue("VDRUUID", "any")) + { + tell(0, "Took timer (%d) for uuid 'any', event (%ld)", timerid, eventid); + + // mark 'na' record as assumed and create new with my UUID in primary key + + timerDb->setCharValue("ACTION", taAssumed); + timerDb->setCharValue("STATE", tsIgnore); + + timerDb->update(); + + // I take the timer -> new record will created! + + timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + timerDb->insert(); + + // update timerid !!! + + timerid = timerDb->getLastInsertId(); + timerDb->setValue("ID", timerid); + } + } + else + { + // modify timer ... + + if (timerDb->hasValue("VDRUUID", "any")) + { + tell(0, "Error: Ignoring modify request of timer (%d) without VDRUUID", timerid); + timerDb->getValue("INFO")->sPrintf("Error: Ignoring modify request of timer (%d) without VDRUUID", timerid); + timerDb->setCharValue("ACTION", taFailed); + timerDb->setCharValue("STATE", tsError); + timerDb->update(); + updateTimerDone(timerid, doneid, tdsTimerCreateFailed); + continue; + } + + tell(1, "Modify timer (%d), set event to (%d)", timerid, event->EventID()); + +// #TODO ?!?! +// if (event && timer->Event() != event) +// timer->SetEvent(event); + } + + // update the timer with data from timers table ... + + updateTimerObjectFromRow(timer, timerDb->getRow(), event); + + // add / store ... + + if (insert) + { + timers->Add(timer); + updateTimerDone(timerid, doneid, tdsTimerCreated); + createCount++; + } + else + { + if (requetedAction == taAdjust && event) + { + // adjust time to given event .. + + cTimer* dummyTimer = new cTimer(event); + timer->SetStart(dummyTimer->Start()); + timer->SetStop(dummyTimer->Stop()); + timer->SetDay(dummyTimer->Day()); + delete dummyTimer; + } + + modifyCount++; + } + + timerDb->setValue("AUX", timer->Aux()); + timerDb->setCharValue("ACTION", taAssumed); + timerDb->setCharValue("STATE", tsPending); + timerDb->store(); + } + } + + selectPendingTimerActions->freeResult(); + + // schedules lock freigeben + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + schedulesKey.Remove(); +#else + delete schedulesLock; +#endif + + if (createCount || modifyCount || deleteCount) + timers->SetModified(); + + timerJobsUpdateTriggered = no; + + tell(0, "Timer requests done, created %d, modified %d, deleted %d in %s", + createCount, modifyCount, deleteCount, ms2Dur(cTimeMs::Now()-start).c_str()); + + return success; +} + +//*************************************************************************** +// Timer Done to Failed +//*************************************************************************** + +int cUpdate::updateTimerDone(int timerid, int doneid, char state) +{ + if (doneid == na) + return done; + + timerDoneDb->clear(); + timerDoneDb->setValue("ID", doneid); + + if (timerDoneDb->find()) + { + // don't toggle 'D'eleted by user to re'J'ected + + if (timerDoneDb->hasCharValue("STATE", tdsTimerDeleted) && state == tdsTimerRejected) + return done; + + timerDoneDb->setValue("TIMERID", timerid); + timerDoneDb->setCharValue("STATE", state); + timerDoneDb->update(); + } + else + { + tell(0, "Warning: Id (%d) in timersdone for timer (%d) not found.", + doneid, timerid); + } + + return done; +} + +//*************************************************************************** +// Update Timer Table +//*************************************************************************** + +int cUpdate::updateTimerTable() +{ + cMutexLock lock(&timerMutex); + int timerMod = 0; + int cnt = 0; + + tell(1, "Updating table timers (and remove deleted and finished timers older than 2 days)"); + + timerDb->deleteWhere("%s < unix_timestamp() - %d and %s in ('D','F')", + timerDb->getField("UPDSP")->getDbName(), + 2 * tmeSecondsPerDay, + timerDb->getField("STATE")->getDbName()); + + connection->startTransaction(); + + // -------------------------- + // get timers lock + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cTimersLock timersLock(true); + cTimers* timers = timersLock.Timers(); +#else + cTimers* timers = &Timers; +#endif + + // -------------------------- + // remove deleted timers + + timerDb->clear(); + timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + + for (int f = selectMyTimer->find(); f && dbConnected(); f = selectMyTimer->fetch()) + { + int exist = no; + + // delete only assumed timers + + if (!timerDb->getValue("ACTION")->isEmpty() && !timerDb->hasCharValue("ACTION", taAssumed)) + continue; + + // count my timers to detect truncated (epmty) table + // on empty table ignore known timer ids + + cnt++; + + for (const cTimer* t = timers->First(); t; t = timers->Next(t)) + { + int timerid = getTimerIdOf(t); + + // compare by timerid + + if (timerid != na && timerDb->getIntValue("ID") == timerid) + { + exist = yes; + break; + } + + // compare by start-time and channelid + + if (t->StartTime() == timerDb->getIntValue("StartTime") && + strcmp(t->Channel()->GetChannelID().ToString(), timerDb->getStrValue("ChannelId")) == 0) + { + exist = yes; + break; + } + } + + if (!exist) + { + int doneid = timerDb->getIntValue("DONEID"); + + if (!timerDb->hasCharValue("STATE", tsFinished)) + { + timerDb->setCharValue("STATE", tsDeleted); + timerDb->update(); + } + + if (doneid > 0) + { + timerDoneDb->clear(); + timerDoneDb->setValue("ID", doneid); + + if (timerDoneDb->find() && + (timerDoneDb->hasCharValue("STATE", tdsTimerCreated) || timerDoneDb->hasCharValue("STATE", tdsTimerRequested))) + { + timerDoneDb->setCharValue("STATE", tdsTimerDeleted); + timerDoneDb->update(); + } + } + } + } + + selectMyTimer->freeResult(); + + if (!cnt && timers->Count()) + tell(0, "No timer of my uuid found, assuming cleared table and ignoring the known timerids"); + + // -------------------------- + // update timers + + for (cTimer* t = timers->First(); t && dbConnected(); t = timers->Next(t)) + { + int insert = yes; + int timerId = getTimerIdOf(t); + + // no timer id or not in table -> handle as insert! + + timerDb->clear(); + + if (timerId != na && cnt) + { + timerDb->setValue("ID", timerId); + timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + + insert = !timerDb->find(); + timerDb->clearChanged(); + } + + updateRowByTimer(timerDb->getRow(), t); + + if (insert) + { + timerDb->setCharValue("STATE", tsPending); + timerDb->insert(); + + // get timerid of auto increment field and store to timers 'aux' data + + timerId = timerDb->getLastInsertId(); + setTimerId(t, timerId); + timerMod++; + } + else if (!timerDb->getValue("STATE")->isEmpty() && strchr("DE", timerDb->getStrValue("STATE")[0])) + { + timerDb->setValue("STATE", "P"); // timer pending + } + + if (insert || timerDb->getChanges()) + { + timerDb->setValue("ID", timerId); // set ID for update!! + timerDb->update(); // at least for aux (on insert case) + + tell(1, "'%s' timer for event %u '%s' at database", + insert ? "Insert" : "Update", + t->Event() ? t->Event()->EventID() : na, + t->Event() ? t->Event()->Title() : t->File()); + } + else + { + tell(3, "Nothing changed ... skipping db update"); + } + + timerDb->reset(); + } + + connection->commit(); + timerTableUpdateTriggered = no; + + if (timerMod) + timers->SetModified(); + + tell(1, "Updating table timers done"); + + return success; +} + +//*************************************************************************** +// Recording Changed +//*************************************************************************** + +int cUpdate::recordingChanged() +{ + cMutexLock lock(&runningRecMutex); + + cRunningRecording* rr = runningRecordings.First(); + + tell(3, "recording changed; rr = %p", (void*)rr); + + while (rr && dbConnected()) + { + int timerWasDeleted = no; + + // first: update timer state .. + + if (!isEmpty(rr->aux)) + { + int timerid = getTimerIdOf(rr->aux); + timerDb->clear(); + timerDb->setValue("ID", timerid); + timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid); + + tell(1, "Try to lookup timer with id %d", timerid); + + if (timerDb->find()) + { + tell(1, "Found timer %ld", timerDb->getIntValue("ID")); + + if (timerDb->hasCharValue("STATE", tsDeleted)) + timerWasDeleted = yes; + else + timerDb->setCharValue("STATE", rr->failed ? tsError : rr->finished ? tsFinished : tsRunning); + + timerDb->setValue("INFO", rr->info); + timerDb->store(); + } + else + tell(0, "Error: Can't lookup timer, id %d not found!", timerid); + + timerDb->reset(); + } + + // second: update done entry and remove from 'running' if 'finished' .. + + if (rr->finished) + { + if (rr->doneid != na) + { + timerDoneDb->clear(); + timerDoneDb->setValue("ID", rr->doneid); + + if (timerDoneDb->find()) + { + if (timerWasDeleted) + timerDoneDb->setCharValue("STATE", tdsTimerDeleted); // -> Recording deleted + else if (rr->failed) + timerDoneDb->setCharValue("STATE", tdsRecordingFailed); // -> Recording failed + else + timerDoneDb->setCharValue("STATE", tdsRecordingDone); // -> Recording done + + tell(1, "Update 'done state' for doneid %ld to %s", rr->doneid, timerDoneDb->getStrValue("STATE")); + + timerDoneDb->update(); + } + else + { + tell(0, "Error: Can't update timersdone state, doneid %ld not found!", rr->doneid); + } + } + + cRunningRecording* rrNext = runningRecordings.Next(rr); + runningRecordings.Del(rr); + rr = rrNext; + + continue; + } + + rr = runningRecordings.Next(rr); + } + + return done; +} diff --git a/ttools.c b/ttools.c new file mode 100644 index 0000000..c30f960 --- /dev/null +++ b/ttools.c @@ -0,0 +1,583 @@ +/* + * ttools.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <string> + +#include "update.h" +#include "ttools.h" + +using namespace std; + +//*************************************************************************** +// Content Of Tag +//*************************************************************************** + +int contentOfTag(const char* tag, const char* xml, char* buf, int size) +{ + string sTag = "<" + string(tag) + ">"; + string eTag = "</" + string(tag) + ">"; + + const char* s; + const char* e; + + if (buf) + *buf = 0; + + if ((s = strstr(xml, sTag.c_str())) && (e = strstr(xml, eTag.c_str()))) + { + s += strlen(sTag.c_str()); + + if (buf) + sprintf(buf, "%.*s", (int)(e-s), s); + + return success; + } + + return fail; +} + +//*************************************************************************** +// Content Value Of Tag +//*************************************************************************** + +int contentOfTag(const cTimer* timer, const char* tag, char* buf, int size) +{ + char epgaux[512+TB]; + + if (!timer || isEmpty(timer->Aux())) + return fail; + + if (contentOfTag("epgd", timer->Aux(), epgaux, 512) != success) + return fail; + + if (contentOfTag(tag, epgaux, buf, size) != success) + return fail; + + return success; +} + +int contentOfTag(const cTimer* timer, const char* tag, int& value) +{ + char epgaux[512+TB]; + char buf[100+TB]; + + if (!timer || isEmpty(timer->Aux())) + return fail; + + if (contentOfTag("epgd", timer->Aux(), epgaux, 512) != success) + return fail; + + if (contentOfTag(tag, epgaux, buf, 100) != success) + return fail; + + value = atoi(buf); + + return success; +} + +//*************************************************************************** +// Get Timer Id Of +//*************************************************************************** + +int getTimerIdOf(const cTimer* timer) +{ + char tid[100+TB]; + + if (!timer || isEmpty(timer->Aux())) + return na; + + if (contentOfTag(timer, "timerid", tid, 100) != success) + return na; + + return atoi(tid); +} + +int getTimerIdOf(const char* aux) +{ + char tid[100+TB]; + char epgaux[512+TB]; + + if (isEmpty(aux)) + return na; + + if (contentOfTag("epgd", aux, epgaux, 512) != success) + return fail; + + if (contentOfTag("timerid", epgaux, tid, 100) != success) + return fail; + + return atoi(tid); +} + +//*************************************************************************** +// Remove Tag +//*************************************************************************** + +void removeTag(char* xml, const char* tag) +{ + string sTag = "<" + string(tag) + ">"; + string eTag = "</" + string(tag) + ">"; + + const char* s; + const char* e; + + if ((s = strstr(xml, sTag.c_str())) && (e = strstr(xml, eTag.c_str()))) + { + char tmp[10000+TB]; + + e += strlen(eTag.c_str()); + + // sicher ist sicher ;) + + if (e <= s) + return; + + sprintf(tmp, "%.*s%s", int(s-xml), xml, e); + + strcpy(xml, tmp); + } +} + +//*************************************************************************** +// Insert Tag +//*************************************************************************** + +int insertTag(char* xml, const char* parent, const char* tag, int value) +{ + char* tmp; + string sTag = "<" + string(parent) + ">"; + const char* s; + + if ((s = strstr(xml, sTag.c_str()))) + { + s += strlen(sTag.c_str()); + asprintf(&tmp, "%.*s<%s>%d</%s>%s", int(s-xml), xml, tag, value, tag, s); + } + else + { + asprintf(&tmp, "%s<%s><%s>%d</%s></%s>", xml, parent, tag, value, tag, parent); + } + + strcpy(xml, tmp); + free(tmp); + + return success; +} + +int insertTag(char* xml, const char* parent, const char* tag, const char* value) +{ + char* tmp; + string sTag = "<" + string(parent) + ">"; + const char* s; + + if ((s = strstr(xml, sTag.c_str()))) + { + s += strlen(sTag.c_str()); + asprintf(&tmp, "%.*s<%s>%s</%s>%s", int(s-xml), xml, tag, value, tag, s); + } + else + { + asprintf(&tmp, "%s<%s><%s>%s</%s></%s>", xml, parent, tag, value, tag, parent); + } + + strcpy(xml, tmp); + free(tmp); + + return success; +} + +//*************************************************************************** +// Set Tag To +//*************************************************************************** + +int setTagTo(cTimer* timer, const char* tag, int value) +{ + char aux[10000+TB] = ""; + + if (!isEmpty(timer->Aux())) + strcpy(aux, timer->Aux()); + + removeTag(aux, tag); + insertTag(aux, "epgd", tag, value); + + timer->SetAux(aux); + + return done; +} + +int setTagTo(cTimer* timer, const char* tag, const char* value) +{ + char aux[10000+TB] = ""; + + if (!isEmpty(timer->Aux())) + strcpy(aux, timer->Aux()); + + removeTag(aux, tag); + insertTag(aux, "epgd", tag, value); + + timer->SetAux(aux); + + return done; +} + +//*************************************************************************** +// Set Timer Id +//*************************************************************************** + +int setTimerId(cTimer* timer, int tid) +{ + char aux[10000+TB] = ""; + + if (!isEmpty(timer->Aux())) + strcpy(aux, timer->Aux()); + + // remove old timerid - if exist + + removeTag(aux, "timerid"); + insertTag(aux, "epgd", "timerid", tid); + + timer->SetAux(aux); + + return done; +} + +//*************************************************************************** +// Get Timer By Id +//*************************************************************************** + +cTimer* getTimerById(cTimers* timers, int timerid) +{ + for (cTimer* t = timers->First(); t; t = timers->Next(t)) + if (timerid == getTimerIdOf(t)) + return t; + + return 0; +} + +//*************************************************************************** +// Get Timer By Event +//*************************************************************************** + +cTimer* getTimerByEvent(cTimers* timers, const cEvent* event) +{ + cTimer* timer = 0; + + eTimerMatch tm = tmNone; + + timer = timers->GetMatch(event, &tm); + + if (tm != tmFull) + timer = 0; + + return timer; +} + +//*************************************************************************** +// New Row From Event +//*************************************************************************** + +cDbRow* newTimerRowFromEvent(const cEvent* event) +{ + cTimer* timer = new cTimer(event); + cDbRow* timerRow = newRowFromTimer(timer); + + delete timer; + + return timerRow; +} + +//*************************************************************************** +// New Row From Timer +//*************************************************************************** + +cDbRow* newRowFromTimer(const cTimer* timer) +{ + cDbRow* timerRow = new cDbRow("timers"); + + updateRowByTimer(timerRow, timer); + + return timerRow; +} + +//*************************************************************************** +// Update Row By Timer +//*************************************************************************** + +int updateRowByTimer(cDbRow* timerRow, const cTimer* t) +{ + int autotimerid = na; + int autotimerinssp = na; + int doneid = na; + int namingmode = na; + char tmplExpression[100+TB] = ""; + int childLock = no; + int epgs = no; + char directory[512+TB] = ""; + char source[40+TB] = ""; + cString channelId = t->Event() ? t->Event()->ChannelID().ToString() : t->Channel()->GetChannelID().ToString(); + + contentOfTag(t, "autotimerid", autotimerid); + contentOfTag(t, "autotimerinssp", autotimerinssp); + contentOfTag(t, "doneid", doneid); + contentOfTag(t, "namingmode", namingmode); + contentOfTag(t, "template", tmplExpression, 100); + contentOfTag(t, "directory", directory, 512); + contentOfTag(t, "source", source, 40); + + timerRow->setValue("VDRUUID", Epg2VdrConfig.uuid); + timerRow->setValue("EVENTID", t->Event() ? (long)t->Event()->EventID() : 0); + timerRow->setValue("_STARTTIME", t->Event() ? t->Event()->StartTime() : 0); + timerRow->setValue("CHANNELID", channelId); + timerRow->setValue("DAY", t->Day()); + timerRow->setValue("STARTTIME", t->Start()); + timerRow->setValue("ENDTIME", t->Stop()); + timerRow->setValue("WEEKDAYS", t->WeekDays()); + timerRow->setValue("PRIORITY", t->Priority()); + timerRow->setValue("LIFETIME", t->Lifetime()); + timerRow->setValue("VPS", t->HasFlags(tfVps) ? yes : no); + timerRow->setValue("ACTIVE", t->HasFlags(tfActive) ? yes : no); + + timerRow->setValue("DIRECTORY", directory); + + if (!isEmpty(directory) && strncmp(t->File(), directory, strlen(directory)) == 0) + { + int len = strlen(directory); + + if (t->File()[len] == '~') + len++; + + timerRow->setValue("FILE", t->File()+len); + } + else + timerRow->setValue("FILE", t->File()); + + if (namingmode != na) + timerRow->setValue("NAMINGMODE", namingmode); + + if (!isEmpty(tmplExpression)) + timerRow->setValue("TEMPLATE", tmplExpression); + + if (autotimerinssp != na) + timerRow->setValue("AUTOTIMERINSSP", autotimerinssp); + + if (autotimerid != na) + timerRow->setValue("AUTOTIMERID", autotimerid); + + if (doneid != na) + timerRow->setValue("DONEID", doneid); + + // AUX: "<epgsearch><channel>62 - TNTSerieHD</channel><searchtimer>Falling Skies</searchtimer><start>1411498680</start><stop>1411502100</stop><s-id>3</s-id><eventid>565129</eventid></epgsearch> + // AUX: <epgd>......</epgd><pin-plugin><protected>yes</protected></pin-plugin>" + + if (t->Aux()) + { + epgs = strstr(t->Aux(), "<epgsearch>") != 0; + childLock = strstr(t->Aux(), "<pin-plugin><protected>yes</protected></pin-plugin>") != 0; + } + + timerRow->setValue("SOURCE", !isEmpty(source) ? source : epgs ? "epgs" : Epg2VdrConfig.uuid); + timerRow->setValue("CHILDLOCK", childLock); + timerRow->setValue("AUX", t->Aux()); // update aux also in table + + return done; +} + +//*************************************************************************** +// New Timer From Row +//*************************************************************************** + +cEpgTimer* newTimerObjectFromRow(cDbRow* timerRow, cDbRow* vdrRow) +{ + cEpgTimer* timer = 0; + const cEvent* event = 0; + cString buf; + tChannelID channelId = tChannelID::FromString(timerRow->getStrValue("CHANNELID")); + uint flags = tfNone; + char* file; + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; + const cChannel* channel = channels->GetByChannelID(channelId); +#else + cChannels* channels = &Channels; + const cChannel* channel = channels->GetByChannelID(channelId); +#endif + + const char* dir = timerRow->getStrValue("DIRECTORY"); + + file = strdup(timerRow->getStrValue("FILE")); + strReplace(file, ':', '|'); + + if (timerRow->getIntValue("VPS")) + flags |= tfVps; + + if (timerRow->getIntValue("ACTIVE")) + flags |= tfActive; + + if (timerRow->hasCharValue("STATE", tsRunning)) + flags |= tfRecording; + + timer = new cEpgTimer(no, no, channel); + + buf = cString::sprintf("%d:%d:%s:%04d:%04d:%d:%d:%s%s%s:%s", + flags, + channel ? channel->Number() : 0, + (const char*)cTimer::PrintDay(timerRow->getIntValue("DAY"), + timerRow->getIntValue("WEEKDAYS"), yes), + (int)timerRow->getIntValue("STARTTIME"), + (int)timerRow->getIntValue("ENDTIME"), + (int)timerRow->getIntValue("PRIORITY"), + (int)timerRow->getIntValue("LIFETIME"), + !isEmpty(dir) ? dir : "", + !isEmpty(dir) && !isEmpty(file) ? "~" : "", + file, + timerRow->getStrValue("AUX")); + + free(file); + timer->Parse(buf); + + if (channel) + { +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cStateKey schedulesKey; + const cSchedules* schedules = cSchedules::GetSchedulesRead(schedulesKey); +#else + cSchedulesLock* schedulesLock = new cSchedulesLock(false); + const cSchedules* schedules = (cSchedules*)cSchedules::Schedules(*schedulesLock); +#endif + if (schedules) + { + const cSchedule* schedule = schedules->GetSchedule(channel); + + if (schedule) + event = schedule->GetEvent(timerRow->getIntValue("EVENTID")); + } + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + if (schedules) schedulesKey.Remove(); +#else + delete schedulesLock; +#endif + } + + if (event) + timer->SetEvent(event); + + timer->setTimerId(timerRow->getIntValue("ID")); + timer->setEventId(timerRow->getIntValue("EVENTID")); + timer->setAction(!timerRow->getValue("ACTION")->isNull() ? timerRow->getStrValue("ACTION")[0] : ' '); + timer->setVdr(vdrRow->getStrValue("NAME"), vdrRow->getStrValue("UUID"), vdrRow->hasValue("STATE", "attached")); + timer->setState(!timerRow->getValue("STATE")->isNull() ? timerRow->getStrValue("STATE")[0] : ' ', timerRow->getStrValue("INFO")); + +#ifdef WITH_PIN + timer->SetFskProtection(timerRow->getIntValue("CHILDLOCK")); +#endif + + return timer; +} + +//*************************************************************************** +// update Timer Object From Row +//*************************************************************************** + +int updateTimerObjectFromRow(cTimer* timer, cDbRow* timerRow, const cEvent* event) +{ + if (!timerRow->getValue("FILE")->isEmpty()) + { + string path = ""; + + if (!timerRow->getValue("DIRECTORY")->isEmpty()) + path = timerRow->getStrValue("DIRECTORY") + string("~"); + + if (!timerRow->getValue("FILE")->isEmpty()) + path += timerRow->getStrValue("FILE"); + + timer->SetFile(path.c_str()); + } + else if (!timerRow->getValue("DIRECTORY")->isEmpty()) + { + string path = timerRow->getStrValue("DIRECTORY") + string("~") + string(timer->File()); + timer->SetFile(path.c_str()); + } + else if (!event) + { +#if APIVERSNUM >= 20301 + LOCK_CHANNELS_READ; + const cChannels* channels = Channels; +#else + cChannels* channels = &Channels; +#endif + + tChannelID channelId = tChannelID::FromString(timerRow->getStrValue("CHANNELID")); + const cChannel* channel = channels->GetByChannelID(channelId); + + timer->SetFile(channel->Name()); + tell(0, "Missing file, using channel name instead '%s'", channel->Name()); + } + + if (!timerRow->getValue("DAY")->isNull()) + timer->SetDay(timerRow->getIntValue("DAY")); + + if (!timerRow->getValue("WEEKDAYS")->isNull()) + timer->SetWeekDays(timerRow->getIntValue("WEEKDAYS")); + + if (!timerRow->getValue("STARTTIME")->isNull()) + timer->SetStart(timerRow->getIntValue("STARTTIME")); + + if (!timerRow->getValue("ENDTIME")->isNull()) + timer->SetStop(timerRow->getIntValue("ENDTIME")); + + if (!timerRow->getValue("PRIORITY")->isNull()) + timer->SetPriority(timerRow->getIntValue("PRIORITY")); + + if (!timerRow->getValue("LIFETIME")->isNull()) + timer->SetLifetime(timerRow->getIntValue("LIFETIME")); + + if (!timerRow->getValue("VPS")->isNull()) + { + if (timerRow->getIntValue("VPS")) + timer->SetFlags(tfVps); + else + timer->ClrFlags(tfVps); + } + + if (timerRow->getIntValue("ACTIVE")) + timer->SetFlags(tfActive); + else + timer->ClrFlags(tfActive); + + setTagTo(timer, "timerid", timerRow->getIntValue("ID")); + + if (!timerRow->getValue("SOURCE")->isNull()) + setTagTo(timer, "source", timerRow->getStrValue("SOURCE")); + + if (!timerRow->getValue("DIRECTORY")->isEmpty()) + setTagTo(timer, "directory", timerRow->getStrValue("DIRECTORY")); + + if (!timerRow->getValue("NAMINGMODE")->isNull()) + setTagTo(timer, "namingmode", timerRow->getIntValue("NAMINGMODE")); + + if (!timerRow->getValue("TEMPLATE")->isNull()) + setTagTo(timer, "template", timerRow->getStrValue("TEMPLATE")); + + if (!timerRow->getValue("AUTOTIMERID")->isNull()) + setTagTo(timer, "autotimerid", timerRow->getIntValue("AUTOTIMERID")); + + if (!timerRow->getValue("DONEID")->isNull()) + setTagTo(timer, "doneid", timerRow->getIntValue("DONEID")); + + if (!timerRow->getValue("AUTOTIMERINSSP")->isNull()) + setTagTo(timer, "autotimerinssp", timerRow->getStrValue("AUTOTIMERINSSP")); + + if (!timerRow->getValue("EXPRESSION")->isNull()) + setTagTo(timer, "expression", timerRow->getStrValue("EXPRESSION")); + + timer->Matches(); // adjust times of timer + + return done; +} diff --git a/ttools.h b/ttools.h new file mode 100644 index 0000000..07f9382 --- /dev/null +++ b/ttools.h @@ -0,0 +1,42 @@ +/* + * ttools.h: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef _TTOOLS_H_ +#define _TTOOLS_H_ + +#include <vdr/timers.h> + +#include "service.h" + +//*************************************************************************** +// Timer Tools +//*************************************************************************** + +int contentOfTag(const char* tag, const char* xml, char* buf, int size); +int contentOfTag(const cTimer* timer, const char* tag, char* buf, int size); +int contentOfTag(const cTimer* timer, const char* tag, int& value); +int getTimerIdOf(const cTimer* timer); +int getTimerIdOf(const char* aux); +void removeTag(char* xml, const char* tag); +int insertTag(char* xml, const char* parent, const char* tag, int value); +int insertTag(char* xml, const char* parent, const char* tag, const char* value); +int setTagTo(cTimer* timer, const char* tag, int value); +int setTagTo(cTimer* timer, const char* tag, const char* value); +int setTimerId(cTimer* timer, int tid); +cTimer* getTimerById(cTimers* timers, int timerid); +cTimer* getTimerByEvent(cTimers* timers, const cEvent* event); + +cDbRow* newTimerRowFromEvent(const cEvent* event); +cDbRow* newRowFromTimer(const cTimer* t); +int updateRowByTimer(cDbRow* timerDb, const cTimer* t); + +cEpgTimer* newTimerObjectFromRow(cDbRow* timerRow, cDbRow* vdrRow); +int updateTimerObjectFromRow(cTimer* timer, cDbRow* timerRow, const cEvent* event); + +//*************************************************************************** + +#endif // _TTOOLS_H_ diff --git a/update.c b/update.c new file mode 100644 index 0000000..e789c8d --- /dev/null +++ b/update.c @@ -0,0 +1,1822 @@ +/* + * update.c: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#include <locale.h> + +#include <vdr/videodir.h> +#include <vdr/tools.h> + +#include "epg2vdr.h" +#include "update.h" +#include "handler.h" + +//*************************************************************************** +// ctor +//*************************************************************************** + +cUpdate::cUpdate(cPluginEPG2VDR* aPlugin) + : cThread("epg2vdr-update") +{ + // thread / update control + + plugin = aPlugin; + connection = 0; + loopActive = no; + timerJobsUpdateTriggered = yes; + timerTableUpdateTriggered = yes; + recordingStateChangedTrigger = yes; + updateRecFolderOptionTrigger = no; + + storeAllRecordingInfoFilesTrigger = no; + recordingFullReloadTrigger = no; + manualTrigger = no; + videoBasePath = 0; + dbReconnectTriggered = no; + + fullreload = no; + epgdBusy = yes; + epgdState = cEpgdState::esUnknown; + mainActPending = no; + eventsPending = no; + nextEpgdUpdateAt = 0; + + lastUpdateAt = 0; + lastEventsUpdateAt = 0; + lastRecordingCount = 0; + lastRecordingDeleteAt = 0; + timersTableMaxUpdsp = 0; + + // + + compDb = 0; + eventsDb = 0; + useeventsDb = 0; + fileDb = 0; + imageDb = 0; + imageRefDb = 0; + episodeDb = 0; + vdrDb = 0; + mapDb = 0; + timerDb = 0; + timerDoneDb = 0; + recordingDirDb = 0; + recordingListDb = 0; + + selectMasterVdr = 0; + selectAllImages = 0; + selectUpdEvents = 0; + selectEventById = 0; + selectAllChannels = 0; + selectChannelById = 0; + markUnknownChannel = 0; + selectComponentsOf = 0; + deleteTimer = 0; + selectMyTimer = 0; + selectRecordings = 0; + selectRecForInfoUpdate = 0; + selectTimerByEvent = 0; + selectTimerById = 0; + selectTimerByDoneId = 0; + selectMaxUpdSp = 0; + selectPendingTimerActions = 0; + + dvbDescription = 0; + + // + + epgimagedir = 0; + withutf8 = no; + handlerMaster = no; + + // check/create uuid + + if (isEmpty(Epg2VdrConfig.uuid)) + { + sstrcpy(Epg2VdrConfig.uuid, getUniqueId(), sizeof(Epg2VdrConfig.uuid)); + plugin->SetupStore("Uuid", Epg2VdrConfig.uuid); + Setup.Save(); + + tell(0, "Initially created uuid '%s'", Epg2VdrConfig.uuid); + } +} + +//*************************************************************************** +// dtor +//*************************************************************************** + +cUpdate::~cUpdate() +{ + if (loopActive) + Stop(); + + free(epgimagedir); +} + +//*************************************************************************** +// Init +//*************************************************************************** + +int cUpdate::init() +{ + char* dictPath = 0; + char* pdir; + char* lang; + + lang = setlocale(LC_CTYPE, 0); + + if (lang) + { + tell(0, "Set locale to '%s'", lang); + + if ((strcasestr(lang, "UTF-8") != 0) || (strcasestr(lang, "UTF8") != 0)) + { + tell(0, "detected UTF-8"); + withutf8 = yes; + } + } + else + { + tell(0, "Reseting locale for LC_CTYPE failed."); + } + + strcpy(imageExtension, "jpg"); + asprintf(&epgimagedir, "%s/epgimages", EPG2VDR_DATA_DIR); + asprintf(&pdir, "%s/images", epgimagedir); + + if (!(DirectoryOk(pdir) || MakeDirs(pdir, true))) + tell(0, "could not access or create Directory %s", pdir); + + free(pdir); + + // initialize the dictionary + + asprintf(&dictPath, "%s/epg.dat", cPlugin::ConfigDirectory("epg2vdr/")); + + if (dbDict.in(dictPath) != success) + { + tell(0, "Fatal: Dictionary not loaded, aborting!"); + return fail; + } + + tell(0, "Dictionary '%s' loaded", dictPath); + free(dictPath); + + // init database ... + + cDbConnection::setEncoding(withutf8 ? "utf8" : "latin1"); // mysql uses latin1 for ISO8851-1 + cDbConnection::setHost(Epg2VdrConfig.dbHost); + cDbConnection::setPort(Epg2VdrConfig.dbPort); + cDbConnection::setName(Epg2VdrConfig.dbName); + cDbConnection::setUser(Epg2VdrConfig.dbUser); + cDbConnection::setPass(Epg2VdrConfig.dbPass); + cDbConnection::setConfPath(cPlugin::ConfigDirectory("epg2vdr/")); + + videoBasePath = cVideoDirectory::Name(); + + return success; +} + +//*************************************************************************** +// Exit +//*************************************************************************** + +int cUpdate::exit() +{ + exitDb(); + + return success; +} + +cDbFieldDef imageSizeDef("image", "image", cDBS::ffUInt, 0, cDBS::ftData); + +//*************************************************************************** +// Init/Exit Database Connections +//*************************************************************************** + +int cUpdate::initDb() +{ + int status = success; + + if (!connection) + connection = new cDbConnection(); + + vdrDb = new cDbTable(connection, "vdrs"); + if (vdrDb->open() != success) return fail; + + // DB-API check + + vdrDb->clear(); + vdrDb->setValue("UUID", EPGDNAME); + + if (!vdrDb->find()) + { + tell(0, "Can't lookup epgd information, start epgd to create the tables first! Aborting now."); + return fail; + } + + vdrDb->reset(); + + if (vdrDb->getIntValue("DBAPI") != DB_API) + { + tell(0, "Found dbapi %d, expected %d, please alter the tables first! Aborting now.", + (int)vdrDb->getIntValue("DBAPI"), DB_API); + return fail; + } + + // open tables .. + + mapDb = new cDbTable(connection, "channelmap"); + if (mapDb->open() != success) return fail; + + fileDb = new cDbTable(connection, "fileref"); + if (fileDb->open() != success) return fail; + + imageDb = new cDbTable(connection, "images"); + if (imageDb->open() != success) return fail; + + imageRefDb = new cDbTable(connection, "imagerefs"); + if (imageRefDb->open() != success) return fail; + + episodeDb = new cDbTable(connection, "episodes"); + if (episodeDb->open() != success) return fail; + + eventsDb = new cDbTable(connection, "events"); + if (eventsDb->open() != success) return fail; + + useeventsDb = new cDbTable(connection, "useevents"); + if (useeventsDb->open() != success) return fail; + + compDb = new cDbTable(connection, "components"); + if (compDb->open() != success) return fail; + + timerDb = new cDbTable(connection, "timers"); + if (timerDb->open() != success) return fail; + + timerDoneDb = new cDbTable(connection, "timersdone"); + if (timerDoneDb->open() != success) return fail; + + recordingDirDb = new cDbTable(connection, "recordingdirs"); + if (recordingDirDb->open() != success) return fail; + + recordingListDb = new cDbTable(connection, "recordinglist"); + if (recordingListDb->open() != success) return fail; + + if ((status = cParameters::initDb(connection)) != success) + return status; + + // ------------------------------------------- + // init db values + + dvbDescription = new cDbValue("description", cDBS::ffText, 50000); + + // ------------------------------------------- + // init statements + + tell(2, "Prepare statements ..."); + + selectMasterVdr = new cDbStatement(vdrDb); + + // select uuid, name from vdrs + // where upper(master) = 'Y' and uuid <> ?; + + selectMasterVdr->build("select "); + selectMasterVdr->bind("UUID", cDBS::bndOut); + selectMasterVdr->bind("NAME", cDBS::bndOut, ", "); + selectMasterVdr->build(" from %s where upper(%s) = 'Y' and ", + vdrDb->TableName(), vdrDb->getField("MASTER")->getDbName()); + selectMasterVdr->bindCmp(0, "Uuid", 0, "<>"); + + status += selectMasterVdr->prepare(); + + // all images + + selectAllImages = new cDbStatement(imageRefDb); + + // prepare fields + + imageSize.setField(&imageSizeDef); + imageUpdSp.setField(imageDb->getField("UpdSp")); + masterId.setField(eventsDb->getField("MasterId")); + + // select e.masterid, r.imagename, r.eventid, r.lfn, length(i.image) + // from imagerefs r, images i, events e + // where i.imagename = r.imagename + // and e.eventid = r.eventid + // and (i.updsp > ? or r.updsp > ?) + + selectAllImages->build("select "); + selectAllImages->setBindPrefix("e."); + selectAllImages->bind(&masterId, cDBS::bndOut); + selectAllImages->setBindPrefix("r."); + selectAllImages->bind("ImgName", cDBS::bndOut, ", "); + selectAllImages->bind("EventId", cDBS::bndOut, ", "); + selectAllImages->bind("Lfn", cDBS::bndOut, ", "); + selectAllImages->setBindPrefix("i."); + selectAllImages->build(", length("); + selectAllImages->bind(&imageSize, cDBS::bndOut); + selectAllImages->build(")"); + selectAllImages->clrBindPrefix(); + selectAllImages->build(" from %s r, %s i, %s e where ", + imageRefDb->TableName(), imageDb->TableName(), eventsDb->TableName()); + selectAllImages->build("e.%s = r.%s and i.%s = r.%s and (", + eventsDb->getField("EventId")->getDbName(), + imageRefDb->getField("EventId")->getDbName(), + imageDb->getField("ImgName")->getDbName(), + imageRefDb->getField("ImgName")->getDbName()); + selectAllImages->bindCmp("i", &imageUpdSp, ">"); + selectAllImages->build(" or "); + selectAllImages->bindCmp("r", "UpdSp", 0, ">"); + selectAllImages->build(")"); + + status += selectAllImages->prepare(); + + // select distinct channelid, channelname + // from channelmap; + + selectAllChannels = new cDbStatement(mapDb); + + selectAllChannels->build("select distinct "); + selectAllChannels->bind("CHANNELID", cDBS::bndOut); + selectAllChannels->bind("CHANNELNAME", cDBS::bndOut, ", "); + selectAllChannels->build(" from %s", mapDb->TableName()); + + status += selectAllChannels->prepare(); + + // select distinct channelid, channelname + // from channelmap + // where channleid = ?; + + selectChannelById = new cDbStatement(mapDb); + + selectChannelById->build("select distinct "); + selectChannelById->bind("CHANNELID", cDBS::bndOut); + selectChannelById->bind("CHANNELNAME", cDBS::bndOut, ", "); + selectChannelById->build(" from %s where ", mapDb->TableName()); + selectChannelById->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet); + + status += selectChannelById->prepare(); + + // update channlemap + // set unknownatvdr = 1 + // where channelid = ? + + markUnknownChannel = new cDbStatement(mapDb); + + markUnknownChannel->build("update %s set ", mapDb->TableName()); + markUnknownChannel->bind("UNKNOWNATVDR", cDBS::bndIn | cDBS::bndSet); + markUnknownChannel->build(" where "); + markUnknownChannel->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet); + + status += markUnknownChannel->prepare(); + + // select changed events + + selectUpdEvents = new cDbStatement(eventsDb); + + // select useid, eventid, source, delflg, updflg, fileref, + // tableid, version, title, shorttext, starttime, + // duration, parentalrating, vps, description + // from eventsview + // where + // channelid = ? + // and updsp > ? + // and updflg in (.....) + + selectUpdEvents->build("select "); + selectUpdEvents->bind("USEID", cDBS::bndOut); + // selectUpdEvents->bind("MASTERID", cDBS::bndOut, ", "); + selectUpdEvents->bind("EVENTID", cDBS::bndOut, ", "); + selectUpdEvents->bind("SOURCE", cDBS::bndOut, ", "); + selectUpdEvents->bind("DELFLG", cDBS::bndOut, ", "); + selectUpdEvents->bind("UPDFLG", cDBS::bndOut, ", "); + selectUpdEvents->bind("FILEREF", cDBS::bndOut, ", "); + selectUpdEvents->bind("TABLEID", cDBS::bndOut, ", "); + selectUpdEvents->bind("VERSION", cDBS::bndOut, ", "); + selectUpdEvents->bind("TITLE", cDBS::bndOut, ", "); + selectUpdEvents->bind("SHORTTEXT", cDBS::bndOut, ", "); + selectUpdEvents->bind("STARTTIME", cDBS::bndOut, ", "); + selectUpdEvents->bind("DURATION", cDBS::bndOut, ", "); + selectUpdEvents->bind("PARENTALRATING", cDBS::bndOut, ", "); + selectUpdEvents->bind("VPS", cDBS::bndOut, ", "); + selectUpdEvents->bind("CONTENTS", cDBS::bndOut, ", "); + selectUpdEvents->bind(dvbDescription, cDBS::bndOut, ", "); + selectUpdEvents->build(" from eventsview where "); + selectUpdEvents->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet); + selectUpdEvents->bindCmp(0, "UPDSP", 0, ">", " and "); + selectUpdEvents->build(" and UPDFLG in (%s)", Us::getNeeded()); + + status += selectUpdEvents->prepare(); + + // select event by useid + + selectEventById = new cDbStatement(useeventsDb); + + // select * from eventsview + // where useid = ? + // and updflg in (.....) + + selectEventById->build("select "); + selectEventById->bindAllOut(); + selectEventById->build(" from %s where ", useeventsDb->TableName()); + selectEventById->bind("USEID", cDBS::bndIn | cDBS::bndSet); + selectEventById->build(" and %s in (%s)", + useeventsDb->getField("UPDFLG")->getDbName(), + Us::getNeeded()); + + status += selectEventById->prepare(); + + // ... + + // select stream, type, lang, description + // from components where + // eventid = ?; + // channelid = ?; + + selectComponentsOf = new cDbStatement(compDb); + + selectComponentsOf->build("select "); + selectComponentsOf->bind("Stream", cDBS::bndOut); + selectComponentsOf->bind("Type", cDBS::bndOut, ", "); + selectComponentsOf->bind("Lang", cDBS::bndOut, ", "); + selectComponentsOf->bind("Description", cDBS::bndOut, ", "); + selectComponentsOf->build(" from %s where ", compDb->TableName()); + selectComponentsOf->bind("EventId", cDBS::bndIn | cDBS::bndSet); + selectComponentsOf->bind("ChannelId", cDBS::bndIn | cDBS::bndSet, " and "); + + status += selectComponentsOf->prepare(); + + // select * + // from recordinglist where + // state <> 'D' + + selectRecordings = new cDbStatement(recordingListDb); + + selectRecordings->build("select "); + selectRecordings->bindAllOut(); + selectRecordings->build(" from %s where ", recordingListDb->TableName()); + selectRecordings->build(" (%s <> 'D' or %s is null)", + recordingListDb->getField("STATE")->getDbName(), + recordingListDb->getField("STATE")->getDbName()); + + status += selectRecordings->prepare(); + + // select srcmovieid, srcseriesid, scrseriesepisode + // from recordinglist where + // state <> 'D' or stete is null + // and (updsp > lastifoupd or lastifoupd is null) + + selectRecForInfoUpdate = new cDbStatement(recordingListDb); + + selectRecForInfoUpdate->build("select "); + selectRecForInfoUpdate->bindAllOut(); + selectRecForInfoUpdate->build(" from %s where ", recordingListDb->TableName()); + selectRecForInfoUpdate->build(" (%s <> 'D' or %s is null)", + recordingListDb->getField("STATE")->getDbName(), + recordingListDb->getField("STATE")->getDbName()); + selectRecForInfoUpdate->build(" and (%s > %s or %s is null) ", + recordingListDb->getField("UPDSP")->getDbName(), + recordingListDb->getField("LASTIFOUPD")->getDbName(), + recordingListDb->getField("LASTIFOUPD")->getDbName()); + + status += selectRecForInfoUpdate->prepare(); + + // select * + // from timers where + // state <> 'D' + // and vdruuid = ? + + selectMyTimer = new cDbStatement(timerDb); + + selectMyTimer->build("select "); + selectMyTimer->bindAllOut(); + selectMyTimer->build(" from %s where ", timerDb->TableName()); + selectMyTimer->build(" %s <> 'D'", timerDb->getField("STATE")->getDbName()); + selectMyTimer->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and "); + + status += selectMyTimer->prepare(); + + // select id, eventid, channelid, starttime, state, endtime + // from timers where + // eventid = ? and channelid = ? and vdruuid = ? + + selectTimerByEvent = new cDbStatement(timerDb); + + selectTimerByEvent->build("select "); + selectTimerByEvent->bind("ID", cDBS::bndOut); + selectTimerByEvent->bind("EVENTID", cDBS::bndOut, ", "); + selectTimerByEvent->bind("CHANNELID", cDBS::bndOut, ", "); + selectTimerByEvent->bind("STARTTIME", cDBS::bndOut, ", "); + selectTimerByEvent->bind("STATE", cDBS::bndOut, ", "); + selectTimerByEvent->bind("ENDTIME", cDBS::bndOut, ", "); + selectTimerByEvent->build(" from %s where ", timerDb->TableName()); + selectTimerByEvent->bind("EVENTID", cDBS::bndIn | cDBS::bndSet); + selectTimerByEvent->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet, " and "); + selectTimerByEvent->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and "); + + status += selectTimerByEvent->prepare(); + + // select * + // from timers where + // id = ? + + selectTimerById = new cDbStatement(timerDb); + + selectTimerById->build("select "); + selectTimerById->bindAllOut(); + selectTimerById->build(" from %s where ", timerDb->TableName()); + selectTimerById->bind("ID", cDBS::bndIn | cDBS::bndSet); + + status += selectTimerById->prepare(); + + // select * + // from timers where + // doneid = ? + + selectTimerByDoneId = new cDbStatement(timerDb); + + selectTimerByDoneId->build("select "); + selectTimerByDoneId->bindAllOut(); + selectTimerByDoneId->build(" from %s where ", timerDb->TableName()); + selectTimerByDoneId->bind("DONEID", cDBS::bndIn | cDBS::bndSet); + + status += selectTimerByDoneId->prepare(); + + //----------- + // select + // max(updsp) + // from timers + + selectMaxUpdSp = new cDbStatement(timerDb); + + selectMaxUpdSp->build("select "); + selectMaxUpdSp->bind("UPDSP", cDBS::bndOut, "max("); + selectMaxUpdSp->build(") from %s", timerDb->TableName()); + + status += selectMaxUpdSp->prepare(); + + // select * from timers + // where + // action != 'A' and action != 'F' and action is not null // !taAssumed and !taFailed + // and (vdruuid = ? or vdruuid = 'any') + + selectPendingTimerActions = new cDbStatement(timerDb); + + selectPendingTimerActions->build("select "); + selectPendingTimerActions->bindAllOut(); + selectPendingTimerActions->build(" from %s where ", timerDb->TableName()); + selectPendingTimerActions->build("%s != 'A' and %s != 'F' and %s is not null", + timerDb->getField("ACTION")->getDbName(), + timerDb->getField("ACTION")->getDbName(), + timerDb->getField("ACTION")->getDbName()); + selectPendingTimerActions->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and ("); + selectPendingTimerActions->build(" or %s = 'any')", timerDb->getField("VDRUUID")->getDbName()); + + status += selectPendingTimerActions->prepare(); + + // delete from timers where + // eventid = ? + + deleteTimer = new cDbStatement(timerDb); + + deleteTimer->build("delete from %s where ", timerDb->TableName()); + deleteTimer->bind("EVENTID", cDBS::bndIn | cDBS::bndSet); + deleteTimer->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet, " and "); + deleteTimer->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and "); + + status += deleteTimer->prepare(); + + if (status == success) + { + // ------------------------------------------- + // lookback -> get last stamp + + vdrDb->clear(); + vdrDb->setValue("UUID", Epg2VdrConfig.uuid); + + if (vdrDb->find()) + { + char buf[50+TB]; + + lastUpdateAt = vdrDb->getIntValue("LastUpdate"); + lastEventsUpdateAt = lastUpdateAt; + getParameter("uuid", "lastEventsUpdateAt", lastEventsUpdateAt); + + strftime(buf, 50, "%d.%m.%y %H:%M:%S", localtime(&lastUpdateAt)); + tell(0, "Info: Last update was at '%s'", buf); + } + + // register me to the vdrs table + + int devCount = 0; + char* v; + + for (int i = 0; i < cDevice::NumDevices(); i++) + { + const cDevice* device = cDevice::GetDevice(i); + + if (device && device->NumProvidedSystems()) + devCount ++; + } + + asprintf(&v, "vdr %s epg2vdr %s (%s)", VDRVERSION, VERSION, VERSION_DATE); + + vdrDb->setValue("UUID", Epg2VdrConfig.uuid); + vdrDb->setValue("IP", getIpOf(Epg2VdrConfig.netDevice)); + vdrDb->setValue("MAC", getMacOf(Epg2VdrConfig.netDevice)); + vdrDb->setValue("NAME", getHostName()); + vdrDb->setValue("DBAPI", DB_API); + vdrDb->setValue("VERSION", v); + vdrDb->setValue("STATE", "attached"); + vdrDb->setValue("MASTER", "n"); + vdrDb->setValue("TUNERCOUNT", devCount); + vdrDb->setValue("SHAREINWEB", Epg2VdrConfig.shareInWeb); + vdrDb->setValue("USECOMMONRECFOLDER", Epg2VdrConfig.useCommonRecFolder); + + // set svdrp port if uninitialized, we can't query ther actual port from VDR :( + + if (vdrDb->getIntValue("SVDRP") == 0) + vdrDb->setValue("SVDRP", 6419); // #TODO -> plugin-setup?! + + vdrDb->store(); + updateVdrData(); + free(v); + } + + if (status == success) + status += cEpg2VdrEpgHandler::getSingleton()->updateExternalIdsMap(mapDb); + + return status; +} + +int cUpdate::exitDb() +{ + // de-register me at the vdrs table + + if (vdrDb && vdrDb->isConnected()) + { + vdrDb->setValue("Uuid", Epg2VdrConfig.uuid); + vdrDb->find(); + vdrDb->setValue("Master", "n"); + vdrDb->setValue("State", "detached"); + vdrDb->store(); + } + + cParameters::exitDb(); + + delete selectAllImages; selectAllImages = 0; + delete selectUpdEvents; selectUpdEvents = 0; + delete selectEventById; selectEventById = 0; + delete selectAllChannels; selectAllChannels = 0; + delete selectChannelById; selectChannelById = 0; + delete markUnknownChannel; markUnknownChannel = 0; + delete selectComponentsOf; selectComponentsOf = 0; + delete selectMasterVdr; selectMasterVdr = 0; + delete deleteTimer; deleteTimer = 0; + delete selectMyTimer; selectMyTimer = 0; + delete selectRecordings; selectRecordings = 0; + delete selectRecForInfoUpdate; selectRecForInfoUpdate = 0; + delete selectTimerByEvent; selectTimerByEvent = 0; + delete selectTimerById; selectTimerById = 0; + delete selectTimerByDoneId; selectTimerByDoneId = 0; + delete selectMaxUpdSp; selectMaxUpdSp = 0; + delete selectPendingTimerActions; selectPendingTimerActions = 0; + + delete vdrDb; vdrDb = 0; + delete mapDb; mapDb = 0; + delete fileDb; fileDb = 0; + delete imageDb; imageDb = 0; + delete imageRefDb; imageRefDb = 0; + delete episodeDb; episodeDb = 0; + delete eventsDb; eventsDb = 0; + delete useeventsDb; useeventsDb = 0; + delete compDb; compDb = 0; + delete timerDb; timerDb = 0; + delete timerDoneDb; timerDoneDb = 0; + delete recordingDirDb; recordingDirDb = 0; + delete recordingListDb; recordingListDb = 0; + + delete dvbDescription; dvbDescription = 0; + + delete connection; connection = 0; + + return done; +} + +//*************************************************************************** +// Check Connection +//*************************************************************************** + +int cUpdate::checkConnection(int& timeout) +{ + static int retryCount = 0; + + timeout = retryCount < 5 ? 10 : 60; + + // reconnect requested ? + + if (dbReconnectTriggered) + exitDb(); + + dbReconnectTriggered = no; + + // check connection + + if (!dbConnected(yes)) + { + // try to connect + + tell(0, "Trying to re-connect to database!"); + retryCount++; + + if (initDb() != success) + { + tell(0, "Retry #%d failed, retrying in %d seconds!", retryCount, timeout); + exitDb(); + + return fail; + } + + retryCount = 0; + tell(0, "Connection established successfull!"); + } + + return success; +} + +//*************************************************************************** +// Is Handler Master +//*************************************************************************** + +int cUpdate::isHandlerMaster() +{ + static int initialized = no; + + char flag = 0; + +/* + wenn no - handler ausschalten und db auf 'N' + wenn auto - aushandeln + wenn yes - handler anschalten und db auf 'Y' + +*/ + if (!dbConnected()) + return no; + + // on first call detect state of epgd + + if (!initialized) + { + epgdBusy = no; + + vdrDb->clear(); + vdrDb->setValue("UUID", EPGDNAME); + + if (vdrDb->find()) + { + nextEpgdUpdateAt = vdrDb->getIntValue("NEXTUPDATE"); + epgdState = cEpgdState::toState(vdrDb->getStrValue("State")); + + if (epgdState >= cEpgdState::esBusy && epgdState < cEpgdState::esBusyImages) + epgdBusy = yes; + + tell(1, "Detected epgd state '%s' (%d)", vdrDb->getStrValue("State"), epgdState); + initialized = yes; + } + else + { + tell(0, "Info: Can't detect epgd state"); + epgdBusy = yes; + } + } + + // update handler role + + if (Epg2VdrConfig.masterMode == mmAuto) + { + tell(3, "Auto check master role"); + + // select where "uuid <> ? and upper(master) = 'Y'" + + vdrDb->clear(); + vdrDb->setValue("Uuid", Epg2VdrConfig.uuid); + + handlerMaster = !selectMasterVdr->find(); + + if (!handlerMaster) + tell(3, "Master found, uuid '%s' (%s)", + vdrDb->getStrValue("UUID"), + vdrDb->getStrValue("NAME")); + + selectMasterVdr->freeResult(); + + flag = handlerMaster ? 'y' : 'n'; + } + else if (Epg2VdrConfig.masterMode == mmYes) + { + flag = 'Y'; + handlerMaster = yes; + } + else + { + flag = 'n'; + handlerMaster = no; + } + + // write again to force at least the updsp + + vdrDb->clear(); + vdrDb->setValue("UUID", Epg2VdrConfig.uuid); + vdrDb->find(); + + vdrDb->setValue("STATE", "attached"); + vdrDb->setCharValue("MASTER", flag); + vdrDb->store(); + + // set handler state + + int handlerState = !epgdBusy && handlerMaster; + + if (cEpg2VdrEpgHandler::getSingleton()->getActive() != handlerState) + { + tell(1, "Change handler state to '%s'", handlerState ? "active" : "standby"); + cEpg2VdrEpgHandler::getSingleton()->setActive(handlerState); + } + + return handlerMaster; +} + +// // *************************************************************************** +// // Initially Init Epgd State +// // *************************************************************************** + +// void cUpdate::updateEpgdState() +// { +// epgdBusy = no; + +// if (!dbConnected()) +// return; + +// vdrDb->clear(); +// vdrDb->setValue("UUID", EPGDNAME); + +// if (vdrDb->find()) +// { +// nextEpgdUpdateAt = vdrDb->getIntValue("NEXTUPDATE"); +// epgdState = cEpgdState::toState(vdrDb->getStrValue("State")); + +// if (epgdState >= cEpgdState::esBusy && epgdState < cEpgdState::esBusyImages) +// epgdBusy = yes; + +// tell(1, "Detected epgd state '%s' (%d)", vdrDb->getStrValue("State"), epgdState); +// } +// else +// tell(0, "Info: Can't detect epgd state"); + +// int handlerState = !epgdBusy && isHandlerMaster(); + +// if (cEpg2VdrEpgHandler::getSingleton()->getActive() != handlerState) +// tell(0, "Set handler state initially to '%s'", handlerState ? "active" : "standby"); + +// cEpg2VdrEpgHandler::getSingleton()->setActive(handlerState); + +// vdrDb->reset(); +// } + +// *************************************************************************** +// Update VDR Data +// *************************************************************************** + +void cUpdate::updateVdrData() +{ + int usedMb = 0; + int freeMb = 0; + + cVideoDirectory::VideoDiskSpace(&freeMb, &usedMb); + + vdrDb->clear(); + vdrDb->setValue("UUID", Epg2VdrConfig.uuid); + vdrDb->find(); + + vdrDb->setValue("VIDEODIR", videoBasePath); + vdrDb->setValue("VIDEOTOTAL", usedMb+freeMb); + vdrDb->setValue("VIDEOFREE", freeMb); + + vdrDb->store(); +} + +//*************************************************************************** +// Update Rec Folder Option +//*************************************************************************** + +int cUpdate::updateRecFolderOption() +{ + vdrDb->clear(); + vdrDb->setValue("UUID", Epg2VdrConfig.uuid); + + if (vdrDb->find()) + { + vdrDb->setValue("USECOMMONRECFOLDER", Epg2VdrConfig.useCommonRecFolder); + vdrDb->update(); + } + + updateRecFolderOptionTrigger = no; + recordingStateChangedTrigger = yes; + recordingFullReloadTrigger = yes; + + return done; +} + +//*************************************************************************** +// Trigger Update +//*************************************************************************** + +void cUpdate::triggerDbReconnect() +{ + dbReconnectTriggered = yes; + waitCondition.Broadcast(); +} + +//*************************************************************************** +// Trigger Update +//*************************************************************************** + +int cUpdate::triggerEpgUpdate(int reload) +{ + if (!loopActive) + { + tell(0, "Thread not running, try to recover ..."); + Cancel(3); + Start(); + } + + fullreload = reload; + mainActPending = yes; + manualTrigger = yes; + + if (epgdBusy) + { + Skins.QueueMessage(mtInfo, cString::sprintf(tr("Skipping '%s' request, epgd busy. Trying again later"), + reload ? tr("reload") : tr("update"))); + + return fail; + } + + waitCondition.Broadcast(); // wakeup thread + + return success; +} + +//*************************************************************************** +// Trigger Recording Update +//*************************************************************************** + +int cUpdate::triggerRecUpdate() +{ + if (!loopActive) + { + tell(0, "Thread not running, try to recover ..."); + Cancel(3); + Start(); + } + + recordingStateChangedTrigger = yes; + waitCondition.Broadcast(); // wakeup thread + + return success; +} + +int cUpdate::commonRecFolderOptionChanged() +{ + updateRecFolderOptionTrigger = yes; + waitCondition.Broadcast(); // wakeup thread + + return done; +} + +//*************************************************************************** +// Trigger Store Info Files +//*************************************************************************** + +int cUpdate::triggerStoreInfoFiles() +{ + if (!loopActive) + { + tell(0, "Thread not running, try to recover ..."); + Cancel(3); + Start(); + } + + storeAllRecordingInfoFilesTrigger = yes; + waitCondition.Broadcast(); // wakeup thread + + return success; +} + +//*************************************************************************** +// Trigger Timer Jobs +//*************************************************************************** + +void cUpdate::triggerTimerJobs() +{ + if (!loopActive) + { + tell(0, "Thread not running, try to recover ..."); + Cancel(3); + Start(); + } + + timerTableUpdateTriggered = yes; + timerJobsUpdateTriggered = yes; + waitCondition.Broadcast(); // wakeup thread +} + +//*************************************************************************** +// Epgd State Change +// - called via SVDRP +//*************************************************************************** + +void cUpdate::epgdStateChange(const char* state) +{ + // state control + + if (!dbConnected()) + return; + + epgdState = cEpgdState::toState(state); + epgdBusy = epgdState >= cEpgdState::esBusy && epgdState < cEpgdState::esBusyImages; + + tell(1, "Got epgd state '%s' (%d)", state, epgdState); + + if (epgdState == Es::esBusyMatch) + eventsPending = yes; // events pending due to merge + else if (epgdState == Es::esBusyEvents) + mainActPending = yes; // main action pending + + // epg handler control + + int handlerState = !epgdBusy && handlerMaster; + + if (cEpg2VdrEpgHandler::getSingleton()->getActive() != handlerState) + tell(1, "Change handler state to '%s'", handlerState ? "active" : "standby"); + + cEpg2VdrEpgHandler::getSingleton()->setActive(handlerState); + + // check loop state + + if (!loopActive) + { + tell(0, "Thread not running, try to recover ..."); + Cancel(3); + Start(); + } + + if (!epgdBusy) + waitCondition.Broadcast(); // wakeup thread +} + +//*************************************************************************** +// Stop Thread +//*************************************************************************** + +void cUpdate::Stop() +{ + loopActive = no; + waitCondition.Broadcast(); // wakeup thread + + Cancel(10); // wait up to 10 seconds for thread was stopping +} + +//*************************************************************************** +// Action +//*************************************************************************** + +void cUpdate::Action() +{ + // open tables - inside thread! + + if (initDb() != success) + exitDb(); + + // mutex gets ONLY unlocked when sleeping + + mutex.Lock(); + loopActive = yes; + + // main action loop ... + + while (loopActive && Running()) + { + int reconnectTimeout; // set by checkConnection() + + // wait 1 minute + + waitCondition.TimedWait(mutex, 60*1000); + + // we pass here at least once per minute ... + + if (checkConnection(reconnectTimeout) != success) + continue; + + // recording stuff + + if (dbConnected() && updateRecFolderOptionTrigger) + updateRecFolderOption(); + + if (dbConnected() && recordingStateChangedTrigger) + { + if (Epg2VdrConfig.shareInWeb) + recordingChanged(); // update timer state + + updateVdrData(); // update video disk size/free, ... + updateRecordingTable(recordingFullReloadTrigger); + + recordingFullReloadTrigger = no; + recordingStateChangedTrigger = no; + } + else if (dbConnected() && lastRecordingDeleteAt+5*tmeSecondsPerMinute < time(0)) + { + cleanupDeletedRecordings(); + updateRecordingInfoFiles(); + lastRecordingDeleteAt = time(0); + } + + if (dbConnected() && storeAllRecordingInfoFilesTrigger) + storeAllRecordingInfoFiles(); + + // check handler role + + isHandlerMaster(); + + if (epgdBusy) + continue; + + if (Epg2VdrConfig.shareInWeb) + { + // check timer distribution and take over 'my' timers + + if (dbConnected() && timerJobsUpdateTriggered) + { + tell(2, "Updating EPG prior to 'check of timer request'"); + refreshEpg(0, na); // refresh EPG before performing timer jobs! + performTimerJobs(); + } + + // update timer + + if (dbConnected() && timerTableUpdateTriggered) + updateTimerTable(); + + if (dbConnected()) + timerChanged(); + } + + // if triggered externally or updates pending + + if (dbConnected(yes) && (mainActPending || eventsPending)) + { + time_t lastUpdateStartAt = time(0); + + tell(mainActPending ? 0 : 2, "--- EPG '%s' started ---", fullreload ? "full-reload" : mainActPending ? "update" : "refresh"); + + if (mainActPending) + { + // cleanup unreferenced image-files + + if (cleanupPictures() != success) + continue; + } + + if (refreshEpg() != success) + continue; + + lastEventsUpdateAt = time(0); + setParameter("uuid", "lastEventsUpdateAt", lastEventsUpdateAt); + cSchedules::Cleanup(true); // force VDR to store of epg.data to filesystem + + if (mainActPending) + { + // get pictures from database and copy to local FS + + if (storePicturesToFs() != success) + continue; + + lastUpdateAt = lastUpdateStartAt; // update lookback information (notice) + + vdrDb->clear(); + vdrDb->setValue("Uuid", Epg2VdrConfig.uuid); + vdrDb->find(); + vdrDb->setValue("LastUpdate", lastUpdateAt); + vdrDb->store(); + } + + tell(mainActPending ? 0 : 2, "--- EPG %s finished ---", fullreload ? "reload" : mainActPending ? "update" : "refresh"); + + if (Epg2VdrConfig.loglevel > 2) + connection->showStat("update"); + + if (manualTrigger) + { + Skins.QueueMessage(mtInfo, cString::sprintf(tr("EPG '%s' done"), fullreload ? tr("reload") : tr("update"))); + manualTrigger = no; + } + + mainActPending = no; + eventsPending = no; + fullreload = no; + } + } + + exit(); // don't call exit in dtor outside of thread!! + + loopActive = no; + + tell(0, "Update thread ended (tid=%i)", cThread::ThreadId()); +} + +//*************************************************************************** +// Clear Epg +//*************************************************************************** + +void clearEpg() +{ +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + + LOCK_TIMERS_WRITE; + LOCK_SCHEDULES_WRITE; + + for (cTimer* timer = Timers->First(); timer; timer = Timers->Next(timer)) + timer->SetEvent(0); // processing all timers here (local *and* remote) + + for (cSchedule* schedule = Schedules->First(); schedule; schedule = Schedules->Next(schedule)) + schedule->Cleanup(INT_MAX); + + cEitFilter::SetDisableUntil(time(0) + 10); + +#else + + while (!cSchedules::ClearAll()) + tell(0, "Warning: Clear EPG failed, can't get lock. Retrying ..."); + +#endif +} + +//*************************************************************************** +// Refresh Epg +//*************************************************************************** + +int cUpdate::refreshEpg(const char* forChannelId, int maxTries) +{ + const cEvent* event; + int tries = 0; + int timerChanges = 0; + int total = 0; + int dels = 0; + int channels = 0; + uint64_t start = cTimeMs::Now(); + cDbStatement* select = 0; + + // lookback ... + + getParameter("uuid", "lastEventsUpdateAt", lastEventsUpdateAt); + + // full reload? + + if (fullreload) + { + tell(1, "Removing all events from epg"); + + clearEpg(); + + lastUpdateAt = 0; + lastEventsUpdateAt = 0; + } + + // iterate over all channels in channelmap + + mapDb->clear(); + + if (forChannelId) + { + select = selectChannelById; + mapDb->setValue("CHANNELID", forChannelId); + + tell(1, "Load EPG for channel '%s'", forChannelId); + } + else + { + select = selectAllChannels; + + if (lastEventsUpdateAt) + tell(2, "Update EPG, loading changes since %s", l2pTime(lastEventsUpdateAt).c_str()); + else + tell(2, "Update EPG, reloading all events"); + } + + for (int f = select->find(); f && dbConnected(yes); f = select->fetch()) + { + int count = 0; + cSchedule* s = 0; + tChannelID channelId = tChannelID::FromString(mapDb->getStrValue("ChannelId")); + + channels++; + + eventsDb->clear(); + eventsDb->setValue("UPDSP", forChannelId ? 0 : lastEventsUpdateAt); + eventsDb->setValue("CHANNELID", mapDb->getStrValue("CHANNELID")); + + // get timers lock + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cStateKey timersKey; + tell(3, "-> Try to get timers lock"); + cTimers* timers = cTimers::GetTimersWrite(timersKey, 500/*ms*/); +#else + cTimers* timers = &Timers; +#endif + + // get schedules lock + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + cStateKey schedulesKey; + tell(3, "-> Try to get schedules lock"); + cSchedules* schedules = cSchedules::GetSchedulesWrite(schedulesKey, 500/*ms*/); +#else + cSchedulesLock* schedulesLock = new cSchedulesLock(true, 500/*ms*/); + cSchedules* schedules = (cSchedules*)cSchedules::Schedules(*schedulesLock); + tell(3, "LOCK (refreshEpg)"); +#endif + + if (!schedules || !timers) + { + tell(3, "Info: Can't get write lock on '%s'", !schedules ? "schedules" : "timers"); + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + if (schedules) schedulesKey.Remove(); + if (timers) timersKey.Remove(); +#else + delete schedulesLock; +#endif + + if (tries++ > maxTries) + { + select->freeResult(); + tell(3, "Warning: Aborting refresh after %d tries", tries); + break; + } + + tell(3, "Retrying in 3 seconds"); + sleep(3); + + continue; + } + + tries = 0; + + // get schedule (channel) + + if (!(s = (cSchedule*)schedules->GetSchedule(channelId))) + s = schedules->AddSchedule(channelId); + + // ----------------------------------------- + // iterate over all events of this channel + + for (int found = selectUpdEvents->find(); found && dbConnected(); found = selectUpdEvents->fetch()) + { + cTimer* timer = 0; + char updFlg = toupper(eventsDb->getStrValue("UPDFLG")[0]); + + updFlg = updFlg == 0 ? 'P' : updFlg; // fix missing flag + + // ignore unneded event rows .. + + if (!Us::isNeeded(updFlg)) + continue; + + // get event / timer + + if (event = s->GetEvent(eventsDb->getIntValue("USEID"))) + { + if (Us::isRemove(updFlg)) + tell(2, "Remove event %uld of channel '%s' due to updflg %c", + event->EventID(), (const char*)event->ChannelID().ToString(), updFlg); + + if (event->HasTimer()) + { + for (timer = timers->First(); timer; timer = timers->Next(timer)) + if (timer->Event() == event) + break; + } + + if (timer) + timer->SetEvent(0); + + s->DelEvent((cEvent*)event); + } + + if (!Us::isRemove(updFlg)) + event = s->AddEvent(createEventFromRow(eventsDb->getRow())); + else if (event) + { + event = 0; + dels++; + } + + if (timer && event) + { + timer->SetEvent(event); + timer->Matches(event); + timerChanges++; + } + else if (timer) + { + tell(0, "Info: Timer '%s', has no event anymore", *timer->ToDescr()); + } + + count++; + } + + selectUpdEvents->freeResult(); + + // Kanal fertig machen .. + + s->Sort(); + s->SetModified(); + + // schedules lock freigeben + +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + schedulesKey.Remove(); + tell(3, "-> Released schedules lock"); + timersKey.Remove(); + tell(3, "-> Released timers lock"); +#else + tell(3, "LOCK free (refreshEpg)"); + delete schedulesLock; +#endif + + tell(2, "Processed channel '%s' - '%s' with %d updates", + eventsDb->getStrValue("ChannelId"), + mapDb->getStrValue("ChannelName"), + count); + + total += count; + } + + select->freeResult(); + + if (timerChanges) + { +#if defined (APIVERSNUM) && (APIVERSNUM >= 20301) + LOCK_TIMERS_WRITE; + cTimers* timers = Timers; +#else + cTimers* timers = &Timers; +#endif + timers->SetModified(); + } + + if (lastEventsUpdateAt) + tell(2, "Updated changes since '%s'; %d channels, " + "%d events (%d deletions) in %s", + forChannelId ? "-" : l2pTime(lastEventsUpdateAt).c_str(), + channels, total, dels, ms2Dur(cTimeMs::Now()-start).c_str()); + else + tell(2, "Updated all %d channels, %d events (%d deletions) in %s", + channels, total, dels, ms2Dur(cTimeMs::Now()-start).c_str()); + + return dbConnected(yes) ? success : fail; +} + +//*************************************************************************** +// To/From Row +//*************************************************************************** + +cEvent* cUpdate::createEventFromRow(const cDbRow* row) +{ + cEvent* e = new cEvent(row->getIntValue("USEID")); + + e->SetTableID(row->getIntValue("TABLEID")); + e->SetVersion(row->getIntValue("VERSION")); + e->SetTitle(row->getStrValue("TITLE")); + e->SetShortText(row->getStrValue("SHORTTEXT")); + e->SetStartTime(row->getIntValue("STARTTIME")); + e->SetDuration(row->getIntValue("DURATION")); + e->SetParentalRating(row->getIntValue("PARENTALRATING")); + e->SetVps(row->getIntValue("VPS")); + e->SetDescription(dvbDescription->getStrValue()); + e->SetComponents(0); + + // contents + + uchar contents[MaxEventContents] = { 0 }; + int numContents = 0; + + for (const char* p = row->getStrValue("CONTENTS"); p && numContents < MaxEventContents; p = strchr(p, ',')) + { + if (*p == ',') p++; + + if (*p) + contents[numContents++] = strtol(p, 0, 0); + } + + e->SetContents(contents); + + // components + + if (row->hasValue("SOURCE", "vdr")) + { + cComponents* components = new cComponents; + + compDb->clear(); + compDb->setBigintValue("EVENTID", row->getBigintValue("EVENTID")); + compDb->setValue("CHANNELID", row->getStrValue("CHANNELID")); + + for (int f = selectComponentsOf->find(); f; f = selectComponentsOf->fetch()) + { + components->SetComponent(components->NumComponents(), + compDb->getIntValue("STREAM"), + compDb->getIntValue("TYPE"), + compDb->getStrValue("LANG"), + compDb->getStrValue("DESCRIPTION")); + } + + selectComponentsOf->freeResult(); + + if (components->NumComponents()) + e->SetComponents(components); // event take ownership of components! + else + delete components; + } + + return e; +} + +//*************************************************************************** +// Store Pictures to local Filesystem +//*************************************************************************** + +int cUpdate::storePicturesToFs() +{ + int count = 0; + int updated = 0; + char* path = 0; + time_t start = time(0); + + if (!Epg2VdrConfig.getepgimages) + return done; + + asprintf(&path, "%s", epgimagedir); + chkDir(path); + free(path); + asprintf(&path, "%s/images", epgimagedir); + chkDir(path); + free(path); + + tell(0, "Load images from database"); + + imageRefDb->clear(); + imageRefDb->setValue("UpdSp", lastUpdateAt); + imageUpdSp.setValue(lastUpdateAt); + + for (int res = selectAllImages->find(); res; res = selectAllImages->fetch()) + { + int eventid = masterId.getIntValue(); + const char* imageName = imageRefDb->getStrValue("ImgName"); + int lfn = imageRefDb->getIntValue("Lfn"); + char* newpath; + char* linkdest = 0; + char* destfile = 0; + int forceLink = no; + int size = imageSize.getIntValue(); + + asprintf(&destfile, "%s/images/%s", epgimagedir, imageName); + + // check target ... image changed? + + if (!fileExists(destfile) || fileSize(destfile) != size) + { + // get image + + imageDb->clear(); + imageDb->setValue("ImgName", imageName); + + if (imageDb->find() && !imageDb->getRow()->getValue("Image")->isNull()) + { + count++; + + // remove existing target + + if (fileExists(destfile)) + { + updated++; + removeFile(destfile); + } + + forceLink = yes; + tell(2, "Store image '%s' with %d bytes", destfile, size); + + if (FILE* fh1 = fopen(destfile, "w")) + { + fwrite(imageDb->getStrValue("Image"), 1, size, fh1); + fclose(fh1); + } + else + { + tell(1, "Can't write image to '%s', error was '%s'", destfile, strerror(errno)); + } + } + + imageDb->reset(); + } + + free(destfile); + + // create links ... + + asprintf(&linkdest, "./images/%s", imageName); + +#ifdef _IMG_LINK + if (!lfn) + { + // for lfn 0 create additional link without "_?" + + asprintf(&newpath, "%s/%d.%s", epgimagedir, eventid, imageExtension); + createLink(newpath, linkdest, forceLink); + free(newpath); + } +#endif + + // create link with index + + asprintf(&newpath, "%s/%d_%d.%s", epgimagedir, eventid, lfn, imageExtension); + createLink(newpath, linkdest, forceLink); + free(newpath); + + // ... + + free(linkdest); + + if (!dbConnected()) + break; + } + + selectAllImages->freeResult(); + + tell(0, "Got %d images from database in %ld seconds (%d updates, %d new)", + count, time(0) - start, updated, count-updated); + + return dbConnected(yes) ? success : fail; +} + +//*************************************************************************** +// Remove Pictures +//*************************************************************************** + +int cUpdate::cleanupPictures() +{ + const char* ext = ".jpg"; + struct dirent* dirent; + DIR* dir; + char* pdir; + int iCount = 0; + int lCount = 0; + + imageRefDb->countWhere("", iCount); + + if (iCount < 100) // less than 100 image lines are suspicious ;) + { + tell(0, "Exit image cleanup to avoid deleting of all images on empty imagerefs table"); + return done; + } + + // ----------------------- + // remove unused images + + tell(1, "Starting cleanup of images in '%s'", epgimagedir); + + // ----------------------- + // cleanup 'images' directory + + cDbStatement* stmt = new cDbStatement(imageRefDb); + + stmt->build("select "); + stmt->bind("FileRef", cDBS::bndOut); + stmt->build(" from %s where ", imageRefDb->TableName()); + stmt->bind("ImgName", cDBS::bndIn | cDBS::bndSet); + + if (stmt->prepare() != success) + { + delete stmt; + return fail; + } + + iCount = 0; + + // open directory + + asprintf(&pdir, "%s/images", epgimagedir); + + if (!(dir = opendir(pdir))) + { + tell(1, "Can't open directory '%s', '%s'", pdir, strerror(errno)); + + free(pdir); + + return done; + } + + free(pdir); + + while (dbConnected() && (dirent = readdir(dir))) + { + // check extension + + if (strncmp(dirent->d_name + strlen(dirent->d_name) - strlen(ext), ext, strlen(ext)) != 0) + continue; + + imageRefDb->clear(); + imageRefDb->setValue("ImgName", dirent->d_name); + + if (!stmt->find()) + { + asprintf(&pdir, "%s/images/%s", epgimagedir, dirent->d_name); + + if (!removeFile(pdir)) + iCount++; + + free(pdir); + } + + stmt->freeResult(); + } + + delete stmt; + closedir(dir); + + if (!dbConnected(yes)) + return fail; + + // ----------------------- + // remove wasted symlinks + + if (!(dir = opendir(epgimagedir))) + { + tell(1, "Can't open directory '%s', '%s'", epgimagedir, strerror(errno)); + return done; + } + + tell(1, "Remove %s symlinks", fullreload ? "all" : "old"); + + while ((dirent = readdir(dir))) + { + // check extension + + if (strncmp(dirent->d_name + strlen(dirent->d_name) - strlen(ext), ext, strlen(ext)) != 0) + continue; + + asprintf(&pdir, "%s/%s", epgimagedir, dirent->d_name); + + // fileExists use access() which dereference links! + + if (isLink(pdir) && (fullreload || !fileExists(pdir))) + { + if (!removeFile(pdir)) + lCount++; + } + + free(pdir); + } + + closedir(dir); + tell(1, "Cleanup finished, removed (%d) images and (%d) symlinks", iCount, lCount); + + return success; +} + +//*************************************************************************** +// Link Needed +//*************************************************************************** + +int cUpdate::pictureLinkNeeded(const char* linkName) +{ + int found; + + if (!dbConnected()) + return yes; + + // we don't need to patch the linkname "123456_0.jpg" + // since atoi() stops at the first non numerical character ... + + imageRefDb->clear(); + imageRefDb->setValue("LFN", 0L); + imageRefDb->setBigintValue("EVENTID", atol(linkName)); + + found = imageRefDb->find(); + imageRefDb->reset(); + + return found; +} diff --git a/update.h b/update.h new file mode 100644 index 0000000..3e4f8e6 --- /dev/null +++ b/update.h @@ -0,0 +1,280 @@ +/* + * update.h: EPG2VDR plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + */ + +#ifndef __UPDATE_H +#define __UPDATE_H + +#include <mysql/mysql.h> +#include <queue> + +#define __STL_CONFIG_H + +#include <vdr/status.h> + +#include "lib/common.h" +#include "lib/db.h" +#include "lib/epgservice.h" + +#include "epg2vdr.h" +#include "parameters.h" + +#define EPGDNAME "epgd" + +//*************************************************************************** +// Running Recording +//*************************************************************************** + +class cRunningRecording : public cListObject +{ + public: + + cRunningRecording(const cTimer* t, long did = na) + { + timer = t; + doneid = did; + lastBreak = 0; + info = 0; + + finished = no; + failed = no; + + // copy until timer get waste .. + + aux = strdup(timer->Aux() ? timer->Aux() : ""); + } + + ~cRunningRecording() + { + free(aux); + free(info); + } + + void setInfo(const char* i) { info = strdup(i); } + + // data + + const cTimer* timer; // #TODO, it's may be fatal to hold a pointer to a timer! + time_t lastBreak; + int finished; + int failed; + long doneid; + char* aux; + char* info; +}; + +//*************************************************************************** +// Event Details +//*************************************************************************** + +class cEventDetails +{ + public: + + cEventDetails() { changes = 0; } + ~cEventDetails() { } + + int getChanges() { return changes; } + void clearChanges() { changes = 0; } + + void setValue(const char* name, const char* value); + void setValue(const char* name, int value); + + int storeToFs(const char* path); + int loadFromFs(const char* path); + int updateByRow(cDbRow* row); + int updateToRow(cDbRow* row); + + private: + + int changes; + std::map<std::string, std::string> values; + + static const char* fields[]; +}; + +//*************************************************************************** +// Update +//*************************************************************************** + +class cUpdate : public cThread, public cStatus, public cParameters +{ + public: + + enum MasterMode + { + mmAuto, + mmYes, + mmNo, + + mmCount + }; + + cUpdate(cPluginEPG2VDR* aPlugin); + ~cUpdate(); + + // interface + + int init(); + int exit(); + + void Stop(); + int isEpgdBusy() { return epgdBusy; } + time_t getNextEpgdUpdateAt() { return nextEpgdUpdateAt; } + void triggerDbReconnect(); + int triggerEpgUpdate(int reload = no); + int triggerRecUpdate(); + int commonRecFolderOptionChanged(); + int triggerStoreInfoFiles(); + void triggerTimerJobs(); + void epgdStateChange(const char* state); + + protected: + + // notifications from VDRs status interface + + virtual void TimerChange(const cTimer* Timer, eTimerChange Change); + virtual void Recording(const cDevice *Device, const char *Name, const char *FileName, bool On); + virtual void ChannelSwitch(const cDevice *Device, int ChannelNumber, bool LiveView); + + private: + + struct TimerId + { + unsigned int eventId; + char channelId[100]; + }; + + // functions + + int initDb(); + int exitDb(); + + void Action(void); + int isHandlerMaster(); + void updateVdrData(); + int updateRecFolderOption(); + int dbConnected(int force = no) + { return connection && connection->isConnected() && (!force || connection->check() == success); } + int checkConnection(int& timeout); + + int refreshEpg(const char* channelid = 0, int maxTries = 5); + cEvent* createEventFromRow(const cDbRow* row); + int lookupVdrEventOf(int eId, const char* cId); + int storePicturesToFs(); + int cleanupPictures(); + int pictureLinkNeeded(const char* linkName); + + tChannelID toChanID(const char* chanIdStr) + { + if (isEmpty(chanIdStr)) + return tChannelID::InvalidID; + + return tChannelID::FromString(chanIdStr); + } + + // timer stuff + + int updateTimerTable(); + int performTimerJobs(); + int recordingChanged(); + int updateTimerDone(int timerid, int doneid, char state); + int timerChanged(); + + // recording stuff + + int updateRecordingTable(int fullReload = no); + int cleanupDeletedRecordings(int force = no); + int updateRecordingDirectory(const cRecording* recording); + int updatePendingRecordingInfoFiles(const cRecordings* recordings); + int storeAllRecordingInfoFiles(); + int updateRecordingInfoFiles(); + + // data + + cDbConnection* connection; + cPluginEPG2VDR* plugin; + int handlerMaster; + int loopActive; + time_t nextEpgdUpdateAt; + time_t lastUpdateAt; + time_t lastEventsUpdateAt; + time_t lastRecordingDeleteAt; + int lastRecordingCount; + char* epgimagedir; + int withutf8; + cCondVar waitCondition; + cMutex mutex; + int fullreload; + char imageExtension[3+TB]; + + cMutex timerMutex; + int dbReconnectTriggered; + int timerJobsUpdateTriggered; + int timerTableUpdateTriggered; + int manualTrigger; + int recordingStateChangedTrigger; + int recordingFullReloadTrigger; + int storeAllRecordingInfoFilesTrigger; + int updateRecFolderOptionTrigger; + cList<cRunningRecording> runningRecordings; + cMutex runningRecMutex; + + Es::State epgdState; + int epgdBusy; + int eventsPending; + int mainActPending; + const char* videoBasePath; + int timersTableMaxUpdsp; + + cDbTable* eventsDb; + cDbTable* useeventsDb; + cDbTable* fileDb; + cDbTable* imageDb; + cDbTable* imageRefDb; + cDbTable* episodeDb; + cDbTable* mapDb; + cDbTable* timerDb; + cDbTable* timerDoneDb; + cDbTable* vdrDb; + cDbTable* compDb; + cDbTable* recordingDirDb; + cDbTable* recordingListDb; + + cDbStatement* selectMasterVdr; + cDbStatement* selectAllImages; + cDbStatement* selectUpdEvents; + cDbStatement* selectEventById; + cDbStatement* selectAllChannels; + cDbStatement* selectChannelById; + cDbStatement* markUnknownChannel; + cDbStatement* selectComponentsOf; + cDbStatement* deleteTimer; + cDbStatement* selectMyTimer; + cDbStatement* selectRecordings; + cDbStatement* selectRecForInfoUpdate; + cDbStatement* selectPendingTimerActions; + cDbStatement* selectTimerByEvent; + cDbStatement* selectTimerById; + cDbStatement* selectTimerByDoneId; + cDbStatement* selectMaxUpdSp; + + cDbValue vdrEvtId; + cDbValue extEvtId; + cDbValue vdrStartTime; + cDbValue extChannelId; + cDbValue imageUpdSp; + cDbValue imageSize; + cDbValue masterId; + + cDbValue* dvbDescription; + + std::queue<std::string> pendingNewRecordings; // recordings to store details + std::vector<TimerId> deletedTimers; +}; + +//*************************************************************************** +#endif //__UPDATE_H |