From 5eacf5bf36ddbac082a9e40a2bcdfd0f04fd3f9f Mon Sep 17 00:00:00 2001 From: horchi Date: Sun, 5 Mar 2017 14:51:57 +0100 Subject: commit of actual revision --- .gitignore | 9 + COPYING | 340 ++++ HISTORY.h | 616 +++++++ Make.config | 41 + Makefile | 229 +++ README | 146 ++ TODO | 7 + configs/epg.dat | 979 ++++++++++ configs/epgsearch/epgsearch.conf | 4 + configs/epgsearch/epgsearchcats.conf | 42 + configs/epgsearch/epgsearchuservars.conf | 12 + contrib/epg2vdr.ignore | 25 + epg2vdr.c | 991 ++++++++++ epg2vdr.h | 80 + handler.h | 1141 ++++++++++++ lib/Makefile | 108 ++ lib/common.c | 1921 ++++++++++++++++++++ lib/common.h | 560 ++++++ lib/config.c | 59 + lib/config.h | 47 + lib/configuration.c | 193 ++ lib/configuration.h | 140 ++ lib/curl.c | 454 +++++ lib/curl.h | 77 + lib/db.c | 1649 +++++++++++++++++ lib/db.h | 1370 ++++++++++++++ lib/dbdict.c | 527 ++++++ lib/dbdict.h | 471 +++++ lib/demo.c | 531 ++++++ lib/demo.dat | 17 + lib/epgservice.c | 121 ++ lib/epgservice.h | 468 +++++ lib/imgtools.c | 217 +++ lib/imgtools.h | 31 + lib/json.c | 164 ++ lib/json.h | 36 + lib/python.c | 356 ++++ lib/python.h | 78 + lib/pytst.c | 122 ++ lib/searchtimer.c | 1373 ++++++++++++++ lib/searchtimer.h | 86 + lib/semtst.c | 42 + lib/test.c | 756 ++++++++ lib/thread.c | 342 ++++ lib/thread.h | 92 + menu.c | 823 +++++++++ menu.h | 522 ++++++ menudone.c | 158 ++ menusched.c | 1204 ++++++++++++ menusearchtimer.c | 381 ++++ menutimers.c | 572 ++++++ parameters.c | 253 +++ parameters.h | 59 + ...re-vdr-2.1.x--epghandler-segment-transfer.patch | 65 + patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch | 219 +++ patches/vdr-1.7.28-epghandledexternally.diff | 118 ++ patches/vdr-1.7.29-epgIsUpdate.diff | 52 + patches/vdr-2.3.1.patch | 11 + patches/vdr-2.3.2.patch | 56 + plgconfig.c | 36 + plgconfig.h | 44 + po/de_DE.po | 355 ++++ po/it_IT.po | 343 ++++ recinfofile.c | 245 +++ recording.c | 533 ++++++ service.c | 71 + service.h | 127 ++ status.c | 272 +++ svdrpclient.c | 749 ++++++++ svdrpclient.h | 158 ++ timer.c | 679 +++++++ ttools.c | 583 ++++++ ttools.h | 42 + update.c | 1822 +++++++++++++++++++ update.h | 280 +++ 75 files changed, 27902 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 HISTORY.h create mode 100644 Make.config create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 configs/epg.dat create mode 100644 configs/epgsearch/epgsearch.conf create mode 100644 configs/epgsearch/epgsearchcats.conf create mode 100644 configs/epgsearch/epgsearchuservars.conf create mode 100644 contrib/epg2vdr.ignore create mode 100644 epg2vdr.c create mode 100644 epg2vdr.h create mode 100644 handler.h create mode 100644 lib/Makefile create mode 100644 lib/common.c create mode 100644 lib/common.h create mode 100644 lib/config.c create mode 100644 lib/config.h create mode 100644 lib/configuration.c create mode 100644 lib/configuration.h create mode 100644 lib/curl.c create mode 100644 lib/curl.h create mode 100644 lib/db.c create mode 100644 lib/db.h create mode 100644 lib/dbdict.c create mode 100644 lib/dbdict.h create mode 100644 lib/demo.c create mode 100644 lib/demo.dat create mode 100644 lib/epgservice.c create mode 100644 lib/epgservice.h create mode 100644 lib/imgtools.c create mode 100644 lib/imgtools.h create mode 100644 lib/json.c create mode 100644 lib/json.h create mode 100644 lib/python.c create mode 100644 lib/python.h create mode 100644 lib/pytst.c create mode 100644 lib/searchtimer.c create mode 100644 lib/searchtimer.h create mode 100644 lib/semtst.c create mode 100644 lib/test.c create mode 100644 lib/thread.c create mode 100644 lib/thread.h create mode 100644 menu.c create mode 100644 menu.h create mode 100644 menudone.c create mode 100644 menusched.c create mode 100644 menusearchtimer.c create mode 100644 menutimers.c create mode 100644 parameters.c create mode 100644 parameters.h create mode 100644 patches/pre-vdr-2.1.x--epghandler-segment-transfer.patch create mode 100644 patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch create mode 100644 patches/vdr-1.7.28-epghandledexternally.diff create mode 100644 patches/vdr-1.7.29-epgIsUpdate.diff create mode 100644 patches/vdr-2.3.1.patch create mode 100644 patches/vdr-2.3.2.patch create mode 100644 plgconfig.c create mode 100644 plgconfig.h create mode 100644 po/de_DE.po create mode 100644 po/it_IT.po create mode 100644 recinfofile.c create mode 100644 recording.c create mode 100644 service.c create mode 100644 service.h create mode 100644 status.c create mode 100644 svdrpclient.c create mode 100644 svdrpclient.h create mode 100644 timer.c create mode 100644 ttools.c create mode 100644 ttools.h create mode 100644 update.c create mode 100644 update.h 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 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..f90922e --- /dev/null +++ b/COPYING @@ -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. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 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. + + , 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='' -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 diff --git a/README b/README new file mode 100644 index 0000000..0d2ee30 --- /dev/null +++ b/README @@ -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 ;)) diff --git a/TODO b/TODO new file mode 100644 index 0000000..3fd95d1 --- /dev/null +++ b/TODO @@ -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 +#include + +#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::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::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::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::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::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 \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 + +#include +#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 evtMemList; + std::map 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::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 externIdMap; + std::map 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 +#include + +#include +#include +#include + +#ifdef USEUUID +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef USELIBARCHIVE +# include +# include +#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 ""; + + 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 +#include +#include + +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 and get data of first content which name matches +//*************************************************************************** + +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 // uint_64_t +#include +#include +#include +#include +#include +#include +#include + +#ifdef USESYSD +# include +#endif + +#ifdef USEMD5 +# include // MD5_* +#endif + +#ifdef USELIBXML +# include +# include +# include +#endif + +#ifdef VDR_PLUGIN +# include +#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 +#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 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 = ""); +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 + +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 + +#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 +#include + +#include + +#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 +#include + +#include + +#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::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 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::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 fields; + MYSQL_RES* result; + MYSQL_ROW row; + std::map::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::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::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::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 + +#include +#include +#include +#include +#include + +#include + +#include + +#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::iterator it = statements.begin() ; it != statements.end(); ++it) + { + if (*it) + (*it)->showStat(); + } + + statisticPeriod = time(0); + } + + private: + + time_t statisticPeriod; + std::list 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::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() : ""; } + 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::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::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::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::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 + +#include +#include +#include + +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 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::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::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::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 indices; + + // FiledDefs stored as list to have access via index + std::vector _dfields; + + // same FiledDef references stored as a map to have access via name + std::map 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::iterator getFirstTableIterator() { return tables.begin(); } + std::map::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 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 + +#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::iterator i; + + i = times.begin(); + + return &(*i); + } + + UserTime* getNext() + { + std::list::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::iterator it; + std::list 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 + +#define X_DISPLAY_MISSING 1 + +#include +#include + +#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 + +#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 + +#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 []"); + 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 += " \n" + " conflict #" + num2Str(conflicts) + + "on" + vdrDb->getStrValue("NAME") + + " \n" + " \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, + " " + "%ld" + "%s" + "%s" + "%s" + "%s" + "\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 +#include +#include + +#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 // uint_64_t +#include +#include + +#include +#include + +#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 = ""; + + 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 = ""; + + 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", int(s-xml), xml, tag, value, tag, s); + } + else + { + sprintf(tmp, "%s<%s><%s>%d", 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 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::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 +#include +#include +#include +#include + +#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 diff --git a/menu.c b/menu.c new file mode 100644 index 0000000..7c90606 --- /dev/null +++ b/menu.c @@ -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 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; +} diff --git a/menu.h b/menu.h new file mode 100644 index 0000000..afde73e --- /dev/null +++ b/menu.h @@ -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 +#include + +#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 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 + +#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 + +#include +#include +#include + +#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::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::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 + +#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 +#include + +#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::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(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 { +@@ -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 +# This file is distributed under the same license as the VDR package. +# Klaus Schmidinger , 2000 +# +msgid "" +msgstr "" +"Project-Id-Version: VDR 1.5.7\n" +"Report-Msgid-Bugs-To: \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 \n" +"Language-Team: \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 +# This file is distributed under the same license as the VDR package. +# Alberto Carraro , 2001 +# Antonio Ospite , 2003 +# Sean Carlos , 2005 +# +msgid "" +msgstr "" +"Project-Id-Version: VDR 1.5.7\n" +"Report-Msgid-Bugs-To: \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 \n" +"Language-Team: \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::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::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::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::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::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 + +#include + +#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 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 +#include + +#include + +//*************************************************************************** +// 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 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 + +#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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#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 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* 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 +#include + +#include // uint_64_t +#include +#include + +#include "lib/common.h" + +#ifdef VDR_PLUGIN +# define __STL_CONFIG_H +# include +#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 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* list = 0, int timeoutMs = 20 * 1000); +}; + +//*************************************************************************** +#endif // __SVDRP_CLIENT_H_ diff --git a/timer.c b/timer.c new file mode 100644 index 0000000..931553d --- /dev/null +++ b/timer.c @@ -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 +#include + +#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 + +#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 = ""; + + 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 = ""; + + 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", int(s-xml), xml, tag, value, tag, s); + } + else + { + asprintf(&tmp, "%s<%s><%s>%d", 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", int(s-xml), xml, tag, value, tag, s); + } + else + { + asprintf(&tmp, "%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: "62 - TNTSerieHDFalling Skies141149868014115021003565129 + // AUX: ......yes" + + if (t->Aux()) + { + epgs = strstr(t->Aux(), "") != 0; + childLock = strstr(t->Aux(), "yes") != 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 + +#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 + +#include +#include + +#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 +#include + +#define __STL_CONFIG_H + +#include + +#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 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 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 pendingNewRecordings; // recordings to store details + std::vector deletedTimers; +}; + +//*************************************************************************** +#endif //__UPDATE_H -- cgit v1.2.3