summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore9
-rw-r--r--COPYING340
-rw-r--r--HISTORY.h616
-rw-r--r--Make.config41
-rw-r--r--Makefile229
-rw-r--r--README146
-rw-r--r--TODO7
-rw-r--r--configs/epg.dat979
-rw-r--r--configs/epgsearch/epgsearch.conf4
-rw-r--r--configs/epgsearch/epgsearchcats.conf42
-rw-r--r--configs/epgsearch/epgsearchuservars.conf12
-rw-r--r--contrib/epg2vdr.ignore25
-rw-r--r--epg2vdr.c991
-rw-r--r--epg2vdr.h80
-rw-r--r--handler.h1141
-rw-r--r--lib/Makefile108
-rw-r--r--lib/common.c1921
-rw-r--r--lib/common.h560
-rw-r--r--lib/config.c59
-rw-r--r--lib/config.h47
-rw-r--r--lib/configuration.c193
-rw-r--r--lib/configuration.h140
-rw-r--r--lib/curl.c454
-rw-r--r--lib/curl.h77
-rw-r--r--lib/db.c1649
-rw-r--r--lib/db.h1370
-rw-r--r--lib/dbdict.c527
-rw-r--r--lib/dbdict.h471
-rw-r--r--lib/demo.c531
-rw-r--r--lib/demo.dat17
-rw-r--r--lib/epgservice.c121
-rw-r--r--lib/epgservice.h468
-rw-r--r--lib/imgtools.c217
-rw-r--r--lib/imgtools.h31
-rw-r--r--lib/json.c164
-rw-r--r--lib/json.h36
-rw-r--r--lib/python.c356
-rw-r--r--lib/python.h78
-rw-r--r--lib/pytst.c122
-rw-r--r--lib/searchtimer.c1373
-rw-r--r--lib/searchtimer.h86
-rw-r--r--lib/semtst.c42
-rw-r--r--lib/test.c756
-rw-r--r--lib/thread.c342
-rw-r--r--lib/thread.h92
-rw-r--r--menu.c823
-rw-r--r--menu.h522
-rw-r--r--menudone.c158
-rw-r--r--menusched.c1204
-rw-r--r--menusearchtimer.c381
-rw-r--r--menutimers.c572
-rw-r--r--parameters.c253
-rw-r--r--parameters.h59
-rw-r--r--patches/pre-vdr-2.1.x--epghandler-segment-transfer.patch65
-rw-r--r--patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch219
-rw-r--r--patches/vdr-1.7.28-epghandledexternally.diff118
-rw-r--r--patches/vdr-1.7.29-epgIsUpdate.diff52
-rw-r--r--patches/vdr-2.3.1.patch11
-rw-r--r--patches/vdr-2.3.2.patch56
-rw-r--r--plgconfig.c36
-rw-r--r--plgconfig.h44
-rw-r--r--po/de_DE.po355
-rw-r--r--po/it_IT.po343
-rw-r--r--recinfofile.c245
-rw-r--r--recording.c533
-rw-r--r--service.c71
-rw-r--r--service.h127
-rw-r--r--status.c272
-rw-r--r--svdrpclient.c749
-rw-r--r--svdrpclient.h158
-rw-r--r--timer.c679
-rw-r--r--ttools.c583
-rw-r--r--ttools.h42
-rw-r--r--update.c1822
-rw-r--r--update.h280
75 files changed, 27902 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c97a742
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*.so
+.dependencies
+*.o
+*.a
+*~
+*.mo
+*.pot
+lib/demo
+lib/tst
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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/HISTORY.h b/HISTORY.h
new file mode 100644
index 0000000..c3f3836
--- /dev/null
+++ b/HISTORY.h
@@ -0,0 +1,616 @@
+/*
+ * -----------------------------------
+ * epg2vdr Plugin - Revision History
+ * -----------------------------------
+ *
+ */
+
+#define _VERSION "1.1.42"
+#define VERSION_DATE "01.03.2017"
+
+#define DB_API 4
+
+#ifdef GIT_REV
+# define VERSION _VERSION "-GIT" GIT_REV
+#else
+# define VERSION _VERSION
+#endif
+
+/*
+ * ------------------------------------
+
+2017-03-01: version 1.1.43 (horchi)
+ - bugfix: Fixed crash in meues without database connection
+
+2017-02-27: version 1.1.41 (horchi)
+ - bugfix: Fixed lookup of repeating events
+
+2017-02-27: version 1.1.40 (horchi)
+ - change: minor changes (logging, ...)
+
+2017-02-24: version 1.1.39 (horchi)
+ - change: modified null field handling for timersdonedb
+
+2017-02-14: version 1.1.38 (horchi)
+ - bugfix: fixed compile bug
+
+2017-02-14: version 1.1.37 (horchi)
+ - bugfix: fixed detection if recording is complete or not
+
+2017-02-14: version 1.1.36 (horchi)
+ - added: command menu for schedules (key 0)
+
+2017-02-14: version 1.1.35 (horchi)
+ - change: modified timer delete/reject handling
+
+2017-02-10: version 1.1.34 (horchi)
+ - bugfix: Fixed possible crash at timer update
+
+2017-02-09: version 1.1.33 (horchi)
+ - added: Added epg handler patch for VDR 2.3.2
+ -> enhancement of epg handler interface to fix a threading
+ problem which can occur on systems with more than one tuner
+
+2017-02-08: version 1.1.32 (horchi)
+ - bugfix: first program menu item now also selectable
+
+2017-02-08: version 1.1.31 (horchi)
+ - added: Added patch for VDR >= 2.3.1
+ Important, without this patch it will not work!
+
+2017-02-06: version 1.1.30 (horchi)
+ - bugfix: fix of '1.1.28' next try ;)
+
+2017-02-06: version 1.1.29 (horchi)
+ - bugfix: fixed compile issue with gcc 6.2.0
+
+2017-02-06: version 1.1.28 (horchi)
+ - bugfix: fixed sql error in epg search menu
+
+2017-02-01: version 1.1.27 (horchi)
+ - bugfix: fixed timer naming problem
+
+2017-01-19: version 1.1.26 (horchi)
+ - bugfix: Fixed buf with cyclic db reconnect
+
+2017-01-19: version 1.1.25 (horchi)
+ - added: support of namingmode 'template' by epgd/epghttpd
+
+2017-01-16: version 1.1.24 (horchi)
+ - change: Finished porting to VDR 2.3.2
+
+2017-01-08: version 1.1.23 (horchi)
+ - bugfix: fixed schedules lock problem for vdr < 2.3.x
+
+2017-01-08: version 1.1.22 (horchi)
+ - change: fixed problem with 2.3.1 / 2.3.2
+
+2017-01-07: version 1.1.21 (horchi)
+ - change: now compiles with vdr 2.3.1 / 2.3.2
+
+2017-01-03: version 1.1.20 (horchi)
+ - bugfix: fixed reinstate of events in epg handler
+
+2016-11-30: version 1.1.19 (horchi)
+ - change: merging of lib
+
+2016-11-30: version 1.1.18 (horchi)
+ - added: Delete / Modify of timers on event changes
+
+2016-11-30: version 1.1.17 (horchi)
+ - change: improved format strings for 32 bit systems
+
+2016-11-02: version 1.1.16 (horchi)
+ - change: improved cleanup handling for recordings
+
+2016-11-02: version 1.1.15 (horchi)
+ - added: epg-detail menu toggle even for schedules
+
+2016-11-01: version 1.1.14 (horchi)
+ - added: added title for green and yellow button in epg-detail menu
+
+2016-11-01: version 1.1.13 (horchi)
+ - added: channel toggle in Event-Info menu (green/yellow)
+
+2016-11-01: version 1.1.12 (horchi)
+ - added: started implementaion of schedule toggle in Event-Info menu
+
+2016-10-30: version 1.1.11 (horchi)
+ - added: mark epgseasrch timers (set source to 'epgs')
+
+2016-10-20: version 1.1.10 (horchi)
+ - added: update of db field timers._starttime
+
+2016-10-18: version 1.1.9 (horchi)
+ - bugfix: fixed missing close of socket handle
+
+2016-10-17: version 1.1.8 (horchi)
+ - added: search repetition of event
+
+2016-10-16: version 1.1.7 (horchi)
+ - change: improved recording search using LV distance < 50%
+
+2016-08-26: version 1.1.6 (horchi)
+ - added: support of long eventids for tvsp (merged dev into master)
+
+2016-07-19: version 1.1.5 (rechner)
+ - change: increased event id to unsigned int64
+
+2016-07-15: version 1.1.4 (horchi)
+ - change: started increase of event id to unsigned int64
+
+2016-07-06: version 1.1.3 (horchi)
+ - bugfix: fixed channel switch in schedule menu
+
+2016-07-06: version 1.1.2 (horchi)
+ - change: minor change on menu refresh
+
+2016-07-05: version 1.1.1 (horchi)
+ - bugfix: fixed refreah of schedules menu on timer create
+
+2016-07-04: version 1.1.0 (horchi)
+ - change: merged http branch into master
+
+2016-06-10: version 1.0.49 (horchi)
+ - change: fixed unitialized variable (thx to Joerg)
+
+2016-06-01: version 1.0.48 (horchi)
+ - change: adjustet some log levels in epg handler (more silent)
+
+2016-06-01: version 1.0.47 (horchi)
+ - change: remove 'alter table'
+ - change: added new dictionary version
+
+2016-05-25: version 1.0.46 (horchi)
+ - change: optimized scrap recording trigger
+
+2016-05-24: version 1.0.45 (horchi)
+ - change: fixed sequence of epg load and perform of timer jobs at VDR start
+
+2016-05-23: version 1.0.44 (horchi)
+ - bugfix: fixed crash at timer-service on empty epg
+
+2016-05-23: version 1.0.43 (horchi)
+ - bugfix: fixed crash on setup-menu close
+
+2016-05-23: version 1.0.42 (horchi)
+ - added: support of 'highlighted' flag for user times
+
+2016-05-20: version 1.0.41 (horchi)
+ - added: new column for textual rating and commentator
+ - change: removed unused info column
+
+2016-05-18: version 1.0.40 (horchi)
+ - bugfix: Fixed toggle of timer state with [red]
+ - bugfix: Fixed change of timer start/end timer
+
+2016-05-17: version 1.0.39 (horchi)
+ - added: service to notify timer changes
+
+2016-05-13: version 1.0.38 (horchi)
+ - added: optimized timer lookup for schedules menu
+
+2016-05-12: version 1.0.37 (horchi)
+ - added: store MAC address to vdrs table
+
+2016-05-11: version 1.0.36 (horchi)
+ - added: State query for service interface
+
+2016-05-11: version 1.0.35 (horchi)
+ - change: honor 'Share in Web' config for service 'EPG2VDR_TIMER_SERVICE'
+ - change: set of NAMINGMODE and AUTOTIMEINSSP in timers table
+
+2016-05-09: version 1.0.34 (horchi)
+ - fixed: added sql string-escape for python return
+
+2016-05-04: version 1.0.33 (horchi)
+ - bugfix: fixed cursor pos in timermenu after refresh
+
+2016-05-03: version 1.0.32 (horchi)
+ - change: column with for searchtimer result menu
+
+2016-05-03: version 1.0.31 (horchi)
+ - added: parallel processing für timer service
+
+2016-05-02: version 1.0.30 (horchi)
+ - change: finished service interface for timers
+
+2016-05-02: version 1.0.29 (horchi)
+ - change: redesign of service interface for timers
+
+2016-04-23: version 1.0.28 (horchi)
+ - bugfix: added missing table init
+
+2016-04-23: version 1.0.27 (horchi)
+ - change: added totoal count dor service interface (ForEachTimer)
+
+2016-04-22: version 1.0.26 (horchi)
+ - bugfix: fixed set of scrnew on info.epg2vdr changes
+
+2016-04-22: version 1.0.25 (horchi)
+ - added: reconnect to database server on change of connection settigns
+
+2016-04-22: version 1.0.24 (horchi)
+ - bugfix: fixed crash on toggle NAS option
+
+2016-04-22: version 1.0.23 (horchi)
+ - bugfix: fixed compile without GTFT patch
+
+2016-04-21: version 1.0.22 (horchi)
+ - added: channel switch for channels without epg
+
+2016-04-20: version 1.0.21 (horchi)
+ - bugfix: set plugin menu category to mcMain
+
+2016-04-20: version 1.0.20 (horchi)
+ - change: set plugin menu category to mcMain
+ - added: ForEachTimer to service interface
+ - bugfix: fixed active/deactive distribution
+
+2016-04-19: version 1.0.19 (horchi)
+ - bugfix: fixed timer menu refresh
+
+2016-04-19: version 1.0.18 (horchi)
+ - bugfix: fixed active/deactive handling in timers menu
+
+2016-04-18: version 1.0.17 (horchi)
+ - added: added hack to force skindesigner to use 'favorite menu view' for user defined searches
+
+2016-04-18: version 1.0.16 (horchi)
+ - added: more german translations (thx to utility)
+
+016-04-18: version 1.0.15 (horchi)
+ - bugfix: fixed fix init of trigger flag :o ;)
+
+2016-04-18: version 1.0.14 (horchi)
+ - bugfix: fixed init of trigger flag (thx to mini73)
+
+2016-04-18: version 1.0.13 (horchi)
+ - added: temporary shortcut to searchtimer dialog in epg menu via key 3
+
+2016-04-18: version 1.0.12 (horchi)
+ - added: option to show channels without epg in schedules menu
+
+2016-04-17: version 1.0.11 (horchi)
+ - change: removed needless dependency
+
+2016-04-15: version 1.0.10 (horchi)
+ - bugfix: fixed column width of search result menu
+
+2016-04-15: version 1.0.9 (horchi)
+ - bugfix: fixed column width of search timer menu
+
+2016-04-15: version 1.0.8 (horchi)
+ - bugfix: fixed channel switch in details menu
+
+2016-04-15: version 1.0.7 (horchi)
+ - added: setup option to x-change kBlue and kOk
+
+2016-04-14: version 1.0.6 (horchi)
+ - bugfix: fixed crash on large quicktimes value
+
+2016-04-14: version 1.0.5 (horchi)
+ - change: increased parameter size to 500 character
+
+2016-04-14: version 1.0.4 (horchi)
+ - added: Added user defined favorite search to epg menu
+
+2016-04-14: version 1.0.3 (horchi)
+ - change: Fixed log message typo
+
+2016-04-14: version 1.0.2 (horchi)
+ - change: Update of lib-horchi
+ - change: Morde debug log messages
+
+2016-04-14: version 1.0.1 (horchi)
+ - bugfix: Fixed crash in timers-done menu
+
+2016-04-05: version 1.0.0 (horchi)
+ - change: First BETA release
+ - change: Added config option 'create timer local'
+ - change: Use WEB settings of 'default VDR' at timer create
+
+2016-03-30: version 0.3.49 (horchi)
+ - bugfix: fixed default network device,
+ using first of device list except 'lo' as default
+
+2016-03-30: version 0.3.48 (horchi)
+ - bugfix: fixed buffer error on large aux values
+
+2016-03-26: version 0.3.47 (horchi)
+ - bugfix: fixed char compare of db API
+
+2016-03-22: version 0.3.46 (horchi)
+ - bugfix: fixed format of epg.dat
+
+2016-03-22: version 0.3.45 (horchi)
+ - changed: removed reason from timers table
+
+2016-03-21: version 0.3.44 (horchi)
+ - changed: added default value für timer state
+
+2016-03-18: version 0.3.43
+ - added: added make update to Makefile
+
+2016-03-17: version 0.3.42
+ - change: minor speed improvement for timer menu
+
+2016-03-17: version 0.3.41
+ - change: igoring already tooked 'any'VDR' timer in timer menu
+
+2016-03-16: version 0.3.40
+ - added: added log message on forced epg reload
+ - change: reset 'reason' on successfull retried timers
+
+2016-03-16: version 0.3.39
+ - added: force load of events for specific channel on missing event at timer creation
+
+2016-03-15: version 0.3.38
+ - bugfix: fixed timer job for not vdr bound timers again
+
+2016-03-15: version 0.3.37
+ - bugfix: fixed timer job for not vdr bound timers
+
+2016-03-10: version 0.3.36
+ - bugfix: fixed usage of db connection in wrong thread
+
+2016-03-05: version 0.3.35
+ - bugfix: fixed segfault on parameter read
+
+2016-03-04: version 0.3.34
+ - bugfix: fixed bug with parameter handling
+ - added: trigger update of video space on recording change
+
+2016-03-03: version 0.3.33
+ - added: config option for network device
+
+2016-03-03: version 0.3.32
+ - change: Now and Next of epg menu now configured in webif
+ (Use @Now and @Next as Time like 'Jetzt=@Now')
+
+2016-02-29: version 0.3.31
+ - added: configuration for start page of schedule menu
+ - change: internal redesign of schedule menu implementation
+
+2016-02-26: version 0.3.30
+ - added: more translations
+
+2016-02-26: version 0.3.29
+ - bugfix: fixed compile problem with new translations (thx to Ole)
+
+2016-02-26: version 0.3.28
+ - change: added german translations (thx to Ole)
+
+2016-02-26: version 0.3.27
+ - change: trigger scaping of changed recordings
+ - change: fixed menu display
+
+2016-02-24: version 0.3.26
+ - added: added missing files :o
+
+2016-02-24: version 0.3.25
+ - added: epg2vdr skin interface for timer
+
+2016-02-18: version 0.3.24
+ - added: delete option for dones to search-result menu
+
+2016-02-18: version 0.3.23
+ - added: show count of already done timers in search-result menu
+
+2016-02-18: version 0.3.22
+ - added: test-search for searchtimer
+
+2016-02-16: version 0.3.21
+ - bugfix: fixed potetioal crash on empty user list
+
+2016-02-16: version 0.3.20
+ - change: chnaged state for moved/deleted timers
+
+2016-02-16: version 0.3.19
+ - bugfix: fixed timer move
+
+2016-02-15: version 0.3.18
+ - added: check for recoring running at 'timer move'
+
+2016-02-15: version 0.3.17
+ - change: hold menu position in epg menu at iterate over times
+ - change: added 'Timer still running' confirmation on timer delete
+
+2016-02-15: version 0.3.16
+ - change: minor change of timer on 'move'
+
+2016-02-11: version 0.3.15
+ - added: code and log message review
+
+2016-02-10: version 0.3.14
+ - added: Reorganice recordingslist table at chage of useCommonRecFolder setting
+
+2016-02-10: version 0.3.13
+ - added: Seach of recordings in programm menu with key '2'
+
+2016-02-09: version 0.3.12
+ - bugfix: fixed problem timer handling
+
+2016-02-09: version 0.3.11
+ - bugfix: fixed problem with timer state handling
+
+2016-02-08: version 0.3.10
+ - change: ported new db lib
+ - change: improved updates of timersdone table
+
+2016-02-07: version 0.3.9
+ - change: added missing trigger for timer update
+
+2016-02-05: version 0.3.8
+ - added: delete and activate/deactivate searchtimers
+
+2016-02-05: version 0.3.7
+ - added: centralized some code
+ - added: implemented delete button for done timers
+
+2016-02-05: version 0.3.6
+ - added: searchtimer menu
+
+2016-02-05: version 0.3.5
+ - added: take timer requests even without vdruuid
+ - change: some code cleanup
+
+2016-02-05: version 0.3.4
+ - added: Display OSD message after manual triggered successful reload/update of EPG
+
+2016-02-04: version 0.3.3
+ - bugfix: fixed compile issue
+
+2016-02-04: version 0.3.2
+ - change: removed obsolete table from epg.dat
+
+2016-02-04: version 0.3.1
+ - change: redesign of timerjobs, remove table timerdistribution
+
+2016-02-03: version 0.3.0
+ - change: desupport VDR versions before 2.2.0
+ - change: code cleanup
+
+2016-02-02: version 0.2.8
+ - bugfix: fixed compile error
+
+2016-02-02: version 0.2.7
+ - bugfix: fixed display of filename (cutted at : sign)
+
+2016-02-02: version 0.2.6
+ - change: minor code cleanup
+
+2016-02-01: version 0.2.5
+ - bugfix: fixed crash un undefined channel
+
+2016-02-01: version 0.2.4
+ - bugfix: minor fix of config display
+
+2016-02-01: version 0.2.3
+ - added: improved display of timer status in case of error
+
+2016-01-30: version 0.2.2
+ - added: timer status and action to timer edit menu
+ - change: show only future events at repeat query
+
+2016-01-30: version 0.2.1
+ - bugfix: minor fix
+
+2016-01-30: version 0.2.0
+ - added: Replacement of VDRs Timer and Program menus, now directly support remote timer
+
+2016-01-21: version 0.1.18
+ - bugfix: fixed possible crash at shutdown
+
+2016-01-18: version 0.1.17
+ - change: initialize epg handler still earlier
+ - bugfix: fixed timer delete (via menu)
+ - bugfix: increased source field of table timerdistribution (to fit uuid)
+
+2016-01-18: version 0.1.16
+ - bugfix: fixed timer move (via menu)
+
+2016-01-18: version 0.1.15
+ - bugfix: fixed problem with double epg entries on merge = 0
+
+2016-01-15: version 0.1.14
+ - change: updated dictionary
+
+2015-11-02: version 0.1.13
+ - change: recording path (now without base path)
+ - added: service to register mysql_lib_init
+
+2015-11-02: version 0.1.13
+ - change: recording path (now without base path)
+ - added: service to register mysql_lib_init
+
+2015-06-25: version 0.1.12
+ - change: start implementation of a timer menu
+
+2015-02-02: version 0.1.11
+ - change: merges lib from epgd
+
+2014-12-28: version 0.1.10
+ - bugfix: fixed possible crash at exit
+ - change: deleting old timers from timers table
+
+2014-05-13: version 0.1.9
+ - bugfix: fixed mutex problem in epg handler
+
+2014-03-22: version 0.1.8
+ - bugfix: fixed name lookup for mapdb
+
+2014-01-03: version 0.1.7
+ - bugfix: fixed epg handler
+
+2014-01-03: version 0.1.6
+ - change: adapted epgdata plugin to modified header
+ - change: added fields producer, other and camera
+ - change: removed fields origtitle and team
+
+2013-02-05: version 0.1.5
+ - change: removed activity check, instead wait up to 20 seconds to give the mail loop time to finish
+
+2013-12-31: version 0.1.4
+ - change: increased field guest to 1000
+
+2013-12-04: version 0.1.3
+ - bugfix: fixed insert handling with multimerge
+ - change: speedup reload with multimerge
+
+2013-11-20: version 0.1.2
+ - change: increased shorttext and compshorttext to 300 chars, topic and guest to 500 chars
+ therefore you have to alter your tables
+
+2013-10-28: version 0.1.1
+ - bugfix: fixed core on missing database
+
+2013-10-23: version 0.1.0
+ - change: first release with epg merge
+ - added: 'LoadImages', 'Shutdown On Busy', 'Schedule Boot For Update'
+ and 'Prohibit Shutdown On Busy epgd' to plugin setup menu
+ - removed: "UpdateTime" Option
+
+2013-09-27: version 0.0.7b
+ - change: first alpha version with epg merge
+ - added: optional schedule of wakeup
+ - added: optional "Prohibit Shutdown On Busy 'epgd'"
+ - change: improved picture load and link handling
+
+2013-09-23: version 0.0.7a
+ - change: started devel branch for epg merge
+
+2013-09-16: version 0.0.6
+ - bugfix: fixed install of locale files
+ - bugfix: fixed handler bug (thread conflict), reported by 3po
+ - change: improved handler speed
+ - change: enhanced connection check reported by OleS
+ - bugfix: problem with blacklist, reportet by theChief
+
+2013-09-05: version 0.0.5
+ - change: pause update only if epgd is busy with events (not while epgd is loading images)
+
+2013-09-04: version 0.0.4
+ - added: removed cyclic update og EPG from database
+ -> instead auto trigger update after epgd finished download
+ - added: get changes by DVB of "vdr:000" channels every 5 minutes
+ - added: pause epg handler while epgd is busy
+ - added: pause update while epgd is busy
+
+2013-08-31: version 0.0.3
+ - change: improved speed of connection check
+ (speed of 0.0.1 restored)
+
+2013-08-30: version 0.0.2
+ - bugfix: improved database reconnect
+ - added: check DBAPI on startup
+ - bugfix: empty shorttext now NULL instead of ""
+
+2013-08-28: version 0.0.1
+ - first official release
+
+2012-12-19: version 0.0.1-rc1
+ - initiale version
+
+ * ------------------------------------
+ */
diff --git a/Make.config b/Make.config
new file mode 100644
index 0000000..5d52c29
--- /dev/null
+++ b/Make.config
@@ -0,0 +1,41 @@
+
+# Make.config
+#
+# See the README file for copyright information and how to reach the author.
+#
+#
+
+# user defined stuff
+
+PREFIX = /usr/local
+
+DEBUG = 1
+
+# -----------------------
+# don't touch below ;)
+
+CC = g++
+doCompile = $(CC) -c $(CFLAGS) $(DEFINES)
+doLink = $(CC) $(LFLAGS)
+doLib = ar -rs
+
+USEPYTHON = 1
+USEEPGS = 1
+
+USES = -DVDR_PLUGIN -DUSEUUID -DUSEMD5 -DUSEJSON -DUSEGUNZIP -DPLUGIN_NAME_I18N='"$(PLUGIN)"'
+
+ifdef DEBUG
+ CXXFLAGS += -ggdb -O0
+endif
+
+CXXFLAGS += -fPIC -Wreturn-type -Wall -Wno-parentheses -Wformat -pedantic \
+ -Wno-long-long -Wunused-variable -Wunused-label -Wno-unused-result \
+ -Wunused-value -Wunused-but-set-variable -Wunused-function -Wno-variadic-macros \
+ -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
+
+CXXFLAGS += -D__STDC_FORMAT_MACROS
+
+CFLAGS += $(CXXFLAGS)
+
+%.o: %.c
+ $(doCompile) -o $@ $<
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f392783
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,229 @@
+#
+# Makefile for a Video Disk Recorder plugin
+#
+
+PLUGIN = epg2vdr
+HLIB = -L./lib -lhorchi
+HISTFILE = "HISTORY.h"
+
+include Make.config
+
+# enable graphtftng and/or pin plugin support if autodetection below don't work
+#WITH_GTFT = 1
+#WITH_PIN = 1
+
+### The version number of this plugin (taken from the main source file):
+
+VERSION = $(shell grep 'define _VERSION ' $(HISTFILE) | awk '{ print $$3 }' | sed -e 's/[";]//g')
+
+LASTHIST = $(shell grep '^20[0-3][0-9]' $(HISTFILE) | head -1)
+LASTCOMMENT = $(subst |,\n,$(shell sed -n '/$(LASTHIST)/,/^ *$$/p' $(HISTFILE) | tr '\n' '|'))
+LASTTAG = $(shell git describe --tags --abbrev=0)
+BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
+GIT_REV = $(shell git describe --always 2>/dev/null)
+
+### The directory environment:
+
+# Use package data if installed...otherwise assume we're under the VDR source directory:
+PKGCFG = $(if $(VDRDIR),$(shell pkg-config --variable=$(1) $(VDRDIR)/vdr.pc),$(shell pkg-config --variable=$(1) vdr || pkg-config --variable=$(1) ../../../vdr.pc))
+
+LIBDIR = $(call PKGCFG,libdir)
+LOCDIR = $(call PKGCFG,locdir)
+PLGCFG = $(call PKGCFG,plgcfg)
+CONFDEST = $(call PKGCFG,configdir)/plugins/$(PLUGIN)
+
+# autodetect related plugins
+
+ifeq (exists, $(shell test -e ../graphtftng && echo exists))
+ WITH_GTFT = 1
+endif
+ifeq (exists, $(shell test -e ../vdr-plugin-graphtftng && echo exists))
+ WITH_GTFT = 1
+endif
+ifeq (exists, $(shell test -e ../pin && echo exists))
+ WITH_PIN = 1
+endif
+ifeq (exists, $(shell test -e ../vdr-plugin-pin && echo exists))
+ WITH_PIN = 1
+endif
+
+#
+
+TMPDIR ?= /tmp
+
+### The compiler options:
+
+export CFLAGS += $(call PKGCFG,cflags)
+export CXXFLAGS += $(call PKGCFG,cxxflags)
+
+### The version number of VDR's plugin API:
+
+APIVERSION = $(call PKGCFG,apiversion)
+
+### Allow user defined options to overwrite defaults:
+
+-include $(PLGCFG)
+
+OBJS = $(PLUGIN).o \
+ service.o update.o plgconfig.o parameters.o \
+ timer.o recording.o recinfofile.o \
+ status.o ttools.o svdrpclient.o \
+ menu.o menusched.o menutimers.o menudone.o menusearchtimer.o
+
+LIBS = $(HLIB)
+LIBS += -lrt -larchive -lcrypto -luuid
+LIBS += $(shell mysql_config --libs_r) $(shell python-config --libs) $(shell pkg-config --cflags --libs jansson)
+
+EPG2VDR_DATA_DIR = "/var/cache/vdr"
+
+ifdef WITH_GTFT
+ DEFINES += -DWITH_GTFT
+endif
+
+ifdef WITH_PIN
+ DEFINES += -DWITH_PIN
+endif
+
+ifdef EPG2VDR_DATA_DIR
+ DEFINES += -DEPG2VDR_DATA_DIR='$(EPG2VDR_DATA_DIR)'
+endif
+
+### The name of the distribution archive:
+
+ARCHIVE = $(PLUGIN)-$(VERSION)
+PACKAGE = vdr-$(ARCHIVE)
+
+### The name of the shared object file:
+
+SOFILE = libvdr-$(PLUGIN).so
+
+### Includes and Defines (add further entries here):
+
+INCLUDES += $(shell mysql_config --include)
+
+DEFINES += -DEPG2VDR -DLOG_PREFIX='"$(PLUGIN): "' $(USES)
+
+ifdef GIT_REV
+ DEFINES += -DGIT_REV='"$(GIT_REV)"'
+endif
+
+### The main target:
+
+all: $(SOFILE) i18n
+
+hlib:
+ (cd lib && $(MAKE) -s lib)
+
+### Implicit rules:
+
+%.o: %.c
+ $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) -o $@ $<
+
+### Dependencies:
+
+MAKEDEP = $(CXX) -MM -MG
+DEPFILE = .dependencies
+$(DEPFILE): Makefile
+ $(MAKEDEP) $(CXXFLAGS) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@
+
+-include $(DEPFILE)
+
+### Internationalization (I18N):
+
+PODIR = po
+I18Npo = $(wildcard $(PODIR)/*.po)
+I18Nmo = $(addsuffix .mo, $(foreach file, $(I18Npo), $(basename $(file))))
+I18Nmsgs = $(addprefix $(DESTDIR)$(LOCDIR)/, $(addsuffix /LC_MESSAGES/vdr-$(PLUGIN).mo, $(notdir $(foreach file, $(I18Npo), $(basename $(file))))))
+I18Npot = $(PODIR)/$(PLUGIN).pot
+
+%.mo: %.po
+ msgfmt -c -o $@ $<
+
+$(I18Npot): $(wildcard *.c)
+ xgettext -C -cTRANSLATORS --no-wrap --no-location -k -ktr -ktrNOOP --package-name=vdr-$(PLUGIN) --package-version=$(VERSION) --msgid-bugs-address='<vdr@jwendel.de>' -o $@ `ls $^`
+
+%.po: $(I18Npot)
+ msgmerge -U --no-wrap --no-location --backup=none -q -N $@ $<
+ @touch $@
+
+$(I18Nmsgs): $(DESTDIR)$(LOCDIR)/%/LC_MESSAGES/vdr-$(PLUGIN).mo: $(PODIR)/%.mo
+ install -D -m644 $< $@
+
+.PHONY: i18n
+i18n: $(I18Nmo) $(I18Npot)
+
+install-i18n: $(I18Nmsgs)
+
+### Targets:
+
+$(SOFILE): hlib $(OBJS)
+ $(CXX) $(CXXFLAGS) $(LDFLAGS) -shared $(OBJS) $(LIBS) -o $@
+
+install-lib: $(SOFILE)
+ install -D -m644 $^ $(DESTDIR)$(LIBDIR)/$^.$(APIVERSION)
+
+install: install-lib install-i18n install-config
+
+install-config:
+ if ! test -d $(DESTDIR)$(CONFDEST); then \
+ mkdir -p $(DESTDIR)$(CONFDEST); \
+ chmod a+rx $(DESTDIR)$(CONFDEST); \
+ fi
+ install --mode=644 -D ./configs/epg.dat $(DESTDIR)$(CONFDEST)
+ifdef VDR_USER
+ if test -n $(VDR_USER); then \
+ chown $(VDR_USER) $(DESTDIR)$(CONFDEST); \
+ chown $(VDR_USER) $(DESTDIR)$(CONFDEST)/epg.dat; \
+ fi
+endif
+
+dist: clean
+ @-rm -rf $(TMPDIR)/$(ARCHIVE)
+ @mkdir $(TMPDIR)/$(ARCHIVE)
+ @cp -a * $(TMPDIR)/$(ARCHIVE)
+ @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE)
+ @-rm -rf $(TMPDIR)/$(ARCHIVE)
+ @echo Distribution package created as $(PACKAGE).tgz
+
+clean:
+ @-rm -f $(OBJS) $(DEPFILE) *.so *.core* *~ ./configs/*~
+ @-rm -f $(PODIR)/*.mo $(PODIR)/*.pot $(PODIR)/*~
+ @-rm -f $(PACKAGE).tgz
+ (cd lib && $(MAKE) clean)
+
+cppchk:
+ cppcheck --language=c++ --template="{file}:{line}:{severity}:{message}" --quiet --force *.c *.h
+
+# ------------------------------------------------------
+# Git / Versioning / Tagging
+# ------------------------------------------------------
+
+vcheck:
+ git fetch
+ if test "$(LASTTAG)" = "$(VERSION)"; then \
+ echo "Warning: tag/version '$(VERSION)' already exists, update HISTORY first. Aborting!"; \
+ exit 1; \
+ fi
+
+push: vcheck
+ echo "tagging git with $(VERSION)"
+ git tag $(VERSION)
+ git push --tags
+ git push
+
+commit: vcheck
+ git commit -m "$(LASTCOMMENT)" -a
+
+git: commit push
+
+showv:
+ @echo "Git ($(BRANCH)):\\n Version: $(LASTTAG) (tag)"
+ @echo "Local:"
+ @echo " Version: $(VERSION)"
+ @echo " Change:"
+ @echo -n " $(LASTCOMMENT)"
+
+update:
+ git pull
+ @make clean install
+ restart vdr
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 <vdr/menu.h>
+#include <vdr/tools.h>
+
+#include "plgconfig.h"
+#include "update.h"
+#include "menu.h"
+#include "handler.h"
+
+#if defined (APIVERSNUM) && (APIVERSNUM < 20200)
+# error VDR API versions < 2.2.0 are not supported !
+#endif
+
+cUpdate* oUpdate = 0;
+const char* logPrefix = LOG_PREFIX;
+
+//***************************************************************************
+// Static global handler Instance
+//***************************************************************************
+
+cEpg2VdrEpgHandler* cEpg2VdrEpgHandler::singleton = cEpg2VdrEpgHandler::getSingleton();
+
+//***************************************************************************
+// Menu Edit List Item
+//***************************************************************************
+
+class cMenuEditStrListItem : public cMenuEditIntItem
+{
+ public:
+
+ cMenuEditStrListItem(const char* Name, int* Value, cStringList* List);
+
+ protected:
+
+ virtual void Set();
+ cStringList* list;
+};
+
+cMenuEditStrListItem::cMenuEditStrListItem(const char* Name, int* Value, cStringList* List)
+ : cMenuEditIntItem(Name, Value, 0, List->Size()-1)
+{
+ list = List;
+ Set();
+}
+
+void cMenuEditStrListItem::Set()
+{
+ char* v = (*list)[*value];
+ SetValue(v);
+}
+
+//***************************************************************************
+// Plugin Main Menu
+//***************************************************************************
+
+class cEpgPluginMenu : public cOsdMenu
+{
+ public:
+
+ cEpgPluginMenu(const char* title, cPluginEPG2VDR* aPlugin);
+ virtual ~cEpgPluginMenu() { };
+
+ virtual eOSState ProcessKey(eKeys key);
+
+ protected:
+
+ cPluginEPG2VDR* plugin;
+};
+
+enum EpgMenuState
+{
+ emsTimer = os_User,
+ emsSearchtimer,
+ emsDones,
+ emsProgram,
+ emsUpdate,
+ emsReload
+};
+
+cEpgPluginMenu::cEpgPluginMenu(const char* title, cPluginEPG2VDR* aPlugin)
+ : cOsdMenu(title)
+{
+ plugin = aPlugin;
+
+ SetMenuCategory(mcMain);
+ SetHasHotkeys(yes);
+ Clear();
+
+ if (Epg2VdrConfig.shareInWeb)
+ {
+ cOsdMenu::Add(new cOsdItem(hk(tr("Timer")), (eOSState)emsTimer));
+ cOsdMenu::Add(new cOsdItem(hk(tr("Search Timer")), (eOSState)emsSearchtimer));
+ cOsdMenu::Add(new cOsdItem(hk(tr("Timer journal")), (eOSState)emsDones));
+ }
+
+ cOsdMenu::Add(new cOsdItem(hk(tr("Program")), (eOSState)emsProgram));
+ cOsdMenu::Add(new cOsdItem(hk(tr("Update")), (eOSState)emsUpdate));
+ cOsdMenu::Add(new cOsdItem(hk(tr("Reload")), (eOSState)emsReload));
+
+ SetHelp(0, 0, 0, 0);
+
+ Display();
+}
+
+eOSState cEpgPluginMenu::ProcessKey(eKeys key)
+{
+ eOSState state = cOsdMenu::ProcessKey(key);
+
+ switch (state)
+ {
+ case emsTimer:
+ {
+ state = AddSubMenu(new cMenuEpgTimers());
+ break;
+ }
+
+ case emsSearchtimer:
+ {
+ state = AddSubMenu(new cMenuEpgSearchTimers());
+ break;
+ }
+
+ case emsDones:
+ {
+ state = AddSubMenu(new cEpgMenuDones());
+ break;
+ }
+
+ case emsProgram:
+ {
+ state = AddSubMenu(new cMenuEpgWhatsOn());
+ break;
+ }
+
+ case emsUpdate:
+ {
+ Skins.Message(mtInfo, tr("Update EPG"));
+ oUpdate->triggerEpgUpdate();
+ return osEnd;
+ }
+
+ case emsReload:
+ {
+ Skins.Message(mtInfo, tr("Reload EPG"));
+ oUpdate->triggerEpgUpdate(true);
+ return osEnd;
+ }
+
+ default: ;
+ }
+
+ return state;
+}
+
+//***************************************************************************
+// Plugin Setup Menu
+//***************************************************************************
+
+class cMenuSetupEPG2VDR : public cMenuSetupPage
+{
+ public:
+
+ cMenuSetupEPG2VDR();
+ ~cMenuSetupEPG2VDR() ;
+
+ protected:
+
+ virtual eOSState ProcessKey(eKeys Key);
+ virtual void Store();
+ virtual void Setup();
+
+ private:
+
+ cEpg2VdrConfig data;
+ cMenuDb* menuDb;
+ long int webLoginEnabled;
+ char** userList;
+ int userCount;
+ cStringList interfaceList;
+ int interfaceIndex;
+};
+
+cMenuSetupEPG2VDR::cMenuSetupEPG2VDR()
+{
+ data = Epg2VdrConfig;
+ menuDb = new cMenuDb;
+ webLoginEnabled = no;
+ userList = 0;
+ userCount = 0;
+ interfaceIndex = 0;
+
+ Setup();
+}
+
+cMenuSetupEPG2VDR::~cMenuSetupEPG2VDR()
+{
+ if (userList)
+ {
+ for (int i = 0; i < userCount; i++)
+ free(userList[i]);
+
+ delete userList;
+ }
+
+ delete menuDb;
+}
+
+void cMenuSetupEPG2VDR::Setup()
+{
+ int current = Current();
+
+ //
+ // DVB Event update mode
+
+ static const char* masterModes[4];
+
+ masterModes[0] = tr("auto");
+ masterModes[1] = tr("yes");
+ masterModes[2] = tr("no");
+ masterModes[3] = 0;
+
+ //
+ // List of WEB users
+
+ menuDb->initUserList(userList, userCount, webLoginEnabled);
+
+ if (webLoginEnabled && userCount && !isEmpty(Epg2VdrConfig.user))
+ {
+ // set userIndex to the configured user
+
+ for (int i = 0; i < userCount; i++)
+ {
+ if (strcmp(userList[i], data.user) == 0)
+ {
+ data.userIndex = i;
+ break;
+ }
+ }
+ }
+
+ //
+ // Network Interfaces
+
+ int i = 0;
+ char* interfaces;
+
+ interfaces = strdup(getInterfaces());
+ interfaceList.Clear();
+
+ for (char* p = strtok(interfaces, " "); p; p = strtok(0, " "))
+ {
+ interfaceList.Append(strdup(p));
+
+ if (!isEmpty(data.netDevice) && strncmp(p, data.netDevice, strlen(data.netDevice)) == 0)
+ {
+ tell(0, "Index of '%s' is %d", p, i);
+ interfaceIndex = i;
+ }
+
+ i++;
+ }
+
+ free(interfaces);
+
+ // ...
+
+ Clear();
+
+ Add(new cOsdItem(cString::sprintf("-------------------- %s ----------------------------------", tr("EPG Update"))));
+ cList<cOsdItem>::Last()->SetSelectable(false);
+
+ cOsdMenu::Add(new cMenuEditStraItem(tr("Update DVB EPG Database"), (int*)&data.masterMode, cUpdate::mmCount, masterModes));
+ Add(new cMenuEditBoolItem(tr("Load Images"), &data.getepgimages));
+ Add(new cMenuEditBoolItem(tr("Prohibit Shutdown On Busy 'epgd'"), &data.activeOnEpgd));
+ Add(new cMenuEditBoolItem(tr("Schedule Boot For Update"), &data.scheduleBoot));
+ Add(new cMenuEditBoolItem(tr("Blacklist not configured Channels"), &data.blacklist));
+
+ Add(new cOsdItem(cString::sprintf("--------------------- %s ---------------------------------", tr("Menu"))));
+ cList<cOsdItem>::Last()->SetSelectable(false);
+ Add(new cMenuEditBoolItem(tr("Show In Main Menu"), &data.mainmenuVisible));
+ Add(new cMenuEditBoolItem(tr("Replace Program Menu"), &data.replaceScheduleMenu));
+ Add(new cMenuEditBoolItem(tr("Replace Timer Menu"), &data.replaceTimerMenu));
+ Add(new cMenuEditBoolItem(tr("XChange Key Ok/Blue"), &data.xchgOkBlue));
+ Add(new cMenuEditBoolItem(tr("Show Channels without EPG"), &data.showEmptyChannels));
+
+ Add(new cOsdItem(cString::sprintf("--------------------- %s ---------------------------------", tr("Web"))));
+ cList<cOsdItem>::Last()->SetSelectable(false);
+ Add(new cMenuEditBoolItem(tr("Share in Web"), &data.shareInWeb));
+ Add(new cMenuEditBoolItem(tr("Create Timer Local"), &data.createTimerLocal));
+ Add(new cMenuEditBoolItem(tr("Have Common Recordings Folder (NAS)"), &data.useCommonRecFolder));
+ Add(new cMenuEditStrListItem(tr("SVDRP Interface"), &interfaceIndex, &interfaceList));
+ if (userCount && webLoginEnabled)
+ Add(new cMenuEditStraItem(tr("User"), (int*)&data.userIndex, userCount, userList));
+
+ Add(new cOsdItem(cString::sprintf("--------------------- %s ---------------------------------", tr("MySQL"))));
+ cList<cOsdItem>::Last()->SetSelectable(false);
+ Add(new cMenuEditStrItem(tr("Host"), data.dbHost, sizeof(data.dbHost), tr(FileNameChars)));
+ Add(new cMenuEditIntItem(tr("Port"), &data.dbPort, 1, 99999));
+ Add(new cMenuEditStrItem(tr("Database Name"), data.dbName, sizeof(data.dbName), tr(FileNameChars)));
+ Add(new cMenuEditStrItem(tr("User"), data.dbUser, sizeof(data.dbUser), tr(FileNameChars)));
+ Add(new cMenuEditStrItem(tr("Password"), data.dbPass, sizeof(data.dbPass), tr(FileNameChars)));
+
+ Add(new cOsdItem(cString::sprintf("--------------------- %s ---------------------------------", tr("Technical Stuff"))));
+ cList<cOsdItem>::Last()->SetSelectable(false);
+ Add(new cMenuEditIntItem(tr("Log level"), &data.loglevel, 0, 4));
+
+ SetCurrent(Get(current));
+ Display();
+}
+
+eOSState cMenuSetupEPG2VDR::ProcessKey(eKeys Key)
+{
+ eOSState state = cMenuSetupPage::ProcessKey(Key);
+
+ switch (state)
+ {
+ case osContinue:
+ {
+ if (NORMALKEY(Key) == kUp || NORMALKEY(Key) == kDown)
+ {
+ cOsdItem* item = Get(Current());
+
+ if (item)
+ item->ProcessKey(kNone);
+ }
+
+ break;
+ }
+
+ default: break;
+ }
+
+ return state;
+}
+
+void cMenuSetupEPG2VDR::Store()
+{
+ int useCommonRecFolderOptionChanged = no;
+
+ if (Epg2VdrConfig.useCommonRecFolder != data.useCommonRecFolder)
+ useCommonRecFolderOptionChanged = yes;
+
+ if (data.hasDbLoginChanged(&Epg2VdrConfig))
+ oUpdate->triggerDbReconnect();
+
+ Epg2VdrConfig = data;
+
+ SetupStore("LogLevel", Epg2VdrConfig.loglevel);
+ SetupStore("ShowInMainMenu", Epg2VdrConfig.mainmenuVisible);
+ SetupStore("Blacklist", Epg2VdrConfig.blacklist);
+ SetupStore("DbHost", Epg2VdrConfig.dbHost);
+ SetupStore("DbPort", Epg2VdrConfig.dbPort);
+ SetupStore("DbName", Epg2VdrConfig.dbName);
+ SetupStore("DbUser", Epg2VdrConfig.dbUser);
+ SetupStore("DbPass", Epg2VdrConfig.dbPass);
+ SetupStore("MasterMode", Epg2VdrConfig.masterMode);
+ SetupStore("LoadImages", Epg2VdrConfig.getepgimages);
+ SetupStore("ActiveOnEpgd", Epg2VdrConfig.activeOnEpgd);
+ SetupStore("ScheduleBoot", Epg2VdrConfig.scheduleBoot);
+ SetupStore("ShareInWeb", Epg2VdrConfig.shareInWeb);
+ SetupStore("CreateTimerLocal", Epg2VdrConfig.createTimerLocal);
+ SetupStore("XChgKeyOkBlue", Epg2VdrConfig.xchgOkBlue);
+ SetupStore("ShowEmptyChannels", Epg2VdrConfig.showEmptyChannels);
+ SetupStore("UseCommonRecFolder", Epg2VdrConfig.useCommonRecFolder);
+ SetupStore("ReplaceScheduleMenu", Epg2VdrConfig.replaceScheduleMenu);
+ SetupStore("ReplaceTimerMenu", Epg2VdrConfig.replaceTimerMenu);
+
+ if (userCount && Epg2VdrConfig.userIndex >= 0)
+ {
+ sstrcpy(Epg2VdrConfig.user, userList[Epg2VdrConfig.userIndex], sizeof(Epg2VdrConfig.user));
+ SetupStore("User", Epg2VdrConfig.user);
+ }
+
+ char* device = strdup(interfaceList[interfaceIndex]);
+ char* ip = strchr(device, ':');
+
+ if (!isEmpty(ip))
+ {
+ *(ip++) = 0;
+
+ if (strcmp(Epg2VdrConfig.netDevice, device) != 0)
+ {
+ sstrcpy(Epg2VdrConfig.netDevice, device, sizeof(Epg2VdrConfig.netDevice));
+
+ menuDb->vdrDb->clear();
+ menuDb->vdrDb->setValue("UUID", Epg2VdrConfig.uuid);
+ menuDb->vdrDb->find();
+ menuDb->vdrDb->setValue("IP", ip);
+ menuDb->vdrDb->store();
+ }
+ }
+
+ free(device);
+
+ SetupStore("NetDevice", Epg2VdrConfig.netDevice);
+
+ if (useCommonRecFolderOptionChanged)
+ oUpdate->commonRecFolderOptionChanged();
+}
+
+//***************************************************************************
+// Plugin - EPG2VDR
+//***************************************************************************
+
+cPluginEPG2VDR::cPluginEPG2VDR()
+{
+ oUpdate = 0;
+ connection = 0;
+ timerDb = 0;
+ vdrDb = 0;
+}
+
+cPluginEPG2VDR::~cPluginEPG2VDR()
+{
+ exitDb();
+ delete oUpdate;
+}
+
+int cPluginEPG2VDR::initDb()
+{
+ int status = success;
+
+ exitDb();
+
+ connection = new cDbConnection();
+
+ timerDb = new cDbTable(connection, "timers");
+ if (timerDb->open() != success) return fail;
+
+ vdrDb = new cDbTable(connection, "vdrs");
+ if (vdrDb->open() != success) return fail;
+
+ // ----------
+ // select
+ // t.*,
+ // v.name, v.state
+ // from timers t, vdrs v
+ // where
+ // (t.state in ('P','R') or t.state is null)
+ // and t.vdruuid = v.uuid
+
+ selectTimers = new cDbStatement(timerDb);
+
+ selectTimers->build("select ");
+ selectTimers->setBindPrefix("t.");
+ selectTimers->bindAllOut();
+ selectTimers->setBindPrefix("v.");
+ selectTimers->bind(vdrDb, "NAME", cDBS::bndOut, ", ");
+ selectTimers->bind(vdrDb, "UUID", cDBS::bndOut, ", ");
+ selectTimers->bind(vdrDb, "STATE", cDBS::bndOut, ", ");
+ selectTimers->clrBindPrefix();
+ selectTimers->build(" from %s t, %s v where (t.%s in ('P','R') or t.%s is null)",
+ timerDb->TableName(), vdrDb->TableName(),
+ timerDb->getField("STATE")->getDbName(),
+ timerDb->getField("STATE")->getDbName());
+ selectTimers->build(" and t.%s = v.%s",
+ timerDb->getField("VDRUUID")->getDbName(),
+ vdrDb->getField("UUID")->getDbName());
+
+ status += selectTimers->prepare();
+
+ return status;
+}
+
+int cPluginEPG2VDR::exitDb()
+{
+ if (connection)
+ {
+ delete timerDb; timerDb = 0;
+ delete vdrDb; vdrDb = 0;
+ delete connection; connection = 0;
+ }
+
+ return done;
+}
+
+void cPluginEPG2VDR::DisplayMessage(const char *s)
+{
+ tell(0, "%s", s);
+
+ Skins.Message(mtInfo, tr(s));
+ sleep(Setup.OSDMessageTime);
+}
+
+const char *cPluginEPG2VDR::CommandLineHelp()
+{
+ return 0;
+}
+
+const char **cPluginEPG2VDR::SVDRPHelpPages()
+{
+ static const char *HelpPages[] =
+ {
+ "UPDATE\n"
+ " Load new/changed events database.",
+ "RELOAD\n"
+ " Reload all events from database to EPG",
+ "STATE <state>\n"
+ " For internal usage",
+ "TIMERJOB\n"
+ " Check timer jobs",
+ "UPDREC\n"
+ " Trigger update of recordings",
+ "STOREIFO\n"
+ " Trigger store of recordin info files",
+ 0
+ };
+
+ return HelpPages;
+}
+
+cString cPluginEPG2VDR::SVDRPCommand(const char* cmd, const char* Option, int &ReplyCode)
+{
+ // ------------------------------------
+ // update epg from database
+
+ if (strcasecmp(cmd, "UPDATE") == 0)
+ {
+ oUpdate->triggerEpgUpdate();
+ return "EPG2VDR update started.";
+ }
+
+ // ------------------------------------
+ // reload epg from database
+
+ else if (strcasecmp(cmd, "RELOAD") == 0)
+ {
+ oUpdate->triggerEpgUpdate(true);
+ return "EPG2VDR full reload of events from database.";
+ }
+
+ // ------------------------------------
+ // update recording table
+
+ else if (strcasecmp(cmd, "UPDREC") == 0)
+ {
+ oUpdate->triggerRecUpdate();
+ return "EPG2VDR update of recordings triggert.";
+ }
+
+ // ------------------------------------
+ // store recording info files
+
+ else if (strcasecmp(cmd, "STOREIFO") == 0)
+ {
+ oUpdate->triggerStoreInfoFiles();
+ return "EPG2VDR store of info files triggert.";
+ }
+
+ // ------------------------------------
+ // inform about epgd's state change
+
+ else if (strcasecmp(cmd, "STATE") == 0)
+ {
+ if (isEmpty(Option))
+ {
+ ReplyCode = 901;
+ return "Error: Missing option";
+ }
+
+ oUpdate->epgdStateChange(Option);
+ return "EPG2VDR epgd state change accepted";
+ }
+
+ // ------------------------------------
+ // trigger processing timer job
+
+ else if (strcasecmp(cmd, "TIMERJOB") == 0)
+ {
+ oUpdate->triggerTimerJobs();
+ return "EPG2VDR check of timer jobs triggered";
+ }
+
+ // ------------------------------------
+ // delete recording
+
+ else if (strcasecmp(cmd, "DELREC") == 0)
+ {
+ if (isEmpty(Option))
+ {
+ ReplyCode = 901;
+ return "Error: Missing option";
+ }
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ LOCK_RECORDINGS_WRITE;
+ cRecordings* recordings = Recordings;
+#else
+ cRecordings* recordings = &Recordings;
+#endif
+
+ if (cRecording* rec = recordings->GetByName(Option))
+ {
+ if (rec->IsInUse())
+ {
+ ReplyCode = 554;
+ tell(0, "Can't modify, recoring is in use");
+ return "Can't delete, recoring is in use";
+ }
+
+ if (!rec->Delete())
+ {
+ ReplyCode = 554;
+ tell(0, "Error: Delete of recording '%s' failed", Option);
+ return "Error: Delete of recording failed";
+ }
+
+ recordings->DelByName(rec->FileName());
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ recordings->SetModified();
+#endif
+ }
+ else
+ {
+ ReplyCode = 550;
+ tell(0, "Error: Delete failed, can't find recording '%s'", Option);
+ return "Error: Delete failed, can't find recording";
+ }
+
+ oUpdate->triggerRecUpdate();
+ tell(0, "Deleted recording '%s'", Option);
+ return "Deleted recording";
+ }
+
+ // ------------------------------------
+ // play recording
+
+ else if (strcasecmp(cmd, "PLAYREC") == 0)
+ {
+ if (isEmpty(Option))
+ {
+ ReplyCode = 901;
+ return "Error: Missing option";
+ }
+
+ char* opt = strdup(Option);
+ char* name = skipspace(opt);
+ char* position = skipspace(opt);
+
+ while (*position && !isspace(*position))
+ position++;
+
+ if (*position)
+ {
+ *position = 0;
+ position++;
+ }
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ LOCK_RECORDINGS_READ;
+ const cRecording* recording = Recordings->GetByName(name);
+#else
+ const cRecording* recording = Recordings.GetByName(name);
+#endif
+
+ if (!recording)
+ {
+ free(opt);
+ ReplyCode = 901;
+ return "Recording not found";
+ }
+
+ cReplayControl::SetRecording(0);
+ cControl::Shutdown();
+
+ if (!isEmpty(position))
+ {
+ int pos = 0;
+
+ if (strcasecmp(position, "BEGIN") != 0)
+ pos = HMSFToIndex(position, recording->FramesPerSecond());
+
+ cResumeFile Resume(recording->FileName(), recording->IsPesRecording());
+
+ if (pos <= 0)
+ Resume.Delete();
+ else
+ Resume.Save(pos);
+ }
+
+ cReplayControl::SetRecording(recording->FileName());
+ cControl::Launch(new cReplayControl);
+ cControl::Attach();
+ free(opt);
+ tell(0, "Playing recording '%s'", name);
+ return "Playing recording";
+ }
+
+ // ------------------------------------
+ // rename recording
+
+ else if (strcasecmp(cmd, "RENREC") == 0)
+ {
+ char* alt = 0;
+ char* neu = 0;
+ char* p;
+
+ if (!isEmpty(Option))
+ {
+ alt = strdup(Option);
+ p = alt;
+
+ if ((p = strchr(p, ' ')))
+ {
+ *p = 0;
+ neu = p+1;
+ }
+ }
+
+ if (isEmpty(neu) || isEmpty(alt))
+ {
+ free(alt);
+ ReplyCode = 901;
+ return "Error: Missing option";
+ }
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ LOCK_RECORDINGS_WRITE;
+ cRecordings* recordings = Recordings;
+ recordings->SetExplicitModify();
+#else
+ cRecordings* recordings = &Recordings;
+#endif
+
+ if (cRecording* rec = recordings->GetByName(alt))
+ {
+ if (rec->IsInUse())
+ {
+ ReplyCode = 554;
+ tell(0, "Can't modify, recoring is in use");
+ return "Can't modify, recoring is in use";
+ }
+ if (!rec->ChangeName(neu))
+ {
+ ReplyCode = 554;
+ tell(0, "Error: Modify of recordings '%s' failed", alt);
+ free(alt);
+ return "Error: Modify of recording failed";
+ }
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ recordings->SetModified();
+#endif
+ recordings->TouchUpdate();
+ }
+ else
+ {
+ ReplyCode = 550;
+ tell(0, "Error: Modify failed, can't find recording '%s'", alt);
+ free(alt);
+ return "Error: Modify failed, can't find recording";
+ }
+
+ oUpdate->triggerRecUpdate();
+ tell(0, "Modified recording '%s' to '%s'", alt, neu);
+ free(alt);
+ return "Modified recording";
+ }
+
+ return 0;
+}
+
+//***************************************************************************
+// Service
+//***************************************************************************
+
+bool cPluginEPG2VDR::Service(const char* id, void* data)
+{
+ if (!data)
+ return fail;
+
+ tell(4, "Service called with '%s', %d/%d", id,
+ Epg2VdrConfig.replaceScheduleMenu, Epg2VdrConfig.replaceTimerMenu);
+
+ if (strcmp(id, EPG2VDR_UUID_SERVICE) == 0)
+ {
+ Epg2vdr_UUID_v1_0* req = (Epg2vdr_UUID_v1_0*)data;
+
+ req->uuid = Epg2VdrConfig.uuid;
+
+ return true;
+ }
+
+ else if (strcmp(id, MYSQL_INIT_EXIT) == 0)
+ {
+ Mysql_Init_Exit_v1_0* ref = (Mysql_Init_Exit_v1_0*)data;
+
+ if (ref->action == mieaInit)
+ cDbConnection::init();
+ else if (ref->action == mieaExit)
+ cDbConnection::exit();
+ else
+ tell(0, "Warning: Got unexpected action %d in '%s' call",
+ ref->action, MYSQL_INIT_EXIT);
+
+ return true;
+ }
+
+ else if (strcmp(id, "MainMenuHooksPatch-v1.0::osSchedule") == 0 && Epg2VdrConfig.replaceScheduleMenu)
+ {
+ cOsdMenu** menu = (cOsdMenu**)data;
+
+ if (menu)
+ *menu = new cMenuEpgWhatsOn();
+
+ return true;
+ }
+
+ else if (strcmp(id, "MainMenuHooksPatch-v1.0::osTimers") == 0 && Epg2VdrConfig.replaceTimerMenu)
+ {
+ cOsdMenu** menu = (cOsdMenu**)data;
+
+ if (menu)
+ *menu = new cMenuEpgTimers();
+
+ return true;
+ }
+
+ else if (strcmp(id, EPG2VDR_TIMER_SERVICE) == 0)
+ {
+ cEpgTimer_Service_V1* ts = (cEpgTimer_Service_V1*)data;
+
+ if (ts)
+ return timerService(ts);
+ }
+
+ return false;
+}
+
+//***************************************************************************
+// Timer Service
+//***************************************************************************
+
+int cPluginEPG2VDR::timerService(cEpgTimer_Service_V1* ts)
+{
+ cMutexLock lock(&mutexTimerService);
+ uint64_t start = cTimeMs::Now();
+
+ if (initDb() == success)
+ {
+ timerDb->clear();
+ vdrDb->clear();
+
+ ts->epgTimers.clear();
+
+ for (int f = selectTimers->find(); f && connection->check() == success; f = selectTimers->fetch())
+ {
+ cEpgTimer* epgTimer = newTimerObjectFromRow(timerDb->getRow(), vdrDb->getRow());
+
+ if (Epg2VdrConfig.shareInWeb || epgTimer->isLocal())
+ ts->epgTimers.push_back(epgTimer);
+ else
+ delete epgTimer;
+ }
+
+ tell(1, "Answer '%s' call with %lu timers, duration was (%s)",
+ EPG2VDR_TIMER_SERVICE,
+ ts->epgTimers.size(),
+ ms2Dur(cTimeMs::Now()-start).c_str());
+ }
+
+ exitDb();
+
+ return true;
+}
+
+//***************************************************************************
+// Initialize
+//***************************************************************************
+
+bool cPluginEPG2VDR::Initialize()
+{
+ return true;
+}
+
+//***************************************************************************
+// Start
+//***************************************************************************
+
+bool cPluginEPG2VDR::Start()
+{
+ Mysql_Init_Exit_v1_0 req;
+
+ req.action = mieaInit;
+ Service(MYSQL_INIT_EXIT, &req);
+
+ oUpdate = new cUpdate(this);
+
+ if (oUpdate->init() == success)
+ oUpdate->Start(); // start plugin thread
+ else
+ tell(0, "Initialization failed, start of plugin aborted!");
+
+ return true;
+}
+
+cString cPluginEPG2VDR::Active()
+{
+ if (Epg2VdrConfig.activeOnEpgd && oUpdate->isEpgdBusy())
+ return tr("EPG2VDR Waiting on epgd");
+
+// time_t timeoutAt = time(0) + 10;
+
+// while (oUpdate->isUpdateActive())
+// {
+// tell(0, "EPG2VDR EPG update running, wating up to 10 seconds ..");
+
+// if (time(0) > timeoutAt)
+// {
+// tell(0, "EPG2VDR EPG update running, shutdown timed out, aborting");
+// return 0;
+// }
+
+// usleep(500000);
+// }
+
+ return 0;
+}
+
+time_t cPluginEPG2VDR::WakeupTime()
+{
+ // return custom wakeup time for shutdown script
+
+ if (Epg2VdrConfig.scheduleBoot && oUpdate->getNextEpgdUpdateAt())
+ return oUpdate->getNextEpgdUpdateAt();
+
+ return 0;
+}
+
+cOsdObject* cPluginEPG2VDR::MainMenuAction()
+{
+ return new cEpgPluginMenu(MAINMENUENTRY, this);
+}
+
+cMenuSetupPage* cPluginEPG2VDR::SetupMenu()
+{
+ return new cMenuSetupEPG2VDR;
+}
+
+bool cPluginEPG2VDR::SetupParse(const char *Name, const char *Value)
+{
+ // Parse your own setup parameters and store their values.
+
+ if (!strcasecmp(Name, "LogLevel")) Epg2VdrConfig.loglevel = atoi(Value);
+ else if (!strcasecmp(Name, "ShowInMainMenu")) Epg2VdrConfig.mainmenuVisible = atoi(Value);
+ else if (!strcasecmp(Name, "Blacklist")) Epg2VdrConfig.blacklist = atoi(Value);
+ else if (!strcasecmp(Name, "DbHost")) sstrcpy(Epg2VdrConfig.dbHost, Value, sizeof(Epg2VdrConfig.dbHost));
+ else if (!strcasecmp(Name, "DbPort")) Epg2VdrConfig.dbPort = atoi(Value);
+ else if (!strcasecmp(Name, "DbName")) sstrcpy(Epg2VdrConfig.dbName, Value, sizeof(Epg2VdrConfig.dbName));
+ else if (!strcasecmp(Name, "DbUser")) sstrcpy(Epg2VdrConfig.dbUser, Value, sizeof(Epg2VdrConfig.dbUser));
+ else if (!strcasecmp(Name, "DbPass")) sstrcpy(Epg2VdrConfig.dbPass, Value, sizeof(Epg2VdrConfig.dbPass));
+ else if (!strcasecmp(Name, "MasterMode")) Epg2VdrConfig.masterMode = atoi(Value);
+ else if (!strcasecmp(Name, "LoadImages")) Epg2VdrConfig.getepgimages = atoi(Value);
+ else if (!strcasecmp(Name, "ActiveOnEpgd")) Epg2VdrConfig.activeOnEpgd = atoi(Value);
+ else if (!strcasecmp(Name, "ScheduleBoot")) Epg2VdrConfig.scheduleBoot = atoi(Value);
+ else if (!strcasecmp(Name, "UseCommonRecFolder")) Epg2VdrConfig.useCommonRecFolder = atoi(Value);
+ else if (!strcasecmp(Name, "ShareInWeb")) Epg2VdrConfig.shareInWeb = atoi(Value);
+ else if (!strcasecmp(Name, "CreateTimerLocal")) Epg2VdrConfig.createTimerLocal = atoi(Value);
+ else if (!strcasecmp(Name, "XChgKeyOkBlue")) Epg2VdrConfig.xchgOkBlue = atoi(Value);
+ else if (!strcasecmp(Name, "ShowEmptyChannels")) Epg2VdrConfig.showEmptyChannels = atoi(Value);
+ else if (!strcasecmp(Name, "Uuid")) sstrcpy(Epg2VdrConfig.uuid, Value, sizeof(Epg2VdrConfig.uuid));
+ else if (!strcasecmp(Name, "NetDevice")) sstrcpy(Epg2VdrConfig.netDevice, Value, sizeof(Epg2VdrConfig.netDevice));
+ else if (!strcasecmp(Name, "ReplaceScheduleMenu")) Epg2VdrConfig.replaceScheduleMenu = atoi(Value);
+ else if (!strcasecmp(Name, "ReplaceTimerMenu")) Epg2VdrConfig.replaceTimerMenu = atoi(Value);
+ else if (!strcasecmp(Name, "User")) sstrcpy(Epg2VdrConfig.user, Value, sizeof(Epg2VdrConfig.user));
+
+ else
+ return false;
+
+ return true;
+}
+
+void cPluginEPG2VDR::Stop()
+{
+ oUpdate->Stop();
+
+ Mysql_Init_Exit_v1_0 req;
+
+ req.action = mieaExit;
+ Service(MYSQL_INIT_EXIT, &req);
+}
+
+//***************************************************************************
+
+VDRPLUGINCREATOR(cPluginEPG2VDR)
diff --git a/epg2vdr.h b/epg2vdr.h
new file mode 100644
index 0000000..d2f05a2
--- /dev/null
+++ b/epg2vdr.h
@@ -0,0 +1,80 @@
+/*
+ * epg2vdr.h: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __EPG2VDR_H
+#define __EPG2VDR_H
+
+#include <list>
+
+#include <vdr/plugin.h>
+#include "plgconfig.h"
+#include "service.h"
+#include "HISTORY.h"
+
+#include "lib/db.h"
+
+class cUpdate;
+class cEpg2VdrEpgHandler;
+extern cUpdate* oUpdate;
+
+//***************************************************************************
+// Constants
+//***************************************************************************
+
+static const char* DESCRIPTION = trNOOP("epg2vdr plugin");
+static const char* MAINMENUENTRY = tr("EPG and Timer Service");
+
+//***************************************************************************
+// cPluginEPG2VDR
+//***************************************************************************
+
+cOsdMenu* newWathsOn();
+
+class cPluginEPG2VDR : public cPlugin
+{
+ public:
+
+ cPluginEPG2VDR(void);
+ virtual ~cPluginEPG2VDR();
+
+ virtual const char* Version(void) { return VERSION; }
+ virtual const char* VersionDate(void) { return VERSION_DATE; }
+ virtual const char* Description(void) { return tr(DESCRIPTION); }
+ virtual const char* CommandLineHelp(void);
+ virtual bool Service(const char* id, void* data);
+ virtual const char** SVDRPHelpPages(void);
+ virtual cString SVDRPCommand(const char* Cmd, const char* Option, int& ReplyCode);
+ virtual bool Initialize(void);
+ virtual bool Start(void);
+ virtual cString Active(void);
+ virtual const char* MainMenuEntry(void)
+ { return Epg2VdrConfig.mainmenuVisible ? MAINMENUENTRY : 0; }
+ virtual cOsdObject* MainMenuAction(void);
+ virtual cMenuSetupPage* SetupMenu(void);
+ virtual bool SetupParse(const char* Name, const char* Value);
+ virtual void Stop();
+ virtual void DisplayMessage(const char* s);
+ virtual time_t WakeupTime(void);
+
+ protected:
+
+ int initDb();
+ int exitDb();
+
+ int timerService(cEpgTimer_Service_V1* ts);
+
+ private:
+
+ cDbConnection* connection;
+ cDbTable* timerDb;
+ cDbTable* vdrDb;
+ cDbStatement* selectTimers;
+ cMutex mutexTimerService;
+};
+
+//***************************************************************************
+#endif // EPG2VDR_H
diff --git a/handler.h b/handler.h
new file mode 100644
index 0000000..f341d94
--- /dev/null
+++ b/handler.h
@@ -0,0 +1,1141 @@
+/*
+ * handler.h: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __HANDLER_H
+#define __HANDLER_H
+
+#include "update.h"
+
+#define CHANNELMARKOBSOLETE "OBSOLETE"
+
+//***************************************************************************
+// Define tEventID again, to create a compiler error case it was defines different
+//***************************************************************************
+
+typedef u_int32_t tEventID; // on error vdr >= 2.3.1 and patch is not applied!!
+
+//***************************************************************************
+// Mutex Try
+//***************************************************************************
+
+class cMutexTry
+{
+ public:
+
+ cMutexTry()
+ {
+ locked = 0;
+
+ pthread_mutexattr_t attr;
+ pthread_mutexattr_init(&attr);
+ pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
+ pthread_mutex_init(&mutex, &attr);
+ }
+
+ ~cMutexTry()
+ {
+ pthread_mutex_destroy(&mutex);
+ }
+
+ int tryLock()
+ {
+ if (pthread_mutex_trylock(&mutex) == 0)
+ {
+ locked++;
+ // tell(0, "[%d] got lock (%d) [%p]", cThread::ThreadId(), locked, this);
+
+ return yes;
+ }
+
+ // tell(0, "[%d] don't got lock (%d) [%p]", cThread::ThreadId(), locked, this);
+
+ return no;
+ }
+
+ void lock()
+ {
+ pthread_mutex_lock(&mutex);
+ locked++;
+ }
+
+ void unlock()
+ {
+ if (locked)
+ {
+ // tell(0, "[%d] unlock (%d) [%p]", cThread::ThreadId(), locked, this);
+ locked--;
+ pthread_mutex_unlock(&mutex);
+ }
+ }
+
+ private:
+
+ pthread_mutex_t mutex;
+ int locked;
+};
+
+//***************************************************************************
+// EPG Handler
+//***************************************************************************
+
+class cEpgHandlerInstance
+{
+ public:
+
+ cEpgHandlerInstance()
+ {
+ initialized = no;
+ connection = 0;
+ eventsDb = 0;
+ compDb = 0;
+ mapDb = 0;
+ vdrDb = 0;
+ endTime = 0;
+ updateDelFlg = 0;
+ selectDelFlg = 0;
+ delCompOf = 0;
+ selectMergeSp = 0;
+ selectEventByStarttime = 0;
+
+ tell(0, "Handler: Init handler instance for thread %d", cThread::ThreadId());
+ }
+
+ virtual ~cEpgHandlerInstance() { exitDb(); }
+
+ virtual int dbConnected(int force = no)
+ {
+ return initialized
+ && connection
+ && (!force || connection->check() == success);
+ }
+
+ int checkConnection()
+ {
+ static int retry = 0;
+
+ // check connection
+
+ if (!dbConnected(yes))
+ {
+ // try to connect
+
+ tell(0, "Handler: Trying to re-connect to database!");
+ retry++;
+
+ if (initDb() != success)
+ {
+ exitDb();
+
+ tell(0, "Handler: Database re-connect failed!");
+ return fail;
+ }
+
+ retry = 0;
+ tell(0, "Handler: Connection established successfull!");
+ }
+
+ return success;
+ }
+
+ int isEpgdBusy()
+ {
+ int busy = no;
+
+ if (!dbConnected())
+ return true;
+
+ vdrDb->clear();
+ vdrDb->setValue("Uuid", EPGDNAME);
+
+ if (vdrDb->find())
+ {
+ if (strcasecmp(vdrDb->getStrValue("State"), "busy (events)") == 0)
+ busy = yes;
+ }
+
+ vdrDb->reset();
+
+ return busy;
+ }
+
+ int initDb()
+ {
+ int status = success;
+
+ exitDb();
+
+ connection = new cDbConnection();
+
+ vdrDb = new cDbTable(connection, "vdrs");
+ if (vdrDb->open() != success) return fail;
+
+ mapDb = new cDbTable(connection, "channelmap");
+ if (mapDb->open() != success) return fail;
+
+ eventsDb = new cDbTable(connection, "events");
+ if (eventsDb->open() != success) return fail;
+
+ compDb = new cDbTable(connection, "components");
+ if (compDb->open() != success) return fail;
+
+ // select
+ // * from events
+ // where
+ // source = 'vdr'
+ // and starttime = ?
+ // and channelid = ?
+
+ selectEventByStarttime = new cDbStatement(eventsDb);
+
+ selectEventByStarttime->build("select ");
+ selectEventByStarttime->bindAllOut(0, cDBS::ftAll);
+ selectEventByStarttime->build(" from %s where source = 'vdr'", eventsDb->TableName());
+ selectEventByStarttime->bind("STARTTIME", cDBS::bndIn | cDBS::bndSet, " and ");
+ selectEventByStarttime->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet, " and ");
+
+ status += selectEventByStarttime->prepare();
+
+ // prepare statement to get mapsp
+ // select
+ // mergesp from channelmap
+ // where
+ // source != 'vdr'
+ // and channelid = ? limit 1
+
+ selectMergeSp = new cDbStatement(mapDb);
+
+ selectMergeSp->build("select ");
+ selectMergeSp->bind("MergeSp", cDBS::bndOut);
+ selectMergeSp->build(" from %s where source != 'vdr'", mapDb->TableName());
+ selectMergeSp->bind("ChannelId", cDBS::bndIn | cDBS::bndSet, " and ");
+ selectMergeSp->build(" limit 1");
+
+ status += selectMergeSp->prepare();
+
+ // prepare statement to mark wasted DVB events
+
+ // update events set delflg = ?, updsp = ?
+ // where channelid = ? and source = ?
+ // and starttime+duration > ?
+ // and starttime < ?
+ // and (tableid > ? or (tableid = ? and version <> ?))
+
+ endTime = new cDbValue("starttime+duration", cDBS::ffInt, 10);
+ updateDelFlg = new cDbStatement(eventsDb);
+
+ updateDelFlg->build("update %s set ", eventsDb->TableName());
+ updateDelFlg->bind("DelFlg", cDBS::bndIn | cDBS::bndSet);
+ updateDelFlg->bind("UpdFlg", cDBS::bndIn | cDBS::bndSet, ", ");
+ updateDelFlg->bind("UpdSp", cDBS::bndIn | cDBS::bndSet, ", ");
+ updateDelFlg->build(" where ");
+ updateDelFlg->bind("ChannelId", cDBS::bndIn | cDBS::bndSet);
+ updateDelFlg->bind("Source", cDBS::bndIn | cDBS::bndSet, " and ");
+ updateDelFlg->bindCmp(0, endTime, ">" , " and ");
+ updateDelFlg->bindCmp(0, "StartTime", 0, "<" , " and ");
+ updateDelFlg->bindCmp(0, "TableId", 0, ">" , " and (");
+ updateDelFlg->bindCmp(0, "TableId", 0, "=" , " or (");
+ updateDelFlg->bindCmp(0, "Version", 0, "<>" , " and ");
+ updateDelFlg->build("));");
+
+ status += updateDelFlg->prepare();
+
+ // we need the same as select :(
+
+ selectDelFlg = new cDbStatement(eventsDb);
+
+ selectDelFlg->build("select ");
+ selectDelFlg->bind("EventId", cDBS::bndOut);
+ selectDelFlg->build(" from %s where ", eventsDb->TableName());
+ selectDelFlg->bind("ChannelId", cDBS::bndIn | cDBS::bndSet);
+ selectDelFlg->bind("Source", cDBS::bndIn | cDBS::bndSet, " and ");
+ selectDelFlg->bindCmp(0, endTime, ">" , " and ");
+ selectDelFlg->bindCmp(0, "StartTime", 0, "<" , " and ");
+ selectDelFlg->bindCmp(0, "TableId", 0, ">" , " and (");
+ selectDelFlg->bindCmp(0, "TableId", 0, "=" , " or (");
+ selectDelFlg->bindCmp(0, "Version", 0, "<>" , " and ");
+ selectDelFlg->build("));");
+
+ status += selectDelFlg->prepare();
+
+ // -----------------
+ // delete from components where eventid = ?;
+
+ delCompOf = new cDbStatement(compDb);
+
+ delCompOf->build("delete from %s where ", compDb->TableName());
+ delCompOf->bind("EventId", cDBS::bndIn | cDBS::bndSet);
+ delCompOf->build(";");
+
+ status += delCompOf->prepare();
+
+ if (status == success)
+ {
+ status += updateMemList();
+ status += updateExternalIdsMap();
+ }
+
+ initialized = yes;
+ return status;
+ }
+
+ int exitDb()
+ {
+ initialized = no;
+
+ if (connection)
+ {
+ delete endTime; endTime = 0;
+ delete updateDelFlg; updateDelFlg = 0;
+ delete selectDelFlg; selectDelFlg = 0;
+ delete delCompOf; delCompOf = 0;
+
+ delete vdrDb; vdrDb = 0;
+ delete eventsDb; eventsDb = 0;
+ delete compDb; compDb = 0;
+ delete mapDb; mapDb = 0;
+ delete selectMergeSp; selectMergeSp = 0;
+ delete selectEventByStarttime; selectEventByStarttime = 0;
+
+ delete connection; connection = 0;
+ }
+
+ return done;
+ }
+
+ int updateMemList()
+ {
+ time_t start = time(0);
+
+ evtMemList.clear();
+
+ // select eventid, channelid, version, tableid, delflg
+ // from events where source = 'vdr'
+
+ cDbStatement* selectAllVdrEvents = new cDbStatement(eventsDb);
+
+ selectAllVdrEvents->build("select ");
+ selectAllVdrEvents->bind("EventId", cDBS::bndOut);
+ selectAllVdrEvents->bind("ChannelId", cDBS::bndOut, ", ");
+ selectAllVdrEvents->bind("Version", cDBS::bndOut, ", ");
+ selectAllVdrEvents->bind("TableId", cDBS::bndOut, ", ");
+ selectAllVdrEvents->bind("DelFlg", cDBS::bndOut, ", ");
+ selectAllVdrEvents->build(" from %s where source = 'vdr'", eventsDb->TableName());
+
+ if (selectAllVdrEvents->prepare() != success)
+ {
+ tell(0, "Handler: Aborted reading hashes from db due to prepare error");
+ delete selectAllVdrEvents;
+ return fail;
+ }
+
+ tell(1, "Handler: Start reading hashes from db");
+
+ eventsDb->clear();
+
+ for (int f = selectAllVdrEvents->find(); f; f = selectAllVdrEvents->fetch())
+ {
+ char evtKey[100];
+
+ if (eventsDb->hasValue("DelFlg", "Y"))
+ continue;
+
+ sprintf(evtKey, "%ld:%s",
+ eventsDb->getIntValue("STARTTIME"),
+ eventsDb->getStrValue("CHANNELID"));
+
+ evtMemList[evtKey].version = eventsDb->getIntValue("Version");
+ evtMemList[evtKey].tableid = eventsDb->getIntValue("TableId");
+
+ tell(4, "Handler: cInsert: '%s' with %d/%d", evtKey, evtMemList[evtKey].tableid, evtMemList[evtKey].version);
+ }
+
+ selectAllVdrEvents->freeResult();
+ delete selectAllVdrEvents;
+
+ tell(1, "Handler: Finished reading hashes from db, got %d hashes (in %ld seconds)",
+ (int)evtMemList.size(), time(0)-start);
+
+ return success;
+ }
+
+ //***************************************************************************
+ // Ignore Channel
+ //***************************************************************************
+
+ virtual bool IgnoreChannel(const cChannel* Channel)
+ {
+ static time_t nextRetryAt = time(0);
+ LogDuration l("IgnoreChannel", 5);
+
+ // if this method is called the channel is
+ // for the db an we have the active role!
+ // (already checked by cEpg2VdrEpgHandler)
+
+ // only check the DB connection here!!
+
+ if (!dbConnected() && time(0) < nextRetryAt)
+ return true;
+
+ if (checkConnection() != success || externIdMap.size() < 1)
+ {
+ nextRetryAt = time(0) + 60;
+ return true;
+ }
+
+ return false;
+ }
+
+ //***************************************************************************
+ // Transaction Stuff
+ //***************************************************************************
+
+ virtual bool BeginSegmentTransfer(const cChannel* Channel, bool dummy)
+ {
+ // inital die channelid setzen
+
+ channelId = Channel->GetChannelID();
+
+ return false;
+ }
+
+ //***************************************************************************
+ // Handled Externally
+ // hier wird festgelegt ob das Event via VDR im EPG landen soll
+ //***************************************************************************
+
+ virtual bool HandledExternally(const cChannel* Channel)
+ {
+ static int reportFirst = yes;
+
+ LogDuration l("HandledExternally", 5);
+
+ if (!channelId.Valid())
+ {
+ // if 'Channel' valid and 'channelId' not valid assume SegmentTransfer patch is missing
+ // -> report this ..
+
+ if (Channel->GetChannelID().Valid() && reportFirst)
+ {
+ reportFirst = no;
+ tell(0, "Handler: WARNING: Assume 'SegmentTransfer' patch is missing, "
+ "VDR < 2.1.0 have to be patched! Or disable the handler in the plugin setup");
+ }
+
+ return false;
+ }
+
+ // dbConnected() here okay -> if not connected we return false an the vdr will handle the event?!?
+
+ if (dbConnected() && getExternalIdOfChannel(&channelId) != "")
+ return true;
+
+ return false;
+ }
+
+ virtual bool EndSegmentTransfer(bool Modified, bool dummy)
+ {
+ if (dummy || !dbConnected() || !connection->inTransaction())
+ return false;
+
+ if (Modified)
+ connection->commit();
+ else
+ connection->rollback();
+
+ if (Epg2VdrConfig.loglevel > 2)
+ connection->showStat("handler");
+
+ return false;
+ }
+
+ //***************************************************************************
+ // Is Update
+ //***************************************************************************
+
+ virtual bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version)
+ {
+ char evtKey[100];
+ LogDuration l("IsUpdate", 5);
+
+ if (!dbConnected())
+ return false;
+
+ if (!isZero(getExternalIdOfChannel(&channelId).c_str()) && StartTime > time(0) + 4 * tmeSecondsPerDay)
+ return false;
+
+ sprintf(evtKey, "%ld:%s", StartTime, (const char*)channelId.ToString());
+
+ if (evtMemList.find(evtKey) == evtMemList.end())
+ {
+ tell(4, "Handler: Handle insert (or starttime update) of event '%s' (%d) for channel '%s'",
+ evtKey, EventID, (const char*)channelId.ToString());
+
+ if (!connection->inTransaction())
+ connection->startTransaction();
+
+ return true;
+ }
+
+ uchar currentTableId = ::max(uchar(evtMemList[evtKey].tableid), uchar(0x4E));
+
+ // skip bigger ids as current
+
+ if (TableID > currentTableId)
+ {
+ tell(4, "Handler: Ignoring update with older tableid (%d) for event '%s' (%d)(has tableid %d)",
+ TableID, evtKey, EventID, evtMemList[evtKey].tableid);
+ return false;
+ }
+
+ // skip if version an tid identical
+
+ if (currentTableId == TableID && evtMemList[evtKey].version == Version)
+ {
+ tell(4, "Handler: Ignoring 'non' update for event '%s' (%d), version still (%d)",
+ evtKey, EventID, Version);
+ return false;
+ }
+
+ if (Epg2VdrConfig.loglevel > 3)
+ tell(4, "Handler: Handle update of event '%s' (%d) %d/%d - %d/%d", evtKey, EventID,
+ Version, TableID,
+ evtMemList[evtKey].version, evtMemList[evtKey].tableid);
+
+ if (!connection->inTransaction())
+ connection->startTransaction();
+
+ return true;
+ }
+
+ //***************************************************************************
+ // Handle Event
+ //***************************************************************************
+
+ virtual bool HandleEvent(cEvent* event)
+ {
+ int oldStartTime = 0;
+
+ if (!dbConnected() || !event || !channelId.Valid())
+ return false;
+
+ LogDuration l("HandleEvent", 5);
+
+ // External-ID:
+ // na -> Kanal nicht konfiguriert -> wird ignoriert
+ // = 0 -> wird vom Sender genommen -> und in der DB abgelegt
+ // > 0 -> echte externe ID -> wird von extern ins epg übertragen,
+ // das Sender EPG wird nur (zusätzlich) in der DB gehalten
+
+ // Events der Kanäle welche nicht in der map zu finden sind ignorieren
+
+ if (getExternalIdOfChannel(&channelId) == "")
+ return false;
+
+ if (!connection->inTransaction())
+ {
+ tell(0, "Handler: Error missing tact in HandleEvent");
+ return false;
+ }
+
+ std::string comp;
+
+ // lookup the event ..
+ // first try by starttime
+
+ eventsDb->clear();
+ eventsDb->setValue("CHANNELID", channelId.ToString());
+ eventsDb->setValue("STARTTIME", event->StartTime());
+
+ int insert = !selectEventByStarttime->find();
+
+ if (insert)
+ {
+ // try lookup by eventid
+
+ eventsDb->setValue("CHANNELID", channelId.ToString());
+ eventsDb->setBigintValue("EVENTID", (long)event->EventID());
+
+ if (eventsDb->find())
+ {
+ // fount => NOT a insert, just a update of the starttime
+
+ insert = no;
+ oldStartTime = eventsDb->getIntValue("STARTTIME");
+ eventsDb->setValue("STARTTIME", event->StartTime());
+ }
+ }
+
+ // reinstate ??
+
+ if (eventsDb->hasValue("DELFLG", "Y"))
+ {
+ char updFlg = Us::usPassthrough;
+
+ mapDb->clear();
+ mapDb->setValue("CHANNELID", channelId.ToString());
+
+ if (selectMergeSp->find())
+ {
+ time_t mergesp = mapDb->getIntValue("MERGESP");
+ long masterid = eventsDb->getIntValue("MASTERID");
+ long useid = eventsDb->getIntValue("USEID");
+
+ if (event->StartTime() > mergesp)
+ updFlg = Us::usRemove;
+ else if (event->StartTime() <= mergesp && masterid == useid)
+ updFlg = Us::usActive;
+ else if (event->StartTime() <= mergesp && masterid != useid)
+ updFlg = Us::usLink;
+ }
+
+ eventsDb->setCharValue("UPDFLG", updFlg);
+ eventsDb->getValue("DELFLG")->setNull();
+ }
+
+ if (!insert && abs(event->StartTime() - eventsDb->getIntValue("StartTime")) > 6*tmeSecondsPerHour)
+ {
+ tell(3, "Handler: Info: Start time of %d/%s - '%s' moved %ld hours from %s to %s - '%s'",
+ event->EventID(), (const char*)channelId.ToString(),
+ eventsDb->getStrValue("Title"),
+ (event->StartTime() - eventsDb->getIntValue("StartTime")) / tmeSecondsPerHour,
+ l2pTime(eventsDb->getIntValue("StartTime")).c_str(),
+ l2pTime(event->StartTime()).c_str(),
+ event->Title());
+ }
+
+ time_t end = event->StartTime() + event->Duration();
+
+ if (!insert
+ && end < time(0) - 2*tmeSecondsPerHour
+ && eventsDb->getIntValue("StartTime") > time(0)
+ && event->StartTime() < eventsDb->getIntValue("StartTime"))
+ {
+ tell(1, "Handler: Info: Got update of %d/%s with startime more than 2h in past "
+ "(%s/%d) before (%s), ignoring update, set delflg instead",
+ event->EventID(), (const char*)channelId.ToString(),
+ l2pTime(event->StartTime()).c_str(), event->Duration(),
+ l2pTime(eventsDb->getIntValue("StartTime")).c_str());
+
+ eventsDb->setValue("DelFlg", "Y");
+ eventsDb->setCharValue("UpdFlg", Us::usDelete);
+ }
+ else
+ {
+ eventsDb->setValue("StartTime", event->StartTime());
+ }
+
+ if (!insert && eventsDb->getIntValue("VPS") != event->Vps())
+ tell(1, "Handler: Toggle vps flag for '%s' at '%s' from %s to %s",
+ event->Title(), (const char*)channelId.ToString(),
+ l2pTime(eventsDb->getIntValue("VPS")).c_str(), l2pTime(event->Vps()).c_str());
+
+ eventsDb->setValue("Source", "vdr");
+ eventsDb->setValue("TableId", event->TableID());
+ eventsDb->setValue("Version", event->Version());
+ eventsDb->setValue("Title", event->Title());
+ eventsDb->setValue("LongDescription", event->Description());
+ eventsDb->setValue("Duration", event->Duration());
+ eventsDb->setValue("ParentalRating", event->ParentalRating());
+ eventsDb->setValue("Vps", event->Vps());
+
+ if (!isEmpty(event->ShortText()))
+ eventsDb->setValue("ShortText", event->ShortText());
+
+ // contents
+
+ char contents[MaxEventContents * 10] = "";
+
+ for (int i = 0; i < MaxEventContents; i++)
+ if (event->Contents(i) > 0)
+ sprintf(eos(contents), "0x%x,", event->Contents(i));
+
+ if (!isEmpty(contents))
+ eventsDb->setValue("CONTENTS", contents);
+
+ // components ..
+
+ compDb->clear();
+ compDb->setBigintValue("EVENTID", eventsDb->getBigintValue("EVENTID"));
+ delCompOf->execute();
+
+ if (event->Components())
+ {
+ for (int i = 0; i < event->Components()->NumComponents(); i++)
+ {
+ tComponent* p = event->Components()->Component(i);
+
+ compDb->clear();
+ compDb->setBigintValue("EventId", eventsDb->getBigintValue("EVENTID"));
+ compDb->setValue("ChannelId", channelId.ToString());
+ compDb->setValue("Stream", p->stream);
+ compDb->setValue("Type", p->type);
+ compDb->setValue("Lang", p->language);
+ compDb->setValue("Description", p->description ? p->description : "");
+ compDb->store();
+ }
+ }
+
+ // compressed ..
+
+ if (event->Title())
+ {
+ comp = event->Title();
+ prepareCompressed(comp);
+ eventsDb->setValue("COMPTITLE", comp.c_str());
+ }
+
+ if (!isEmpty(event->ShortText()))
+ {
+ comp = event->ShortText();
+ prepareCompressed(comp);
+ eventsDb->setValue("COMPSHORTTEXT", comp.c_str());
+ }
+
+ if (!isEmpty(event->Description()))
+ {
+ comp = event->Description();
+ prepareCompressed(comp);
+ eventsDb->setValue("COMPLONGDESCRIPTION", comp.c_str());
+ }
+
+ if (insert)
+ {
+ eventsDb->setValue("UseId", 0L);
+ eventsDb->setCharValue("UpdFlg", Us::usPassthrough); // default (vdr:000 events)
+
+ mapDb->clear();
+ mapDb->setValue("ChannelId", channelId.ToString());
+
+ if (selectMergeSp->find())
+ {
+ // vdr event for merge with external event
+
+ time_t mergesp = mapDb->getIntValue("MergeSp");
+ eventsDb->setCharValue("UpdFlg", event->StartTime() > mergesp ? Us::usInactive : Us::usActive);
+ }
+
+ eventsDb->insert();
+ }
+ else
+ {
+ eventsDb->update();
+ }
+
+ if (!insert && oldStartTime)
+ {
+ char evtKey[100];
+ sprintf(evtKey, "%ld:%s", (long)oldStartTime, (const char*)channelId.ToString());
+ evtMemList.erase(evtKey);
+ tell(4, "Handler: cRemove: '%s' (due to starttime update)", evtKey);
+ }
+
+ // update hash map
+
+ char evtKey[100];
+
+ sprintf(evtKey, "%ld:%s", (long)event->StartTime(), (const char*)channelId.ToString());
+
+ evtMemList[evtKey].version = event->Version();
+ evtMemList[evtKey].tableid = event->TableID();
+
+ tell(4, "Handler: cUpdate/cInsert: '%s' to %d/%d", evtKey, evtMemList[evtKey].tableid, evtMemList[evtKey].version);
+
+ selectEventByStarttime->freeResult();
+
+ return true;
+ }
+
+ //***************************************************************************
+ // Drop Outdated
+ //***************************************************************************
+
+ virtual bool DropOutdated(cSchedule* Schedule, time_t SegmentStart,
+ time_t SegmentEnd, uchar TableID, uchar Version)
+ {
+ // we handle only vdr events here (provided by DVB)
+
+ if (!dbConnected() || getExternalIdOfChannel(&channelId) == "")
+ return false;
+
+ if (!connection->inTransaction())
+ {
+ tell(0, "Handler: Error missing tact DropOutdated");
+ return false;
+ }
+
+ if (SegmentStart <= 0 || SegmentEnd <= 0)
+ return false;
+
+ LogDuration l("DropOutdated", 5);
+
+ eventsDb->clear();
+ eventsDb->setValue("ChannelId", Schedule->ChannelID().ToString());
+ eventsDb->setValue("Source", "vdr");
+ eventsDb->setValue("UpdSp", time(0));
+ eventsDb->setValue("StartTime", SegmentEnd);
+ eventsDb->setValue("TableId", TableID);
+ eventsDb->setValue("Version", Version);
+ endTime->setValue(SegmentStart);
+
+ // remove segment from cache
+
+ for (int f = selectDelFlg->find(); f; f = selectDelFlg->fetch())
+ {
+ char evtKey[100];
+
+ sprintf(evtKey, "%ld:%s",
+ eventsDb->getIntValue("STARTTIME"),
+ eventsDb->getStrValue("CHANNELID"));
+
+ evtMemList.erase(evtKey);
+ tell(4, "Handler: cRemove: '%s'", evtKey);
+ }
+
+ selectDelFlg->freeResult();
+
+ eventsDb->setValue("DELFLG", "Y");
+ eventsDb->setCharValue("UPDFLG", Us::usDelete);
+
+ // mark segment as deleted
+
+ updateDelFlg->execute();
+
+ return true;
+ }
+
+ private:
+
+ std::string getExternalIdOfChannel(tChannelID* channelId)
+ {
+ char* id = 0;
+ std::string extid = "";
+
+ if (externIdMap.size() < 1)
+ return "";
+
+ id = strdup(channelId->ToString());
+
+ if (externIdMap.find(id) != externIdMap.end())
+ extid = externIdMap[id];
+
+ free(id);
+
+ return extid;
+ }
+
+ int updateExternalIdsMap()
+ {
+ externIdMap.clear();
+ tell(1, "Handler: Start reading external ids from db");
+ mapDb->clear();
+
+ // select extid, channelid
+ // from channelmap
+
+ cDbStatement* selectAll = new cDbStatement(mapDb);
+
+ selectAll->build("select ");
+ selectAll->bind("ExternalId", cDBS::bndOut);
+ selectAll->bind("ChannelId", cDBS::bndOut, ", ");
+ selectAll->build(" from %s", mapDb->TableName());
+
+ if (selectAll->prepare() != success)
+ {
+ tell(0, "Handler: Reading external id's from db aborted due to prepare error");
+ delete selectAll;
+ return fail;
+ }
+
+ for (int f = selectAll->find(); f; f = selectAll->fetch())
+ {
+ const char* extid = mapDb->getStrValue("ExternalId");
+ const char* chan = mapDb->getStrValue("ChannelId");
+
+ externIdMap[chan] = extid;
+ }
+
+ tell(1, "Handler: Finished reading external id's from db, got %d id's",
+ (int)externIdMap.size());
+
+ selectAll->freeResult();
+ delete selectAll;
+
+ return success;
+ }
+
+ struct MemMap
+ {
+ int version;
+ int tableid;
+ };
+
+ std::map<std::string,MemMap> evtMemList;
+ std::map<std::string,std::string> externIdMap;
+ tChannelID channelId;
+
+ int initialized;
+ cDbConnection* connection;
+
+ cDbTable* eventsDb;
+ cDbTable* mapDb;
+ cDbTable* vdrDb;
+ cDbTable* compDb;
+
+ cDbValue* endTime;
+ cDbStatement* updateDelFlg;
+ cDbStatement* selectDelFlg;
+ cDbStatement* delCompOf;
+ cDbStatement* selectMergeSp;
+ cDbStatement* selectEventByStarttime;
+
+ cUpdate* update;
+};
+
+//***************************************************************************
+// cEpg2VdrEpgHandler
+//***************************************************************************
+
+class cEpg2VdrEpgHandler : public cEpgHandler
+{
+ public:
+
+ cEpg2VdrEpgHandler() : cEpgHandler() { active = no; }
+
+ ~cEpg2VdrEpgHandler()
+ {
+ handlerMutex.lock();
+
+ std::map<tThreadId,cEpgHandlerInstance*>::iterator it;
+
+ for (it = handler.begin(); it != handler.end(); ++it)
+ {
+ delete handler[it->first];
+ handler[it->first] = 0;
+ }
+
+ handlerMutex.unlock();
+ }
+
+ int getActive() { return active; }
+ void setActive(int state) { active = state; }
+
+ //***************************************************************************
+ // Ignore Channel
+ // - includes the NOEPG feature - so we don't need the noepg plugin
+ //***************************************************************************
+
+ virtual bool IgnoreChannel(const cChannel* Channel)
+ {
+ // IgnoreChannel ist der erste Anlaufpunkt des EIT handlers,
+ // wird hier ignoriert bricht die Verarbeitung des Kanals direkt ab
+
+ tChannelID tmp = Channel->GetChannelID();
+
+ if (externIdMap.size() < 1)
+ return true;
+
+ // Kanäle welche nicht in der map konfiguriert sind (na) werden nicht in der DB verwaltet
+ // abhängig von blacklist durchgelassen oder ignoriert
+
+ if (getExternalIdOfChannel(&tmp) == "")
+ return Epg2VdrConfig.blacklist;
+
+ // ingnore - wenn nicht aktiv
+
+ if (!active)
+ return true;
+
+ // Handler check - only to check if the DB connection is fine
+
+ if (getHandler()->IgnoreChannel(Channel))
+ return true;
+
+ return false;
+ }
+
+ virtual bool HandledExternally(const cChannel* Channel)
+ {
+ return getHandler()->HandledExternally(Channel);
+ }
+
+ virtual bool BeginSegmentTransfer(const cChannel *Channel, bool dummy)
+ {
+ // solange die Datenbank mit einem anderen handler thread
+ // beschäftigt ist (ergo wir den lock nicht bekommen) erst mal ignorieren
+
+ if (!handlerMutex.tryLock())
+ return false;
+
+ getHandler()->BeginSegmentTransfer(Channel, dummy);
+
+ return true;
+ }
+
+ virtual bool EndSegmentTransfer(bool Modified, bool dummy)
+ {
+ handlerMutex.unlock();
+ getHandler()->EndSegmentTransfer(Modified, dummy);
+ return false;
+ }
+
+ virtual bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version)
+ {
+
+ cEpgHandlerInstance* h = getHandler();
+ return h->IsUpdate(EventID, StartTime, TableID, Version);
+ }
+
+ virtual bool HandleEvent(cEvent* event)
+ {
+ cEpgHandlerInstance* h = getHandler();
+ return h->HandleEvent(event);
+ }
+
+ virtual bool DropOutdated(cSchedule* Schedule, time_t SegmentStart,
+ time_t SegmentEnd, uchar TableID, uchar Version)
+ {
+ cEpgHandlerInstance* h = getHandler();
+ return h->DropOutdated(Schedule, SegmentStart, SegmentEnd, TableID, Version);
+ }
+
+ int updateExternalIdsMap(cDbTable* mapDb)
+ {
+ cMutexLock lock(&mapMutex);
+
+ externIdMap.clear();
+ tell(1, "Handler: Start reading external ids from db");
+ mapDb->clear();
+
+ // select extid, channelid
+ // from channelmap
+
+ cDbStatement* selectAll = new cDbStatement(mapDb);
+
+ selectAll->build("select ");
+ selectAll->bind("EXTERNALID", cDBS::bndOut);
+ selectAll->bind("CHANNELID", cDBS::bndOut, ", ");
+ selectAll->bind("SOURCE", cDBS::bndOut, ", ");
+ selectAll->bind("CHANNELNAME", cDBS::bndOut, ", ");
+ selectAll->bind("FORMAT", cDBS::bndOut, ", ");
+ selectAll->bind("MERGE", cDBS::bndOut, ", ");
+ selectAll->build(" from %s", mapDb->TableName());
+
+ if (selectAll->prepare() != success)
+ {
+ tell(0, "Handler: Reading external id's from db aborted due to prepare error");
+ delete selectAll;
+ return fail;
+ }
+
+ // channel lock scope
+ {
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+#else
+ cChannels* channels = &Channels;
+#endif
+
+ for (int f = selectAll->find(); f; f = selectAll->fetch())
+ {
+ std::string extid = mapDb->getStrValue("ExternalId");
+
+ const char* strChannelId = mapDb->getStrValue("ChannelId");
+ const cChannel* channel = channels->GetByChannelID(tChannelID::FromString(strChannelId));
+
+ // update channelname in channelmap
+
+ if (channel && !isEmpty(channel->Name()) &&
+ (!mapDb->hasValue("CHANNELNAME", channel->Name()) || mapDb->getValue("FORMAT")->isNull()))
+ {
+ mapDb->find(); // get all fields from table (needed for update)!
+
+ mapDb->setValue("CHANNELNAME", channel->Name());
+
+ if (strstr(channel->Name(), CHANNELMARKOBSOLETE))
+ mapDb->setValue("UNKNOWNATVDR", yes);
+
+ if (strstr(channel->Name(), "HD"))
+ mapDb->setValue("FORMAT", "HD");
+ else if (strstr(channel->Name(), "3D"))
+ mapDb->setValue("FORMAT", "3D");
+ else
+ mapDb->setValue("FORMAT", "SD");
+
+ mapDb->update();
+ mapDb->reset();
+ }
+
+ // we should get the merge > 1 channels already via a merge 1 entry of the channelmap!
+
+ if (mapDb->getIntValue("Merge") > 1)
+ continue;
+
+ // insert into map
+
+ externIdMap[strChannelId] = extid;
+ }
+ }
+
+ tell(1, "Handler: Finished reading external id's from db, got %d id's",
+ (int)externIdMap.size());
+
+ selectAll->freeResult();
+ delete selectAll;
+
+ return success;
+ }
+
+ static cEpg2VdrEpgHandler* getSingleton()
+ {
+ if (!singleton)
+ singleton = new cEpg2VdrEpgHandler();
+
+ return singleton;
+ }
+
+ private:
+
+ std::string getExternalIdOfChannel(tChannelID* channelId)
+ {
+ char* id = 0;
+ std::string extid = "";
+
+ cMutexLock lock(&mapMutex);
+
+ if (externIdMap.size() < 1)
+ return "";
+
+ id = strdup(channelId->ToString());
+
+ if (externIdMap.find(id) != externIdMap.end())
+ extid = externIdMap[id];
+
+ free(id);
+
+ return extid;
+ }
+
+ cEpgHandlerInstance* getHandler()
+ {
+ if (handler.find(cThread::ThreadId()) == handler.end())
+ handler[cThread::ThreadId()] = new cEpgHandlerInstance();
+
+ return handler[cThread::ThreadId()];
+ }
+
+ std::map<std::string,std::string> externIdMap;
+ std::map<tThreadId,cEpgHandlerInstance*> handler;
+ int active;
+ cMutex mapMutex;
+ cMutexTry handlerMutex;
+
+ static cEpg2VdrEpgHandler* singleton;
+};
+
+//***************************************************************************
+#endif // __HANDLER_H
diff --git a/lib/Makefile b/lib/Makefile
new file mode 100644
index 0000000..0fe3774
--- /dev/null
+++ b/lib/Makefile
@@ -0,0 +1,108 @@
+#
+# Makefile
+#
+# See the README file for copyright information and how to reach the author.
+#
+
+include ../Make.config
+
+LIBTARGET = libhorchi
+HLIB = -L. -lhorchi
+
+DEMO = demo
+TEST = tst
+
+LIBOBJS = common.o config.o db.o epgservice.o dbdict.o json.o
+
+ifdef USEJPEG
+ LIBOBJS += imgtools.o
+endif
+
+ifdef USECURL
+ LIBOBJS += curl.o configuration.o thread.o
+endif
+
+BASELIBS = -lrt -lz -luuid
+BASELIBS += $(shell mysql_config --libs_r)
+
+ifdef USECURL
+ BASELIBS += -lcurl
+endif
+
+ifdef USEEPGS
+ LIBOBJS += searchtimer.o
+endif
+
+ifdef USEPYTHON
+ BASELIBS += $(shell python-config --libs)
+ LIBOBJS += python.o
+endif
+
+ifdef USELIBXML
+ BASELIBS += $(shell xml2-config --libs) $(shell xslt-config --libs)
+endif
+
+ifdef SYSD_NOTIFY
+ BASELIBS += $(shell pkg-config --libs libsystemd-daemon)
+ CFLAGS += $(shell pkg-config --cflags libsystemd-daemon)
+endif
+
+ifdef DEBUG
+ CFLAGS += -ggdb -O0
+endif
+
+CFLAGS += $(shell mysql_config --include)
+DEFINES += $(USES)
+
+ifdef USEPYTHON
+ CFLAGS += $(shell python-config --includes)
+endif
+
+all: lib $(TEST) $(DEMO)
+lib: $(LIBTARGET).a
+
+$(LIBTARGET).a : $(LIBOBJS)
+ @echo Building Lib ...
+ $(doLib) $@ $(LIBOBJS)
+
+tst: test.o lib
+ $(doLink) test.o $(HLIB) -larchive -lcrypto $(BASELIBS) -o $@
+
+demo: demo.o lib
+ $(doLink) demo.o $(HLIB) -larchive -lcrypto $(BASELIBS) -o $@
+
+pytst: pytst.c python.c python.h hlib
+ $(CC) $(CFLAGS) pytst.c python.c -L./lib -lhorchi $(DLIBS) -o pytst
+
+clean:
+ rm -f *.o *~ core $(TEST) $(DEMO) $(LIBTARGET).a
+
+cppchk:
+ cppcheck --template="{file}:{line}:{severity}:{message}" --quiet --force *.c *.h
+
+%.o: %.c
+ @echo Compile "$(*F)" ...
+ $(doCompile) $(*F).c -o $@
+
+#--------------------------------------------------------
+# dependencies
+#--------------------------------------------------------
+
+HEADER = db.h common.h config.h dbdict.h
+
+common.o : common.c $(HEADER) common.h
+configuration.o : configuration.c $(HEADER) configuration.h
+thread.o : thread.c $(HEADER) thread.h
+curl.o : curl.c $(HEADER) curl.h
+imgtools.o : imgtools.c $(HEADER) imgtools.h
+config.o : config.c $(HEADER) config.h
+db.o : db.c $(HEADER) db.h
+epgservice.o : epgservice.c $(HEADER) epgservice.h
+dbdict.o : dbdict.c $(HEADER) dbdict.h
+json.o : json.c $(HEADER) json.h
+python.o : python.c $(HEADER) python.h
+searchtimer.o : searchtimer.c $(HEADER) searchtimer.h
+
+demo.o : demo.c $(HEADER)
+test.o : test.c $(HEADER)
+
diff --git a/lib/common.c b/lib/common.c
new file mode 100644
index 0000000..8a0e9ac
--- /dev/null
+++ b/lib/common.c
@@ -0,0 +1,1921 @@
+/*
+ * common.c:
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <sys/ioctl.h>
+#include <net/if.h>
+#include <arpa/inet.h>
+
+#ifdef USEUUID
+# include <uuid/uuid.h>
+#endif
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <errno.h>
+#include <regex.h>
+#include <limits.h>
+
+#ifdef USELIBARCHIVE
+# include <archive.h>
+# include <archive_entry.h>
+#endif
+
+#include "common.h"
+#include "config.h"
+
+cMyMutex logMutex;
+
+//***************************************************************************
+// Tell
+//***************************************************************************
+
+const char* getLogPrefix()
+{
+ return logPrefix;
+}
+
+void tell(int eloquence, const char* format, ...)
+{
+ if (cEpgConfig::loglevel < eloquence)
+ return ;
+
+ logMutex.Lock();
+
+#ifndef VDR_PLUGIN
+ static int init = no;
+
+ if (!init)
+ {
+ init = yes;
+ openlog(cEpgConfig::logName, LOG_CONS, cEpgConfig::logFacility);
+ }
+#endif
+
+ const int sizeBuffer = 100000;
+ char t[sizeBuffer+100]; *t = 0;
+ va_list ap;
+
+ va_start(ap, format);
+
+ if (getLogPrefix())
+ snprintf(t, sizeBuffer, "%s", getLogPrefix());
+
+ vsnprintf(t+strlen(t), sizeBuffer-strlen(t), format, ap);
+
+ strReplace(t, '\n', '$');
+
+ if (cEpgConfig::logstdout)
+ {
+ char buf[50+TB];
+ timeval tp;
+
+ gettimeofday(&tp, 0);
+
+ tm* tm = localtime(&tp.tv_sec);
+
+ sprintf(buf,"%2.2d:%2.2d:%2.2d,%3.3ld ",
+ tm->tm_hour, tm->tm_min, tm->tm_sec,
+ tp.tv_usec / 1000);
+
+ printf("%s %s\n", buf, t);
+ }
+ else
+ syslog(LOG_ERR, "%s", t);
+
+ logMutex.Unlock();
+
+ va_end(ap);
+}
+
+//***************************************************************************
+// Syslog Facilities
+//***************************************************************************
+
+const Syslog::Facilities Syslog::facilities[] =
+{
+ { "auth", LOG_AUTH },
+ { "cron", LOG_CRON },
+ { "daemon", LOG_DAEMON },
+ { "kern", LOG_KERN },
+ { "lpr", LOG_LPR },
+ { "mail", LOG_MAIL },
+ { "news", LOG_NEWS },
+ { "security", LOG_AUTH },
+ { "syslog", LOG_SYSLOG },
+ { "user", LOG_USER },
+ { "uucp", LOG_UUCP },
+ { "local0", LOG_LOCAL0 },
+ { "local1", LOG_LOCAL1 },
+ { "local2", LOG_LOCAL2 },
+ { "local3", LOG_LOCAL3 },
+ { "local4", LOG_LOCAL4 },
+ { "local5", LOG_LOCAL5 },
+ { "local6", LOG_LOCAL6 },
+ { "local7", LOG_LOCAL7 },
+ { 0, 0 }
+};
+
+//***************************************************************************
+// To Name
+//***************************************************************************
+
+char* Syslog::toName(int code)
+{
+ for (int i = 0; facilities[i].name; i++)
+ if (facilities[i].code == code)
+ return (char*) facilities[i].name; // #83
+
+ return 0;
+}
+
+//***************************************************************************
+// To Code
+//***************************************************************************
+
+int Syslog::toCode(const char* name)
+{
+ for (int i = 0; facilities[i].name; i++)
+ if (strcmp(facilities[i].name, name) == 0)
+ return facilities[i].code;
+
+ return na;
+}
+
+//***************************************************************************
+// Save Realloc
+//***************************************************************************
+
+char* srealloc(void* ptr, size_t size)
+{
+ void* n = realloc(ptr, size);
+
+ if (!n)
+ {
+ free(ptr);
+ ptr = 0;
+ }
+
+ return (char*)n;
+}
+
+//***************************************************************************
+// us now
+//***************************************************************************
+
+double usNow()
+{
+ struct timeval tp;
+
+ gettimeofday(&tp, 0);
+
+ return tp.tv_sec * 1000000.0 + tp.tv_usec;
+}
+
+//***************************************************************************
+// Host ID
+//***************************************************************************
+
+unsigned int getHostId()
+{
+ static unsigned int id = gethostid() & 0xFFFFFFFF;
+ return id;
+}
+
+//***************************************************************************
+// String Operations
+//***************************************************************************
+
+void toUpper(std::string& str)
+{
+ const char* s = str.c_str();
+ int lenSrc = str.length();
+
+ char* dest = (char*)malloc(lenSrc+TB); *dest = 0;
+ char* d = dest;
+
+ int csSrc; // size of character
+
+ for (int ps = 0; ps < lenSrc; ps += csSrc)
+ {
+ csSrc = std::max(mblen(&s[ps], lenSrc-ps), 1);
+
+ if (csSrc == 1)
+ *d++ = toupper(s[ps]);
+ else if (csSrc == 2 && s[ps] == (char)0xc3 && s[ps+1] >= (char)0xa0)
+ {
+ *d++ = s[ps];
+ *d++ = s[ps+1] - 32;
+ }
+ else
+ {
+ for (int i = 0; i < csSrc; i++)
+ *d++ = s[ps+i];
+ }
+ }
+
+ *d = 0;
+
+ str = dest;
+ free(dest);
+}
+
+//***************************************************************************
+// To Case (UTF-8 save)
+//***************************************************************************
+
+const char* toCase(Case cs, char* str)
+{
+ char* s = str;
+ int lenSrc = strlen(str);
+
+ int csSrc; // size of character
+
+ for (int ps = 0; ps < lenSrc; ps += csSrc)
+ {
+ csSrc = std::max(mblen(&s[ps], lenSrc-ps), 1);
+
+ if (csSrc == 1)
+ s[ps] = cs == cUpper ? toupper(s[ps]) : tolower(s[ps]);
+ else if (csSrc == 2 && s[ps] == (char)0xc3 && s[ps+1] >= (char)0xa0)
+ {
+ s[ps] = s[ps];
+ s[ps+1] = cs == cUpper ? toupper(s[ps+1]) : tolower(s[ps+1]);
+ }
+ else
+ {
+ for (int i = 0; i < csSrc; i++)
+ s[ps+i] = s[ps+i];
+ }
+ }
+
+ return str;
+}
+
+void removeChars(std::string& str, const char* ignore)
+{
+ const char* s = str.c_str();
+ int lenSrc = str.length();
+ int lenIgn = strlen(ignore);
+
+ char* dest = (char*)malloc(lenSrc+TB); *dest = 0;
+ char* d = dest;
+
+ int csSrc; // size of character
+ int csIgn; //
+
+ for (int ps = 0; ps < lenSrc; ps += csSrc)
+ {
+ int skip = no;
+
+ csSrc = std::max(mblen(&s[ps], lenSrc-ps), 1);
+
+ for (int pi = 0; pi < lenIgn; pi += csIgn)
+ {
+ csIgn = std::max(mblen(&ignore[pi], lenIgn-pi), 1);
+
+ if (csSrc == csIgn && strncmp(&s[ps], &ignore[pi], csSrc) == 0)
+ {
+ skip = yes;
+ break;
+ }
+ }
+
+ if (!skip)
+ {
+ for (int i = 0; i < csSrc; i++)
+ *d++ = s[ps+i];
+ }
+ }
+
+ *d = 0;
+
+ str = dest;
+ free(dest);
+}
+
+void removeCharsExcept(std::string& str, const char* except)
+{
+ const char* s = str.c_str();
+ int lenSrc = str.length();
+ int lenIgn = strlen(except);
+
+ char* dest = (char*)malloc(lenSrc+TB); *dest = 0;
+ char* d = dest;
+
+ int csSrc; // size of character
+ int csIgn; //
+
+ for (int ps = 0; ps < lenSrc; ps += csSrc)
+ {
+ int skip = yes;
+
+ mblen(0,0);
+ csSrc = std::max(mblen(&s[ps], lenSrc-ps), 1);
+
+ for (int pi = 0; pi < lenIgn; pi += csIgn)
+ {
+ csIgn = std::max(mblen(&except[pi], lenIgn-pi), 1);
+
+ if (csSrc == csIgn && strncmp(&s[ps], &except[pi], csSrc) == 0)
+ {
+ skip = no;
+ break;
+ }
+ }
+
+ if (!skip)
+ {
+ for (int i = 0; i < csSrc; i++)
+ *d++ = s[ps+i];
+ }
+ }
+
+ *d = 0;
+
+ str = dest;
+ free(dest);
+}
+
+void removeWord(std::string& pattern, std::string word)
+{
+ size_t pos;
+
+ if ((pos = pattern.find(word)) != std::string::npos)
+ pattern.swap(pattern.erase(pos, word.length()));
+}
+
+//***************************************************************************
+// String Manipulation
+//***************************************************************************
+
+void prepareCompressed(std::string& pattern)
+{
+ // const char* ignore = " (),.;:-_+*!#?=&%$<>§/'`´@~\"[]{}";
+ const char* notignore = "ABCDEFGHIJKLMNOPQRSTUVWXYZßÖÄÜöäü0123456789";
+
+ toUpper(pattern);
+ removeWord(pattern, " TEIL ");
+ removeWord(pattern, " FOLGE ");
+ removeCharsExcept(pattern, notignore);
+}
+
+//***************************************************************************
+// Get Range Parts of String like '33-123' or '-123' or '21-'
+//***************************************************************************
+
+int rangeFrom(const char* s)
+{
+ return atoi(s);
+}
+
+int rangeTo(const char* s)
+{
+ int to = INT_MAX;
+
+ const char* p = strchr(s, '-');
+
+ if (p && *(p+1))
+ to = atoi(p+1);
+
+ return to;
+}
+
+//***************************************************************************
+// Left Trim
+//***************************************************************************
+
+char* lTrim(char* buf)
+{
+ if (buf)
+ {
+ char *tp = buf;
+
+ while (*tp && strchr("\n\r\t ",*tp))
+ tp++;
+
+ memmove(buf, tp, strlen(tp) +1);
+ }
+
+ return buf;
+}
+
+//*************************************************************************
+// Right Trim
+//*************************************************************************
+
+char* rTrim(char* buf)
+{
+ if (buf)
+ {
+ char *tp = buf + strlen(buf);
+
+ while (tp >= buf && strchr("\n\r\t ",*tp))
+ tp--;
+
+ *(tp+1) = 0;
+ }
+
+ return buf;
+}
+
+//*************************************************************************
+// All Trim
+//*************************************************************************
+
+char* allTrim(char* buf)
+{
+ return lTrim(rTrim(buf));
+}
+
+std::string strReplace(const std::string& what, const std::string& with, const std::string& subject)
+{
+ std::string str = subject;
+ size_t pos = 0;
+
+ while ((pos = str.find(what, pos)) != std::string::npos)
+ {
+ str.replace(pos, what.length(), with);
+ pos += with.length();
+ }
+
+ return str;
+}
+
+std::string strReplace(const std::string& what, long with, const std::string& subject)
+{
+ char swith[100];
+
+ sprintf(swith, "%ld", with);
+
+ return strReplace(what, swith, subject);
+}
+
+std::string strReplace(const std::string& what, double with, const std::string& subject)
+{
+ char swith[100];
+
+ sprintf(swith, "%.2f", with);
+
+ return strReplace(what, swith, subject);
+}
+
+char* strReplace(char* buffer, char from, char to)
+{
+ char* p = buffer;
+
+ while (*p)
+ {
+ if (*p == from)
+ *p = to;
+
+ p++;
+ }
+
+ return buffer;
+}
+
+//***************************************************************************
+// Number to String
+//***************************************************************************
+
+std::string num2Str(int num)
+{
+ char txt[16];
+
+ snprintf(txt, sizeof(txt), "%d", num);
+
+ return std::string(txt);
+}
+
+//***************************************************************************
+// Long to Pretty Time
+//***************************************************************************
+
+std::string l2pTime(time_t t, const char* format)
+{
+ char txt[100];
+ tm* tmp = localtime(&t);
+
+ strftime(txt, sizeof(txt), format, tmp);
+
+ return std::string(txt);
+}
+
+std::string l2pDate(time_t t)
+{
+ char txt[30];
+ tm* tmp = localtime(&t);
+
+ strftime(txt, sizeof(txt), "%d.%m.%Y", tmp);
+
+ return std::string(txt);
+}
+
+//***************************************************************************
+// To HTTP Header Date Format
+//***************************************************************************
+
+std::string l2HttpTime(time_t t)
+{
+ char date[128+TB];
+ tm now;
+
+ static const char *const days[] =
+ { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
+
+ static const char *const mons[] =
+ { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+
+ gmtime_r(&t, &now);
+
+ sprintf(date, "%3s, %02u %3s %04u %02u:%02u:%02u GMT",
+ days[now.tm_wday % 7],
+ (unsigned int)now.tm_mday,
+ mons[now.tm_mon % 12],
+ (unsigned int)(1900 + now.tm_year),
+ (unsigned int)now.tm_hour,
+ (unsigned int)now.tm_min,
+ (unsigned int)now.tm_sec);
+
+ return std::string(date);
+}
+
+//***************************************************************************
+// Is Daylight Saving Time
+//***************************************************************************
+
+int isDST(time_t t)
+{
+ struct tm tm;
+
+ if (!t)
+ t = time(0);
+
+ localtime_r(&t, &tm);
+ tm.tm_isdst = -1; // force DST auto detect
+ mktime(&tm);
+
+ return tm.tm_isdst;
+}
+
+//***************************************************************************
+// Time of
+//***************************************************************************
+
+time_t timeOf(time_t t)
+{
+ struct tm tm;
+
+ localtime_r(&t, &tm);
+
+ tm.tm_year = 0;
+ tm.tm_mon = 0;
+ tm.tm_mday = 0;
+ tm.tm_wday = 0;
+ tm.tm_yday = 0;
+ tm.tm_isdst = -1; // force DST auto detect
+
+ return mktime(&tm);
+}
+
+//***************************************************************************
+// Weekday Of
+//***************************************************************************
+
+int weekdayOf(time_t t)
+{
+ struct tm tm_r;
+ int weekday = localtime_r(&t, &tm_r)->tm_wday;
+
+ return weekday == 0 ? 6 : weekday - 1; // Monday is 0
+}
+
+//***************************************************************************
+// To Weekday Name
+//***************************************************************************
+
+const char* toWeekdayName(uint day)
+{
+ const char* dayNames[] =
+ {
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday",
+ 0
+ };
+
+ if (day > 6)
+ return "<unknown>";
+
+ return dayNames[day];
+}
+
+//***************************************************************************
+// hhmm of
+//***************************************************************************
+
+time_t hhmmOf(time_t t)
+{
+ struct tm tm;
+
+ localtime_r(&t, &tm);
+
+ tm.tm_year = 0;
+ tm.tm_mon = 0;
+ tm.tm_mday = 0;
+ tm.tm_wday = 0;
+ tm.tm_yday = 0;
+ tm.tm_sec = 0;
+ tm.tm_isdst = -1; // force DST auto detect
+
+ return mktime(&tm);
+}
+
+//***************************************************************************
+// time_t to hhmm like '2015'
+//***************************************************************************
+
+int l2hhmm(time_t t)
+{
+ struct tm tm;
+
+ localtime_r(&t, &tm);
+
+ return tm.tm_hour * 100 + tm.tm_min;
+}
+
+//***************************************************************************
+// HHMM to Pretty Time
+//***************************************************************************
+
+std::string hhmm2pTime(int hhmm)
+{
+ char txt[100];
+
+ sprintf(txt, "%02d:%02d", hhmm / 100, hhmm % 100);
+
+ return std::string(txt);
+}
+
+//***************************************************************************
+// Midnight of
+//***************************************************************************
+
+time_t midnightOf(time_t t)
+{
+ struct tm tm;
+
+ localtime_r(&t, &tm);
+
+ tm.tm_hour = 0;
+ tm.tm_min = 0;
+ tm.tm_sec = 0;
+ tm.tm_isdst = -1; // force DST auto detect
+
+ return mktime(&tm);
+}
+
+//***************************************************************************
+// MS to Duration
+//***************************************************************************
+
+std::string ms2Dur(uint64_t t)
+{
+ char txt[30];
+
+ int s = t / 1000;
+ int ms = t % 1000;
+
+ if (s != 0)
+ snprintf(txt, sizeof(txt), "%d.%03d seconds", s, ms);
+ else
+ snprintf(txt, sizeof(txt), "%d ms", ms);
+
+ return std::string(txt);
+}
+
+//***************************************************************************
+// Char to Char-String
+//***************************************************************************
+
+const char* c2s(char c, char* buf)
+{
+ sprintf(buf, "%c", c);
+
+ return buf;
+}
+
+//***************************************************************************
+// End Of String
+//***************************************************************************
+
+char* eos(char* s)
+{
+ if (!s)
+ return 0;
+
+ return s + strlen(s);
+}
+
+//***************************************************************************
+// Store To File
+//***************************************************************************
+
+int storeToFile(const char* filename, const char* data, int size)
+{
+ FILE* fout;
+
+ if ((fout = fopen(filename, "w+")))
+ {
+ fwrite(data, sizeof(char), size, fout);
+ fclose(fout);
+ }
+ else
+ {
+ tell(0, "Error, can't store '%s' to filesystem '%s'", filename, strerror(errno));
+ return fail;
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Load From File
+//***************************************************************************
+
+int loadFromFile(const char* infile, MemoryStruct* data)
+{
+ FILE* fin;
+ struct stat sb;
+
+ data->clear();
+
+ if (!fileExists(infile))
+ {
+ tell(0, "File '%s' not found'", infile);
+ return fail;
+ }
+
+ if (stat(infile, &sb) < 0)
+ {
+ tell(0, "Can't get info of '%s', error was '%s'", infile, strerror(errno));
+ return fail;
+ }
+
+ if ((fin = fopen(infile, "r")))
+ {
+ const char* sfx = suffixOf(infile);
+
+ data->size = sb.st_size;
+ data->modTime = sb.st_mtime;
+ data->memory = (char*)malloc(data->size);
+ fread(data->memory, sizeof(char), data->size, fin);
+ fclose(fin);
+ sprintf(data->tag, "%ld", (long int)data->size);
+
+ if (strcmp(sfx, "gz") == 0)
+ sprintf(data->contentEncoding, "gzip");
+
+ if (strcmp(sfx, "js") == 0)
+ sprintf(data->contentType, "application/javascript");
+
+ else if (strcmp(sfx, "png") == 0 || strcmp(sfx, "jpg") == 0 || strcmp(sfx, "gif") == 0)
+ sprintf(data->contentType, "image/%s", sfx);
+ else if (strcmp(sfx, "svg") == 0)
+ sprintf(data->contentType, "image/%s+xml", sfx);
+ else if (strcmp(sfx, "ico") == 0)
+ strcpy(data->contentType, "image/x-icon");
+
+ else
+ sprintf(data->contentType, "text/%s", sfx);
+ }
+ else
+ {
+ tell(0, "Error, can't open '%s' for reading, error was '%s'", infile, strerror(errno));
+ return fail;
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// TOOLS
+//***************************************************************************
+
+int isEmpty(const char* str)
+{
+ return !str || !*str;
+}
+
+const char* notNull(const char* str, const char* def)
+{
+ if (!str)
+ return def;
+
+ return str;
+}
+
+int isZero(const char* str)
+{
+ const char* p = str;
+
+ while (p && *p)
+ {
+ if (*p != '0')
+ return no;
+
+ p++;
+ }
+
+ return yes;
+}
+
+//***************************************************************************
+// Is Member
+//***************************************************************************
+
+int isMember(const char** list, const char* item)
+{
+ const char** p;
+ int i;
+
+ for (i = 0, p = list; *p; i++, p++)
+ if (strcmp(*p, item) == 0)
+ return i;
+
+ return na;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+char* sstrcpy(char* dest, const char* src, int max)
+{
+ if (!dest || !src)
+ return 0;
+
+ strncpy(dest, src, max);
+ dest[max-1] = 0;
+
+ return dest;
+}
+
+int isLink(const char* path)
+{
+ struct stat sb;
+
+ if (lstat(path, &sb) == 0)
+ return S_ISLNK(sb.st_mode);
+
+ tell(0, "Error: Detecting state for '%s' failed, error was '%s'", path, strerror(errno));
+
+ return false;
+}
+
+const char* suffixOf(const char* path)
+{
+ const char* p;
+
+ if (path && (p = strrchr(path, '.')))
+ return p+1;
+
+ return "";
+}
+
+int fileSize(const char* path)
+{
+ struct stat sb;
+
+ if (lstat(path, &sb) == 0)
+ return sb.st_size;
+
+ tell(0, "Error: Detecting state for '%s' failed, error was '%s'", path, strerror(errno));
+
+ return 0;
+}
+
+time_t fileModTime(const char* path)
+{
+ struct stat sb;
+
+ if (lstat(path, &sb) == 0)
+ return sb.st_mtime;
+
+ tell(0, "Error: Detecting state for '%s' failed, error was '%s'", path, strerror(errno));
+
+ return 0;
+}
+
+int folderExists(const char* path)
+{
+ struct stat fs;
+
+ return stat(path, &fs) == 0 && S_ISDIR(fs.st_mode);
+}
+
+int fileExists(const char* path)
+{
+ return access(path, F_OK) == 0;
+}
+
+int createLink(const char* link, const char* dest, int force)
+{
+ if (!fileExists(link) || force)
+ {
+ // may be the link exists and point to a wrong or already deleted destination ...
+ // .. therefore we delete the link at first
+
+ unlink(link);
+
+ if (symlink(dest, link) != 0)
+ {
+ tell(0, "Failed to create symlink '%s', error was '%s'", link, strerror(errno));
+ return fail;
+ }
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Remove File
+//***************************************************************************
+
+int removeFile(const char* filename)
+{
+ int lnk = isLink(filename);
+
+ if (unlink(filename) != 0)
+ {
+ tell(0, "Can't remove file '%s', '%s'", filename, strerror(errno));
+
+ return 1;
+ }
+
+ tell(3, "Removed %s '%s'", lnk ? "link" : "file", filename);
+
+ return 0;
+}
+
+//***************************************************************************
+// Check Dir
+//***************************************************************************
+
+int chkDir(const char* path)
+{
+ struct stat fs;
+
+ if (stat(path, &fs) != 0 || !S_ISDIR(fs.st_mode))
+ {
+ tell(0, "Creating directory '%s'", path);
+
+ if (mkdir(path, ACCESSPERMS) == -1)
+ {
+ tell(0, "Can't create directory '%s'", strerror(errno));
+ return fail;
+ }
+ }
+
+ return success;
+}
+
+#ifdef USELIBXML
+
+//***************************************************************************
+// Load XSLT
+//***************************************************************************
+
+xsltStylesheetPtr loadXSLT(const char* name, const char* path, int utf8)
+{
+ xsltStylesheetPtr stylesheet;
+ char* xsltfile;
+
+ asprintf(&xsltfile, "%s/%s-%s.xsl", path, name, utf8 ? "utf-8" : "iso-8859-1");
+
+ if ((stylesheet = xsltParseStylesheetFile((const xmlChar*)xsltfile)) == 0)
+ tell(0, "Error: Can't load xsltfile %s", xsltfile);
+ else
+ tell(0, "Info: Stylesheet '%s' loaded", xsltfile);
+
+ free(xsltfile);
+ return stylesheet;
+}
+#endif
+
+#ifdef USEGUNZIP
+
+//***************************************************************************
+// Gnu Unzip
+//***************************************************************************
+
+int gunzip(MemoryStruct* zippedData, MemoryStruct* unzippedData)
+{
+ const int growthStep = 1024;
+
+ z_stream stream = {0,0,0,0,0,0,0,0,0,0,0,Z_NULL,Z_NULL,Z_NULL};
+ unsigned int resultSize = 0;
+ int res = 0;
+
+ unzippedData->clear();
+
+ // determining the size in this way is taken from the sources of the gzip utility.
+
+ memcpy(&unzippedData->size, zippedData->memory + zippedData->size -4, 4);
+ unzippedData->memory = (char*)malloc(unzippedData->size);
+
+ // zlib initialisation
+
+ stream.avail_in = zippedData->size;
+ stream.next_in = (Bytef*)zippedData->memory;
+ stream.avail_out = unzippedData->size;
+ stream.next_out = (Bytef*)unzippedData->memory;
+
+ // The '+ 32' tells zlib to process zlib&gzlib headers
+
+ res = inflateInit2(&stream, MAX_WBITS + 32);
+
+ if (res != Z_OK)
+ {
+ tellZipError(res, " during zlib initialisation", stream.msg);
+ inflateEnd(&stream);
+ return fail;
+ }
+
+ // skip the header
+
+ res = inflate(&stream, Z_BLOCK);
+
+ if (res != Z_OK)
+ {
+ tellZipError(res, " while skipping the header", stream.msg);
+ inflateEnd(&stream);
+ return fail;
+ }
+
+ while (res == Z_OK)
+ {
+ if (stream.avail_out == 0)
+ {
+ unzippedData->size += growthStep;
+ unzippedData->memory = (char*)realloc(unzippedData->memory, unzippedData->size);
+
+ // Set the stream pointers to the potentially changed buffer!
+
+ stream.avail_out = resultSize - stream.total_out;
+ stream.next_out = (Bytef*)(unzippedData + stream.total_out);
+ }
+
+ res = inflate(&stream, Z_SYNC_FLUSH);
+ resultSize = stream.total_out;
+ }
+
+ if (res != Z_STREAM_END)
+ {
+ tellZipError(res, " during inflating", stream.msg);
+ inflateEnd(&stream);
+ return fail;
+ }
+
+ unzippedData->size = resultSize;
+ inflateEnd(&stream);
+
+ return success;
+}
+
+//***************************************************************************
+// gzip
+//***************************************************************************
+
+int gzip(Bytef* dest, uLongf* destLen, const Bytef* source, uLong sourceLen)
+{
+ z_stream stream;
+ int res;
+
+ stream.next_in = (Bytef *)source;
+ stream.avail_in = (uInt)sourceLen;
+ stream.next_out = dest;
+ stream.avail_out = (uInt)*destLen;
+ if ((uLong)stream.avail_out != *destLen) return Z_BUF_ERROR;
+
+ stream.zalloc = (alloc_func)0;
+ stream.zfree = (free_func)0;
+ stream.opaque = (voidpf)0;
+
+ res = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
+
+ if (res == Z_OK)
+ {
+ res = deflate(&stream, Z_FINISH);
+
+ if (res != Z_STREAM_END)
+ {
+ deflateEnd(&stream);
+ res = res == Z_OK ? Z_BUF_ERROR : res;
+ }
+ }
+
+ if (res == Z_STREAM_END)
+ {
+ *destLen = stream.total_out;
+ res = deflateEnd(&stream);
+ }
+
+ if (res != Z_OK)
+ tellZipError(res, " during compression", "");
+
+ return res == Z_OK ? success : fail;
+}
+
+ulong gzipBound(ulong size)
+{
+ return compressBound(size);
+}
+
+//*************************************************************************
+// tellZipError
+//*************************************************************************
+
+void tellZipError(int errorCode, const char* op, const char* msg)
+{
+ if (!op) op = "";
+ if (!msg) msg = "None";
+
+ switch (errorCode)
+ {
+ case Z_OK: return;
+ case Z_STREAM_END: return;
+ case Z_MEM_ERROR: tell(0, "Error: Not enough memory to zip/unzip file%s!\n", op); return;
+ case Z_BUF_ERROR: tell(0, "Error: Couldn't zip/unzip data due to output buffer size problem%s!\n", op); return;
+ case Z_DATA_ERROR: tell(0, "Error: Zipped input data corrupted%s! Details: %s\n", op, msg); return;
+ case Z_STREAM_ERROR: tell(0, "Error: Invalid stream structure%s. Details: %s\n", op, msg); return;
+ default: tell(0, "Error: Couldn't zip/unzip data for unknown reason (%6d)%s!\n", errorCode, op); return;
+ }
+}
+
+#endif // USEGUNZIP
+
+//*************************************************************************
+// Host Data
+//*************************************************************************
+
+#include <sys/utsname.h>
+#include <netdb.h>
+#include <ifaddrs.h>
+
+static struct utsname info;
+
+const char* getHostName()
+{
+ // get info from kernel
+
+ if (uname(&info) == -1)
+ return "";
+
+ return info.nodename;
+}
+
+const char* getFirstIp(int skipLo)
+{
+ struct ifaddrs *ifaddr, *ifa;
+ static char host[NI_MAXHOST] = "";
+ static char netMask[INET6_ADDRSTRLEN] = "";
+
+ if (getifaddrs(&ifaddr) == -1)
+ {
+ tell(0, "Error: getifaddrs() failed");
+ return "";
+ }
+
+ // walk through linked interface list
+
+ for (ifa = ifaddr; ifa; ifa = ifa->ifa_next)
+ {
+ if (!ifa->ifa_addr)
+ continue;
+
+ // For an AF_INET interfaces
+
+ if (ifa->ifa_addr->sa_family == AF_INET) // || ifa->ifa_addr->sa_family == AF_INET6)
+ {
+ int res = getnameinfo(ifa->ifa_addr,
+ (ifa->ifa_addr->sa_family == AF_INET) ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6),
+ host, NI_MAXHOST, 0, 0, NI_NUMERICHOST);
+
+ if (res)
+ {
+ tell(0, "Error: getnameinfo() for '%s' failed: %s", gai_strerror(res), ifa->ifa_name);
+ continue;
+ }
+
+ // skip loopback interface
+
+ if (skipLo && strcmp(host, "127.0.0.1") == 0)
+ continue;
+
+ if (ifa->ifa_netmask && ifa->ifa_netmask->sa_family == AF_INET)
+ {
+ void* p = &((struct sockaddr_in*)ifa->ifa_netmask)->sin_addr;
+ inet_ntop(ifa->ifa_netmask->sa_family, p, netMask, sizeof(netMask));
+ }
+
+ tell(1, "%-8s %-15s %s; netmask '%s'", ifa->ifa_name, host,
+ ifa->ifa_addr->sa_family == AF_INET ? " (AF_INET)" :
+ ifa->ifa_addr->sa_family == AF_INET6 ? " (AF_INET6)" : "",
+ netMask);
+ }
+ }
+
+ freeifaddrs(ifaddr);
+
+ return host;
+}
+
+//***************************************************************************
+// Get Interfaces
+//***************************************************************************
+
+const char* getInterfaces()
+{
+ static char buffer[1000+TB] = "";
+ static char host[NI_MAXHOST] = "";
+ struct ifaddrs *ifaddr, *ifa;
+
+ *buffer = 0;
+
+ if (getifaddrs(&ifaddr) == -1)
+ {
+ tell(0, "Error: getifaddrs() failed");
+ return "";
+ }
+
+ // walk through linked interface list
+
+ for (ifa = ifaddr; ifa; ifa = ifa->ifa_next)
+ {
+ if (!ifa->ifa_addr)
+ continue;
+
+ // For an AF_INET interfaces
+
+ if (ifa->ifa_addr->sa_family == AF_INET) // || ifa->ifa_addr->sa_family == AF_INET6)
+ {
+ int res = getnameinfo(ifa->ifa_addr,
+ (ifa->ifa_addr->sa_family == AF_INET) ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6),
+ host, NI_MAXHOST, 0, 0, NI_NUMERICHOST);
+
+ if (res)
+ {
+ tell(0, "Error: getnameinfo() failed: %s, skipping interface '%s'", gai_strerror(res), ifa->ifa_name);
+ continue;
+ }
+
+ sprintf(eos(buffer), "%s:%s ", ifa->ifa_name, host);
+ }
+ }
+
+ freeifaddrs(ifaddr);
+
+ return buffer;
+}
+
+//***************************************************************************
+// Get First Interface
+//***************************************************************************
+
+const char* getFirstInterface()
+{
+ static char buffer[1000+TB] = "";
+ static char host[NI_MAXHOST] = "";
+ struct ifaddrs *ifaddr, *ifa;
+
+ *buffer = 0;
+
+ if (getifaddrs(&ifaddr) == -1)
+ {
+ tell(0, "Error: getifaddrs() failed");
+ return "";
+ }
+
+ // walk through linked interface list
+
+ for (ifa = ifaddr; ifa; ifa = ifa->ifa_next)
+ {
+ if (!ifa->ifa_addr)
+ continue;
+
+ // For an AF_INET interfaces
+
+ if (ifa->ifa_addr->sa_family == AF_INET) // || ifa->ifa_addr->sa_family == AF_INET6)
+ {
+ int res = getnameinfo(ifa->ifa_addr,
+ (ifa->ifa_addr->sa_family == AF_INET) ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6),
+ host, NI_MAXHOST, 0, 0, NI_NUMERICHOST);
+
+ if (res)
+ {
+ tell(0, "Error: getnameinfo() failed: %s, skipping interface '%s'", gai_strerror(res), ifa->ifa_name);
+ continue;
+ }
+
+ if (strcasecmp(ifa->ifa_name, "lo") != 0)
+ sprintf(buffer, "%s", ifa->ifa_name);
+ }
+ }
+
+ freeifaddrs(ifaddr);
+
+ return buffer;
+}
+
+//***************************************************************************
+// Get IP Of
+//***************************************************************************
+
+const char* getIpOf(const char* device)
+{
+ struct ifreq ifr;
+ int fd;
+
+ if (isEmpty(device))
+ return getFirstIp();
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+
+ ifr.ifr_addr.sa_family = AF_INET;
+ strncpy(ifr.ifr_name, device, IFNAMSIZ-1);
+
+ ioctl(fd, SIOCGIFADDR, &ifr);
+ close(fd);
+
+ // caller has to copy the result string 'before' calling again!
+
+ return inet_ntoa(((struct sockaddr_in*)&ifr.ifr_addr)->sin_addr);
+}
+
+//***************************************************************************
+// Get Mask Of
+//***************************************************************************
+
+const char* getMaskOf(const char* device)
+{
+ struct ifreq ifr;
+ static char netMask[INET6_ADDRSTRLEN] = "";
+ int fd;
+
+ if (isEmpty(device))
+ device = getFirstInterface();
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+
+ ifr.ifr_addr.sa_family = AF_INET;
+ strncpy(ifr.ifr_name, device, IFNAMSIZ-1);
+ ioctl(fd, SIOCGIFNETMASK, &ifr);
+ close(fd);
+
+ if (ifr.ifr_netmask.sa_family == AF_INET)
+ {
+ void* p = &((struct sockaddr_in*)(&ifr.ifr_netmask))->sin_addr;
+ inet_ntop(ifr.ifr_netmask.sa_family, p, netMask, sizeof(netMask));
+
+ tell(5, "netmask for device '%s' is '%s'", device, netMask);
+ }
+
+ return netMask;
+}
+
+//***************************************************************************
+// Broadcast Address Of
+//***************************************************************************
+
+const char* bcastAddressOf(const char* ipStr, const char* maskStr)
+{
+ struct in_addr host, mask, broadcast;
+ static char bcastAddress[INET_ADDRSTRLEN] = "";
+
+ if (isEmpty(maskStr))
+ maskStr = "255.255.255.0";
+
+ if (inet_pton(AF_INET, ipStr, &host) == 1 && inet_pton(AF_INET, maskStr, &mask) == 1)
+ {
+ broadcast.s_addr = host.s_addr | ~mask.s_addr;
+
+ if (inet_ntop(AF_INET, &broadcast, bcastAddress, INET_ADDRSTRLEN))
+ tell(5, "Bcast for '%s' is '%s'", ipStr, bcastAddress);
+ else
+ tell(0, "Error: Failed converting number to string");
+ }
+ else
+ {
+ tell(0, "Error: Failed converting strings to numbers");
+ }
+
+ return bcastAddress;
+}
+
+//***************************************************************************
+// MAC Address Of
+//***************************************************************************
+
+const char* getMacOf(const char* device)
+{
+ enum { macTuppel = 6 };
+
+ static char mac[20+TB];
+ struct ifreq ifr;
+ int s;
+
+ s = socket(AF_INET, SOCK_DGRAM, 0);
+ strcpy(ifr.ifr_name, "eth0");
+ ioctl(s, SIOCGIFHWADDR, &ifr);
+
+ for (int i = 0; i < macTuppel; i++)
+ sprintf(&mac[i*3],"%02x:",((unsigned char*)ifr.ifr_hwaddr.sa_data)[i]);
+
+ mac[17] = 0;
+
+ close(s);
+
+ return mac;
+}
+
+#ifdef USELIBARCHIVE
+
+//***************************************************************************
+// unzip <file> and get data of first content which name matches <filter>
+//***************************************************************************
+
+int unzip(const char* file, const char* filter, char*& buffer, int& size, char* entryName)
+{
+ const int step = 1024*10;
+
+ int bufSize = 0;
+ int r;
+ int res;
+
+ struct archive_entry* entry;
+ struct archive* a = archive_read_new();
+
+ *entryName = 0;
+ buffer = 0;
+ size = 0;
+
+ archive_read_support_filter_all(a);
+ archive_read_support_format_all(a);
+
+ r = archive_read_open_filename(a, file, 10204);
+
+ if (r != ARCHIVE_OK)
+ {
+ tell(0, "Error: Open '%s' failed - %s", file, strerror(errno));
+ return 1;
+ }
+
+ while (archive_read_next_header(a, &entry) == ARCHIVE_OK)
+ {
+ strcpy(entryName, archive_entry_pathname(entry));
+
+ if (strstr(entryName, filter))
+ {
+ bufSize = step;
+ buffer = (char*)malloc(bufSize+1);
+
+ while ((res = archive_read_data(a, buffer+size, step)) > 0)
+ {
+ size += res;
+ bufSize += step;
+
+ buffer = (char*)realloc(buffer, bufSize+1);
+ }
+
+ buffer[size] = 0;
+
+ break;
+ }
+ }
+
+ r = archive_read_free(a);
+
+ if (r != ARCHIVE_OK)
+ {
+ size = 0;
+ free(buffer);
+ return fail;
+ }
+
+ return size > 0 ? success : fail;
+}
+
+#endif
+
+//***************************************************************************
+// cMyMutex
+//***************************************************************************
+
+cMyMutex::cMyMutex (void)
+{
+ locked = 0;
+ pthread_mutexattr_t attr;
+ pthread_mutexattr_init(&attr);
+ pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK_NP);
+ pthread_mutex_init(&mutex, &attr);
+}
+
+cMyMutex::~cMyMutex()
+{
+ pthread_mutex_destroy(&mutex);
+}
+
+void cMyMutex::Lock(void)
+{
+ pthread_mutex_lock(&mutex);
+ locked++;
+}
+
+void cMyMutex::Unlock(void)
+{
+ if (!--locked)
+ pthread_mutex_unlock(&mutex);
+}
+
+//***************************************************************************
+// cMyTimeMs
+//***************************************************************************
+
+cMyTimeMs::cMyTimeMs(int Ms)
+{
+ if (Ms >= 0)
+ Set(Ms);
+ else
+ begin = 0;
+}
+
+uint64_t cMyTimeMs::Now(void)
+{
+#define MIN_RESOLUTION 5 // ms
+ static bool initialized = false;
+ static bool monotonic = false;
+ struct timespec tp;
+ if (!initialized) {
+ // check if monotonic timer is available and provides enough accurate resolution:
+ if (clock_getres(CLOCK_MONOTONIC, &tp) == 0) {
+ // long Resolution = tp.tv_nsec;
+ // require a minimum resolution:
+ if (tp.tv_sec == 0 && tp.tv_nsec <= MIN_RESOLUTION * 1000000) {
+ if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0) {
+ monotonic = true;
+ }
+ else
+ tell(0, "Error: cMyTimeMs: clock_gettime(CLOCK_MONOTONIC) failed");
+ }
+ else
+ tell(0, "Info: cMyTimeMs: not using monotonic clock - resolution is too bad (%ld s %ld ns)", tp.tv_sec, tp.tv_nsec);
+ }
+ else
+ tell(0, "Error: cMyTimeMs: clock_getres(CLOCK_MONOTONIC) failed");
+ initialized = true;
+ }
+ if (monotonic) {
+ if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0)
+ return (uint64_t(tp.tv_sec)) * 1000 + tp.tv_nsec / 1000000;
+ tell(0, "Error: cMyTimeMs: clock_gettime(CLOCK_MONOTONIC) failed");
+ monotonic = false;
+ // fall back to gettimeofday()
+ }
+ struct timeval t;
+ if (gettimeofday(&t, NULL) == 0)
+ return (uint64_t(t.tv_sec)) * 1000 + t.tv_usec / 1000;
+ return 0;
+}
+
+void cMyTimeMs::Set(int Ms)
+{
+ begin = Now() + Ms;
+}
+
+bool cMyTimeMs::TimedOut(void)
+{
+ return Now() >= begin;
+}
+
+uint64_t cMyTimeMs::Elapsed(void)
+{
+ return Now() - begin;
+}
+
+//**************************************************************************
+// Regular Expression Searching
+//**************************************************************************
+
+int rep(const char* string, const char* expression, Option options)
+{
+ const char* tmpA;
+ const char* tmpB;
+
+ return rep(string, expression, tmpA, tmpB, options);
+}
+
+int rep(const char* string, const char* expression, const char*& s_location,
+ Option options)
+{
+ const char* tmpA;
+
+ return rep(string, expression, s_location, tmpA, options);
+}
+
+
+int rep(const char* string, const char* expression, const char*& s_location,
+ const char*& e_location, Option options)
+{
+ regex_t reg;
+ regmatch_t rm;
+ int status;
+ int opt = 0;
+
+ // Vorbereiten von reg fuer die Expressionsuche mit regexec
+ // Flags: REG_EXTENDED = Use Extended Regular Expressions
+ // REG_ICASE = Ignore case in match.
+
+ reg.re_nsub = 0;
+
+ // Options umwandeln
+ if (options & repUseRegularExpression)
+ opt = opt | REG_EXTENDED;
+ if (options & repIgnoreCase)
+ opt = opt | REG_ICASE;
+
+ if (regcomp( &reg, expression, opt) != 0)
+ return fail;
+
+ // Suchen des ersten Vorkommens von reg in string
+
+ status = regexec(&reg, string, 1, &rm, 0);
+ regfree(&reg);
+
+ if (status != 0)
+ return fail;
+
+ // Suche erfolgreich =>
+ // Setzen der ermittelten Start- und Endpositionen
+
+ s_location = (char*)(string + rm.rm_so);
+ e_location = (char*)(string + rm.rm_eo);
+
+ return success;
+}
+
+//***************************************************************************
+// Class LogDuration
+//***************************************************************************
+
+LogDuration::LogDuration(const char* aMessage, int aLogLevel)
+{
+ logLevel = aLogLevel;
+ strcpy(message, aMessage);
+
+ // at last !
+
+ durationStart = cMyTimeMs::Now();
+}
+
+LogDuration::~LogDuration()
+{
+ tell(logLevel, "duration '%s' was (%ldms)",
+ message, (long)(cMyTimeMs::Now() - durationStart));
+}
+
+void LogDuration::show(const char* label)
+{
+ tell(logLevel, "elapsed '%s' at '%s' was (%ldms)",
+ message, label, (long)(cMyTimeMs::Now() - durationStart));
+}
+
+//***************************************************************************
+// Get Unique ID
+//***************************************************************************
+
+#ifdef USEUUID
+const char* getUniqueId()
+{
+ static char uuid[sizeUuid+TB] = "";
+
+ uuid_t id;
+ uuid_generate(id);
+ uuid_unparse_upper(id, uuid);
+
+ return uuid;
+}
+#endif // USEUUID
+
+//***************************************************************************
+// Create MD5
+//***************************************************************************
+
+#ifdef USEMD5
+
+int createMd5(const char* buf, md5* md5)
+{
+ MD5_CTX c;
+ unsigned char out[MD5_DIGEST_LENGTH];
+
+ MD5_Init(&c);
+ MD5_Update(&c, buf, strlen(buf));
+ MD5_Final(out, &c);
+
+ for (int n = 0; n < MD5_DIGEST_LENGTH; n++)
+ sprintf(md5+2*n, "%02x", out[n]);
+
+ md5[sizeMd5] = 0;
+
+ return done;
+}
+
+int createMd5OfFile(const char* path, const char* name, md5* md5)
+{
+ FILE* f;
+ char buffer[1000];
+ int nread = 0;
+ MD5_CTX c;
+ unsigned char out[MD5_DIGEST_LENGTH];
+ char* file = 0;
+
+ asprintf(&file, "%s/%s", path, name);
+
+ if (!(f = fopen(file, "r")))
+ {
+ tell(0, "Fatal: Cannot build MD5 of '%s'; Error was '%s'", file, strerror(errno));
+ free(file);
+ return fail;
+ }
+
+ free(file);
+
+ MD5_Init(&c);
+
+ while ((nread = fread(buffer, 1, 1000, f)) > 0)
+ MD5_Update(&c, buffer, nread);
+
+ fclose(f);
+
+ MD5_Final(out, &c);
+
+ for (int n = 0; n < MD5_DIGEST_LENGTH; n++)
+ sprintf(md5+2*n, "%02x", out[n]);
+
+ md5[sizeMd5] = 0;
+
+ return success;
+}
+
+#endif // USEMD5
+
+//***************************************************************************
+// Url Unescape
+//***************************************************************************
+/*
+ * The buffer pointed to by @dst must be at least strlen(@src) bytes.
+ * Decoding stops at the first character from @src that decodes to null.
+
+ * Path normalization will remove redundant slashes and slash+dot sequences,
+ * as well as removing path components when slash+dot+dot is found. It will
+ * keep the root slash (if one was present) and will stop normalization
+ * at the first questionmark found (so query parameters won't be normalized).
+ *
+ * @param dst destination buffer
+ * @param src source buffer
+ * @param normalize perform path normalization if nonzero
+ * @return number of valid characters in @dst
+ */
+
+int urlUnescape(char* dst, const char* src, int normalize)
+{
+// CURL* curl;
+// int resultSize;
+
+// if (curl_global_init(CURL_GLOBAL_ALL) != 0)
+// {
+// tell(0, "Error, something went wrong with curl_global_init()");
+
+// return fail;
+// }
+
+// curl = curl_easy_init();
+
+// if (!curl)
+// {
+// tell(0, "Error, unable to get handle from curl_easy_init()");
+
+// return fail;
+// }
+
+// dst = curl_easy_unescape(curl, src, strlen(src), &resultSize);
+
+// tell(0, " [%.40s]", src);
+
+// tell(0, "res size %d [%.40s]", resultSize, dst);
+// return resultSize;
+
+ char* org_dst = dst;
+ int slash_dot_dot = 0;
+ char ch, a, b;
+
+ a = 0;
+
+ do {
+ ch = *src++;
+
+ if (ch == '%' && isxdigit(a = src[0]) && isxdigit(b = src[1]))
+ {
+ if (a < 'A')
+ a -= '0';
+ else if
+ (a < 'a') a -= 'A' - 10;
+ else
+ a -= 'a' - 10;
+
+ if (b < 'A')
+ b -= '0';
+ else if (b < 'a')
+ b -= 'A' - 10;
+ else
+ b -= 'a' - 10;
+
+ ch = 16 * a + b;
+ src += 2;
+ }
+
+ if (normalize)
+ {
+ switch (ch)
+ {
+ case '/': // compress consecutive slashes and remove slash-dot
+ if (slash_dot_dot < 3)
+ {
+
+ dst -= slash_dot_dot;
+ slash_dot_dot = 1;
+ break;
+ }
+ // fall-through
+
+ case '?': // at start of query, stop normalizing
+ if (ch == '?')
+ normalize = 0;
+
+ // fall-through
+
+ case '\0': // remove trailing slash-dot-(dot)
+ if (slash_dot_dot > 1)
+ {
+ dst -= slash_dot_dot;
+
+ // remove parent directory if it was two dots
+
+ if (slash_dot_dot == 3)
+ while (dst > org_dst && *--dst != '/')
+ ; // empty body
+ slash_dot_dot = (ch == '/') ? 1 : 0;
+
+ // keep the root slash if any
+
+ if (!slash_dot_dot && dst == org_dst && *dst == '/')
+ ++dst;
+
+ }
+ break;
+
+ case '.':
+ if (slash_dot_dot == 1 || slash_dot_dot == 2)
+ {
+ ++slash_dot_dot;
+ break;
+ }
+ // fall-through
+
+ default:
+ slash_dot_dot = 0;
+ }
+ }
+
+ *dst++ = ch;
+ } while(ch);
+
+ return (dst - org_dst) - 1;
+}
diff --git a/lib/common.h b/lib/common.h
new file mode 100644
index 0000000..df1c236
--- /dev/null
+++ b/lib/common.h
@@ -0,0 +1,560 @@
+/*
+ * common.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __COMMON_H
+#define __COMMON_H
+
+#include <stdint.h> // uint_64_t
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <zlib.h>
+#include <errno.h>
+#include <string>
+#include <map>
+
+#ifdef USESYSD
+# include <systemd/sd-daemon.h>
+#endif
+
+#ifdef USEMD5
+# include <openssl/md5.h> // MD5_*
+#endif
+
+#ifdef USELIBXML
+# include <libxslt/transform.h>
+# include <libxslt/xsltutils.h>
+# include <libexslt/exslt.h>
+#endif
+
+#ifdef VDR_PLUGIN
+# include <vdr/tools.h>
+#endif
+
+class MemoryStruct;
+
+//***************************************************************************
+// Misc
+//***************************************************************************
+
+#ifndef VDR_PLUGIN
+ inline long min(long a, long b) { return a < b ? a : b; }
+ inline long max(long a, long b) { return a > b ? a : b; }
+#else
+# define __STL_CONFIG_H
+# include <vdr/tools.h>
+#endif
+
+enum Misc
+{
+ success = 0,
+ done = success,
+ fail = -1,
+ na = -1,
+ ignore = -2,
+ all = -3,
+ abrt = -4,
+ yes = 1,
+ on = 1,
+ off = 0,
+ no = 0,
+ TB = 1,
+
+#ifdef USEMD5
+ sizeMd5 = 2 * MD5_DIGEST_LENGTH,
+#endif
+ sizeUuid = 36,
+
+ tmeSecondsPerMinute = 60,
+ tmeSecondsPerHour = tmeSecondsPerMinute * 60,
+ tmeSecondsPerDay = 24 * tmeSecondsPerHour,
+ tmeUsecondsPerSecond = 1000 * 1000
+};
+
+enum Case
+{
+ cUpper,
+ cLower
+};
+
+const char* toCase(Case cs, char* str);
+
+//***************************************************************************
+// Tell
+//***************************************************************************
+
+extern const char* logPrefix;
+
+const char* getLogPrefix();
+void __attribute__ ((format(printf, 2, 3))) tell(int eloquence, const char* format, ...);
+
+//***************************************************************************
+// Syslog
+//***************************************************************************
+
+class Syslog
+{
+ public:
+
+ struct Facilities
+ {
+ const char* name; // #83
+ int code;
+ };
+
+ static const Facilities facilities[];
+
+ static char* toName(int code);
+ static int toCode(const char* name);
+
+ static int syslogFacility;
+};
+
+//***************************************************************************
+//
+//***************************************************************************
+
+char* srealloc(void* ptr, size_t size);
+
+//***************************************************************************
+// Gun-Zip
+//***************************************************************************
+
+ulong gzipBound(ulong size);
+int gzip(Bytef* dest, uLongf* destLen, const Bytef* source, uLong sourceLen);
+void tellZipError(int errorCode, const char* op, const char* msg);
+int gunzip(MemoryStruct* zippedData, MemoryStruct* unzippedData);
+
+//***************************************************************************
+// MemoryStruct
+//***************************************************************************
+
+struct MemoryStruct
+{
+ public:
+
+ MemoryStruct() { expireAt = 0; memory = 0; zmemory = 0; clear(); }
+ MemoryStruct(const MemoryStruct* o)
+ {
+ size = o->size;
+ memory = (char*)malloc(size);
+ memcpy(memory, o->memory, size);
+
+ zsize = o->zsize;
+ zmemory = (char*)malloc(zsize);
+ memcpy(zmemory, o->zmemory, zsize);
+
+ copyAttributes(o);
+ }
+
+ ~MemoryStruct() { clear(); }
+
+ int isEmpty() { return memory == 0; }
+ int isZipped() { return zmemory != 0 && zsize > 0; }
+
+ int append(const char* buf, int len)
+ {
+ memory = srealloc(memory, size+len);
+ memcpy(memory+size, buf, len);
+ size += len;
+
+ return success;
+ }
+
+ void copyAttributes(const MemoryStruct* o)
+ {
+ strcpy(tag, o->tag);
+ strcpy(name, o->name);
+ strcpy(contentType, o->contentType);
+ strcpy(contentEncoding, o->contentEncoding);
+ strcpy(mimeType, o->mimeType);
+ headerOnly = o->headerOnly;
+ modTime = o->modTime;
+ expireAt = o->expireAt;
+ }
+
+ int toGzip()
+ {
+ free(zmemory);
+ zsize = 0;
+
+ if (isEmpty())
+ return fail;
+
+ zsize = gzipBound(size) + 512; // the maximum calculated by the lib, will adusted at gzip() call
+ zmemory = (char*)malloc(zsize);
+
+ if (gzip((Bytef*)zmemory, &zsize, (Bytef*)memory, size) != success)
+ {
+ free(zmemory);
+ zsize = 0;
+ tell(0, "Error gzip failed!");
+
+ return fail;
+ }
+
+ sprintf(contentEncoding, "gzip");
+
+ return success;
+ }
+
+ void clear()
+ {
+ free(memory);
+ memory = 0;
+ size = 0;
+ free(zmemory);
+ zmemory = 0;
+ zsize = 0;
+ *tag = 0;
+ *name = 0;
+ *contentType = 0;
+ *contentEncoding = 0;
+ *mimeType = 0;
+ modTime = time(0);
+ headerOnly = no;
+ headers.clear();
+ statusCode = 0;
+ // expireAt = time(0); -> don't reset 'expireAt' here !!!!
+ }
+
+ // data
+
+ char* memory;
+ long unsigned int size;
+
+ char* zmemory;
+ long unsigned int zsize;
+
+ // tag attribute
+
+ char tag[100+TB]; // the tag to be compared
+ char name[100+TB]; // content name (filename)
+ char contentType[100+TB]; // e.g. text/html
+ char mimeType[100+TB]; //
+ char contentEncoding[100+TB]; //
+ int headerOnly;
+ long statusCode;
+ time_t modTime;
+ time_t expireAt;
+ std::map<std::string, std::string> headers;
+};
+
+//***************************************************************************
+// Tools
+//***************************************************************************
+
+double usNow();
+unsigned int getHostId();
+const char* getHostName();
+const char* getFirstInterface();
+const char* getInterfaces();
+const char* getFirstIp(int skipLo = yes);
+const char* getIpOf(const char* device);
+const char* getMacOf(const char* device);
+const char* getMaskOf(const char* device);
+const char* bcastAddressOf(const char* ipStr, const char* maskStr = 0);
+
+//***************************************************************************
+
+#ifdef USEUUID
+ const char* getUniqueId();
+#endif
+
+void removeChars(std::string& str, const char* ignore);
+void removeCharsExcept(std::string& str, const char* except);
+void removeWord(std::string& pattern, std::string word);
+void prepareCompressed(std::string& pattern);
+std::string strReplace(const std::string& what, const std::string& with, const std::string& subject);
+std::string strReplace(const std::string& what, long with, const std::string& subject);
+std::string strReplace(const std::string& what, double with, const std::string& subject);
+char* strReplace(char* buffer, char from, char to);
+
+int rangeFrom(const char* s);
+int rangeTo(const char* s);
+
+char* rTrim(char* buf);
+char* lTrim(char* buf);
+char* allTrim(char* buf);
+
+int isMember(const char** list, const char* item);
+char* sstrcpy(char* dest, const char* src, int max);
+std::string num2Str(int num);
+int isDST(time_t t = 0);
+time_t timeOf(time_t t);
+int weekdayOf(time_t t);
+const char* toWeekdayName(uint day);
+time_t hhmmOf(time_t t);
+int l2hhmm(time_t t);
+std::string hhmm2pTime(int hhmm);
+time_t midnightOf(time_t t);
+std::string l2pTime(time_t t, const char* format = "%d.%m.%Y %T");
+std::string l2pDate(time_t t);
+std::string l2HttpTime(time_t t);
+std::string ms2Dur(uint64_t t);
+const char* c2s(char c, char* buf);
+char* eos(char* s);
+int urlUnescape(char* dst, const char* src, int normalize = yes);
+
+int storeToFile(const char* filename, const char* data, int size);
+int loadFromFile(const char* infile, MemoryStruct* data);
+
+int folderExists(const char* path);
+int fileExists(const char* path);
+int fileSize(const char* path);
+time_t fileModTime(const char* path);
+int createLink(const char* link, const char* dest, int force);
+int isLink(const char* path);
+const char* suffixOf(const char* path);
+int isEmpty(const char* str);
+const char* notNull(const char* str, const char* def = "<null>");
+int isZero(const char* str);
+int removeFile(const char* filename);
+int chkDir(const char* path);
+
+#ifdef USELIBXML
+ xsltStylesheetPtr loadXSLT(const char* name, const char* path, int utf8);
+#endif
+
+#ifdef USEMD5
+ typedef char md5Buf[sizeMd5+TB];
+ typedef char md5;
+ int createMd5(const char* buf, md5* md5);
+ int createMd5OfFile(const char* path, const char* name, md5* md5);
+#endif
+
+//***************************************************************************
+// Zip
+//***************************************************************************
+
+#ifdef USELIBARCHIVE
+int unzip(const char* file, const char* filter, char*& buffer,
+ int& size, char* entryName);
+#endif
+
+//***************************************************************************
+// cMyMutex
+//***************************************************************************
+
+class cMyMutex
+{
+ friend class cCondVar;
+
+ public:
+
+ cMyMutex(void);
+ ~cMyMutex();
+ void Lock(void);
+ void Unlock(void);
+
+ private:
+
+ pthread_mutex_t mutex;
+ int locked;
+};
+
+//***************************************************************************
+// cMyTimeMs
+//***************************************************************************
+
+class cMyTimeMs
+{
+ private:
+
+ uint64_t begin;
+
+ public:
+
+ cMyTimeMs(int Ms = 0);
+ static uint64_t Now(void);
+ void Set(int Ms = 0);
+ bool TimedOut(void);
+ uint64_t Elapsed(void);
+};
+
+//***************************************************************************
+// Wrapper for Regual Expression Library
+//***************************************************************************
+
+enum Option
+{
+ repUseRegularExpression = 1,
+ repIgnoreCase = 2
+};
+
+int rep(const char* string, const char* expression, Option options = repUseRegularExpression);
+
+int rep(const char* string, const char* expression,
+ const char*& s_location, Option options = repUseRegularExpression);
+
+int rep(const char* string, const char* expression, const char*& s_location,
+ const char*& e_location, Option options = repUseRegularExpression);
+
+//***************************************************************************
+// Log Duration
+//***************************************************************************
+
+class LogDuration
+{
+ public:
+
+ LogDuration(const char* aMessage, int aLogLevel = 2);
+ ~LogDuration();
+
+ void show(const char* label = "");
+
+ protected:
+
+ char message[1000];
+ uint64_t durationStart;
+ int logLevel;
+};
+
+//***************************************************************************
+// Semaphore
+//***************************************************************************
+
+#include <sys/sem.h>
+
+class Sem
+{
+ public:
+
+ Sem(key_t aKey)
+ {
+ locked = no;
+ key = aKey;
+
+ if ((id = semget(key, 1, 0666 | IPC_CREAT)) == -1)
+ tell(0, "Error: Can't get semaphore, errno (%d) '%s'",
+ errno, strerror(errno));
+ }
+
+ ~Sem()
+ {
+ if (locked)
+ v();
+ }
+
+ // ----------------------
+ // get lock
+
+ int p()
+ {
+ sembuf sops[2];
+
+ sops[0].sem_num = 0;
+ sops[0].sem_op = 0; // wait for lock
+ sops[0].sem_flg = SEM_UNDO;
+
+ sops[1].sem_num = 0;
+ sops[1].sem_op = 1; // increment
+ sops[1].sem_flg = SEM_UNDO | IPC_NOWAIT;
+
+ if (semop(id, sops, 2) == -1)
+ {
+ tell(0, "Error: Can't lock semaphore, errno (%d) '%s'",
+ errno, strerror(errno));
+
+ return fail;
+ }
+
+ locked = yes;
+
+ return success;
+ }
+
+ // ----------------------
+ // increment
+
+ int inc()
+ {
+ sembuf sops[1];
+
+ sops[0].sem_num = 0;
+ sops[0].sem_op = 1; // increment
+ sops[0].sem_flg = SEM_UNDO | IPC_NOWAIT;
+
+ if (semop(id, sops, 1) == -1)
+ {
+ if (errno != EAGAIN)
+ tell(0, "Error: Can't lock semaphore, errno was (%d) '%s'",
+ errno, strerror(errno));
+
+ return fail;
+ }
+
+ locked = yes;
+
+ return success;
+ }
+
+ // ----------------------
+ // decrement
+
+ int dec()
+ {
+ return v();
+ }
+
+ // ----------------------
+ // check
+
+ int check()
+ {
+ sembuf sops[1];
+
+ sops[0].sem_num = 0;
+ sops[0].sem_op = 0;
+ sops[0].sem_flg = SEM_UNDO | IPC_NOWAIT;
+
+ if (semop(id, sops, 1) == -1)
+ {
+ if (errno != EAGAIN)
+ tell(0, "Error: Can't lock semaphore, errno was (%d) '%s'",
+ errno, strerror(errno));
+
+ return fail;
+ }
+
+ return success;
+ }
+
+ // ----------------------
+ // release lock
+
+ int v()
+ {
+ sembuf sops;
+
+ sops.sem_num = 0;
+ sops.sem_op = -1; // release control
+ sops.sem_flg = SEM_UNDO | IPC_NOWAIT;
+
+ if (semop(id, &sops, 1) == -1)
+ {
+ if (errno != EAGAIN)
+ tell(0, "Error: Can't unlock semaphore, errno (%d) '%s'",
+ errno, strerror(errno));
+
+ return fail;
+ }
+
+ locked = no;
+
+ return success;
+ }
+
+ private:
+
+ key_t key;
+ int id;
+ int locked;
+};
+
+//***************************************************************************
+#endif //___COMMON_H
diff --git a/lib/config.c b/lib/config.c
new file mode 100644
index 0000000..f7d3fba
--- /dev/null
+++ b/lib/config.c
@@ -0,0 +1,59 @@
+/*
+ * config.c:
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <syslog.h>
+
+#include "config.h"
+
+//***************************************************************************
+// Statics
+//***************************************************************************
+
+int cEpgConfig::logstdout = no;
+int cEpgConfig::loglevel = 1;
+int cEpgConfig::argLoglevel = na;
+int cEpgConfig::logFacility = LOG_USER;
+const char* cEpgConfig::logName = "unknown";
+
+//***************************************************************************
+// Common EPG Service Configuration
+//***************************************************************************
+
+cEpgConfig::cEpgConfig()
+{
+ // database connection
+
+ sstrcpy(dbHost, "localhost", sizeof(dbHost));
+ dbPort = 3306;
+ sstrcpy(dbName, "epg2vdr", sizeof(dbName));
+ sstrcpy(dbUser, "epg2vdr", sizeof(dbUser));
+ sstrcpy(dbPass, "epg", sizeof(dbPass));
+
+ sstrcpy(netDevice, getFirstInterface(), sizeof(netDevice));
+
+ uuid[0] = 0;
+
+ getepgimages = yes;
+}
+
+//***************************************************************************
+// Has DB Login Changed
+//***************************************************************************
+
+int cEpgConfig::hasDbLoginChanged(cEpgConfig* old)
+{
+ if (old->dbPort != dbPort ||
+ strcmp(old->dbHost, dbHost) != 0 ||
+ strcmp(old->dbName, dbName) != 0 ||
+ strcmp(old->dbUser, dbUser) != 0 ||
+ strcmp(old->dbPass, dbPass) != 0)
+ {
+ return yes;
+ }
+
+ return no;
+}
diff --git a/lib/config.h b/lib/config.h
new file mode 100644
index 0000000..a233bc3
--- /dev/null
+++ b/lib/config.h
@@ -0,0 +1,47 @@
+/*
+ * config.h:
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __EPG_CONFIG_H
+#define __EPG_CONFIG_H
+
+#include "common.h"
+
+//***************************************************************************
+// Config
+//***************************************************************************
+
+struct cEpgConfig
+{
+ public:
+
+ cEpgConfig();
+
+ // database connection
+
+ int hasDbLoginChanged(cEpgConfig* old);
+
+ char dbHost[100+TB];
+ int dbPort;
+ char dbName[100+TB];
+ char dbUser[100+TB];
+ char dbPass[100+TB];
+
+ char netDevice[20+TB];
+ char uuid[sizeUuid+TB];
+
+ int getepgimages;
+
+ // static stuff
+
+ static int logstdout;
+ static int loglevel;
+ static int argLoglevel;
+ static int logFacility;
+ static const char* logName;
+};
+
+#endif // __EPG_CONFIG_H
diff --git a/lib/configuration.c b/lib/configuration.c
new file mode 100644
index 0000000..1f89930
--- /dev/null
+++ b/lib/configuration.c
@@ -0,0 +1,193 @@
+/*
+ * configuration.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "configuration.h"
+
+time_t cSystemNotification::lastWatchdogAt = time(0);
+const char* cSystemNotification::pidfile = "";
+
+//***************************************************************************
+// Class cSystemNotification
+//***************************************************************************
+
+cSystemNotification::cSystemNotification()
+ : cThread("SystemNotification", yes)
+{
+ interval = 0;
+ stop = no;
+}
+
+//***************************************************************************
+// System Watchdog Check / notify
+//***************************************************************************
+
+void cSystemNotification::check(int force)
+{
+ if (interval && (force || lastWatchdogAt <= time(0) - interval))
+ {
+ // notify all 'n' seconds
+
+ notify(evKeepalive);
+ lastWatchdogAt = time(0);
+ }
+}
+
+//***************************************************************************
+// Start / Stop notification thread
+//***************************************************************************
+
+int cSystemNotification::startNotifyThread(int timeout)
+{
+ threadTimeout = timeout;
+ Start(yes);
+ return success;
+}
+
+int cSystemNotification::stopNotifyThread()
+{
+ stop = yes;
+ waitCondition.Broadcast();
+ Cancel(3);
+ return success;
+}
+
+//***************************************************************************
+// action
+//***************************************************************************
+
+void cSystemNotification::action()
+{
+ cMyMutex mutex;
+ time_t timeoutAt = time(0) + threadTimeout;
+
+ stop = no;
+ mutex.Lock();
+
+ while (time(0) < timeoutAt && Running() && !stop)
+ {
+ // tell(0, "loop ...");
+
+ waitCondition.TimedWait(mutex, interval*1000);
+
+ if (!stop)
+ check();
+ }
+
+ if (time(0) >= timeoutAt)
+ tell(0, "Warning: Ending notification thread, timeout reached!");
+}
+
+//***************************************************************************
+// Notify System Deamon :o :(
+//***************************************************************************
+
+int cSystemNotification::notify(int event, const char* format, ...)
+{
+#ifdef USESYSD
+ char* message;
+ char* tmp;
+ va_list ap;
+
+ if (isEmpty(format))
+ format = "";
+
+ va_start(ap, format);
+ vasprintf(&tmp, format, ap);
+
+ switch (event)
+ {
+ case evStatus: asprintf(&message, "%s", tmp); break;
+ case evStopping: asprintf(&message, "STOPPING=1\n%s", tmp); break;
+ case evReady: asprintf(&message, "READY=1\nSTATUS=Ready\nMAINPID=%d\n%s", getpid(), tmp); break;
+ case evKeepalive: asprintf(&message, "WATCHDOG=1\n%s", tmp); break;
+ }
+
+ tell(event == evKeepalive ? 2 : 1, "Calling sd_notify(%s)", message);
+ sd_notify(0, message);
+
+ free(tmp);
+ free(message);
+#endif
+
+ // handle pidfile at evReady / evStopping
+
+ if (!isEmpty(pidfile))
+ {
+ if (event == evReady)
+ {
+ FILE* f = fopen(pidfile, "w");
+
+ if (f)
+ {
+ pid_t pid = getpid();
+ tell(1, "Creating pidfile '%s'; pid %d", pidfile, pid);
+ fprintf(f, "%d\n", pid);
+ fclose(f);
+ }
+ else
+ tell(0, "Error: Can't create pid file '%s' error was (%d) '%s'",
+ pidfile, errno, strerror(errno));
+ }
+ else if (event == evStopping)
+ {
+ if (fileExists(pidfile))
+ {
+ tell(1, "Removing pidfile '%s'", pidfile);
+ removeFile(pidfile);
+ }
+ }
+ }
+
+ return done;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+int cSystemNotification::getWatchdogState(int minInterval)
+{
+ interval = 0;
+
+#ifdef USESYSD
+
+# ifdef SYSDWDIFO
+ uint64_t us = 0;
+ int sec;
+
+ if (sd_watchdog_enabled(0, &us) > 0 && us > 0)
+ {
+ sec = us / tmeUsecondsPerSecond;
+
+ if (sec < minInterval*2)
+ {
+ tell(0, "Warning: Systemd watchdog configured to (%ds) but min %ds are required,"
+ " ignoring watchdog", sec, minInterval * 2);
+ return no;
+ }
+
+ interval = sec / 2;
+
+ tell(0, "Info: Systemd watchdog request interval"
+ " of at least(%ds), using (%ds) now!", sec, interval);
+
+ return yes;
+ }
+
+ tell(0, "Info: Systemd watchdog not configured, epgd won't be sending keep-alive messages!");
+
+# else
+ interval = defaultInterval;
+ return yes;
+# endif
+
+#else
+ tell(0, "Info: Systemd support not enabled, epgd won't be sending notifications!");
+#endif
+
+ return no;
+}
diff --git a/lib/configuration.h b/lib/configuration.h
new file mode 100644
index 0000000..bd6ba96
--- /dev/null
+++ b/lib/configuration.h
@@ -0,0 +1,140 @@
+/*
+ * configuration.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __CONFIGURATION_H
+#define __CONFIGURATION_H
+
+#include "thread.h"
+#include "common.h"
+
+extern const char* confDir;
+
+//***************************************************************************
+// Configuration
+//***************************************************************************
+
+class Configuration
+{
+ public:
+
+ Configuration() {};
+ virtual ~Configuration() {};
+
+ virtual int atConfigItem(const char* Name, const char* Value) = 0;
+
+ virtual int readConfig(const char* file = 0)
+ {
+ int count = 0;
+ FILE* f;
+ char* line = 0;
+ size_t size = 0;
+ char* value;
+ char* name;
+ char* fileName;
+
+ if (!isEmpty(file))
+ asprintf(&fileName, "%s", file);
+ else
+ asprintf(&fileName, "%s/epgd.conf", confDir);
+
+ if (access(fileName, F_OK) != 0)
+ {
+ fprintf(stderr, "Cannot access configuration file '%s'\n", fileName);
+ free(fileName);
+ return fail;
+ }
+
+ f = fopen(fileName, "r");
+
+ while (getline(&line, &size, f) > 0)
+ {
+ char* p = strchr(line, '#');
+ if (p) *p = 0;
+
+ allTrim(line);
+
+ if (isEmpty(line))
+ continue;
+
+ if (!(value = strchr(line, '=')))
+ continue;
+
+ *value = 0;
+ value++;
+ lTrim(value);
+ name = line;
+ allTrim(name);
+
+ if (atConfigItem(name, value) != success)
+ {
+ fprintf(stderr, "Found unexpected parameter '%s', aborting\n", name);
+ free(fileName);
+ return fail;
+ }
+
+ count++;
+ }
+
+ free(line);
+ fclose(f);
+
+ tell(0, "Read %d option from %s", count , fileName);
+
+ free(fileName);
+
+ return success;
+ }
+};
+
+//***************************************************************************
+// System Notification Interface (systemd, watchdog, pidfile, ...)
+//***************************************************************************
+
+class cSystemNotification : public cThread
+{
+ public:
+
+ enum SystemEvent
+ {
+ evReady,
+ evStatus,
+ evKeepalive,
+ evStopping
+ };
+
+ enum Misc
+ {
+ defaultInterval = 60
+ };
+
+ cSystemNotification();
+
+ int __attribute__ ((format(printf, 3, 4))) notify(int event, const char* format = 0, ...);
+ int getWatchdogState(int minInterval);
+ void check(int force = no);
+
+ int startNotifyThread(int timeout);
+ int stopNotifyThread();
+
+ static void setPidFile(const char* file) { pidfile = file; }
+
+ protected:
+
+ virtual void action();
+
+ int interval;
+ int threadTimeout;
+ cCondVar waitCondition;
+ int stop;
+
+ static time_t lastWatchdogAt;
+ static const char* pidfile;
+};
+
+//***************************************************************************
+
+#endif // __CONFIGURATION_H
diff --git a/lib/curl.c b/lib/curl.c
new file mode 100644
index 0000000..ab5cf82
--- /dev/null
+++ b/lib/curl.c
@@ -0,0 +1,454 @@
+/*
+ * curlfuncs.cpp
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "curl.h"
+
+//***************************************************************************
+// Singelton
+//***************************************************************************
+
+cCurl curl;
+
+std::string cCurl::sBuf = "";
+int cCurl::curlInitialized = no;
+cSystemNotification* cCurl::sysNotification = 0;
+
+//***************************************************************************
+// Callbacks
+//***************************************************************************
+
+size_t collect_data(void *ptr, size_t size, size_t nmemb, void* stream)
+{
+ std::string sTmp;
+ register size_t actualsize = size * nmemb;
+
+ if ((FILE *)stream == NULL)
+ {
+ sTmp.assign((char *)ptr, actualsize);
+ cCurl::sBuf += sTmp;
+ }
+ else
+ {
+ fwrite(ptr, size, nmemb, (FILE *)stream);
+ }
+
+ return actualsize;
+}
+
+//***************************************************************************
+// Object
+//***************************************************************************
+
+cCurl::cCurl()
+{
+ handle = 0;
+}
+
+cCurl::~cCurl()
+{
+ exit();
+}
+
+//***************************************************************************
+// Create / Destroy
+//***************************************************************************
+
+int cCurl::create()
+{
+ if (!curlInitialized)
+ {
+ // call only once per process and *before* any thread is started!
+
+ if (curl_global_init(CURL_GLOBAL_NOTHING /*CURL_GLOBAL_ALL*/) != 0)
+ {
+ tell(0, "Error, something went wrong with curl_global_init()");
+ return fail;
+ }
+
+ curlInitialized = yes;
+ }
+
+ return done;
+}
+
+int cCurl::destroy()
+{
+ if (curlInitialized)
+ curl_global_cleanup();
+
+ curlInitialized = no;
+
+ return done;
+}
+
+//***************************************************************************
+// Init / Exit
+//***************************************************************************
+
+int cCurl::init(const char* httpproxy)
+{
+ if (!handle)
+ {
+ if (!(handle = curl_easy_init()))
+ {
+ tell(0, "Could not create new curl instance");
+ return fail;
+ }
+ }
+
+ // Reset Options
+
+ if (!isEmpty(httpproxy))
+ {
+ curl_easy_setopt(handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
+ curl_easy_setopt(handle, CURLOPT_PROXY, httpproxy); // Specify HTTP proxy
+ }
+
+ curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, collect_data);
+ curl_easy_setopt(handle, CURLOPT_WRITEDATA, 0); // Set option to write to string
+ curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, yes);
+ curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, 0); // Send header to this function
+ curl_easy_setopt(handle, CURLOPT_WRITEHEADER, 0); // Pass some header details to this struct
+ curl_easy_setopt(handle, CURLOPT_MAXFILESIZE, 100*1024*1024); // Set maximum file size to get (bytes)
+ curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 1); // No progress meter
+ curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1); // No signaling
+ curl_easy_setopt(handle, CURLOPT_TIMEOUT, 30); // Set timeout
+ curl_easy_setopt(handle, CURLOPT_NOBODY, 0); //
+ curl_easy_setopt(handle, CURLOPT_USERAGENT, CURL_USERAGENT); // Some servers don't like requests
+
+ return success;
+}
+
+int cCurl::exit()
+{
+ if (handle)
+ curl_easy_cleanup(handle);
+
+ handle = 0;
+
+ return done;
+}
+
+//***************************************************************************
+// Get Url
+//***************************************************************************
+
+int cCurl::GetUrl(const char *url, std::string *sOutput, const std::string &sReferer)
+{
+ CURLcode res;
+
+ init();
+
+ curl_easy_setopt(handle, CURLOPT_URL, url); // Set the URL to get
+
+ if (sReferer != "")
+ curl_easy_setopt(handle, CURLOPT_REFERER, sReferer.c_str());
+
+ curl_easy_setopt(handle, CURLOPT_HTTPGET, yes);
+ curl_easy_setopt(handle, CURLOPT_FAILONERROR, yes);
+ curl_easy_setopt(handle, CURLOPT_WRITEDATA, 0); // Set option to write to string
+ sBuf = "";
+
+ res = curl_easy_perform(handle);
+
+ if (res != CURLE_OK)
+ {
+ *sOutput = "";
+ return 0;
+ }
+
+ *sOutput = sBuf;
+ return 1;
+}
+
+int cCurl::GetUrlFile(const char *url, const char *filename, const std::string &sReferer)
+{
+ int nRet = 0;
+ init();
+
+ // Point the output to a file
+
+ FILE *fp;
+ if ((fp = fopen(filename, "w")) == NULL)
+ return 0;
+
+ curl_easy_setopt(handle, CURLOPT_WRITEDATA, fp); // Set option to write to file
+ curl_easy_setopt(handle, CURLOPT_URL, url); // Set the URL to get
+ if (sReferer != "")
+ curl_easy_setopt(handle, CURLOPT_REFERER, sReferer.c_str());
+ curl_easy_setopt(handle, CURLOPT_HTTPGET, yes);
+ if (curl_easy_perform(handle) == 0)
+ nRet = 1;
+ else
+ nRet = 0;
+
+ curl_easy_setopt(handle, CURLOPT_WRITEDATA, NULL); // Set option back to default (string)
+ fclose(fp);
+ return nRet;
+}
+
+int cCurl::PostUrl(const char *url, const std::string &sPost, std::string *sOutput, const std::string &sReferer)
+{
+ init();
+
+ int retval = 1;
+ std::string::size_type nStart = 0, nEnd, nPos;
+ std::string sTmp, sName, sValue;
+ struct curl_httppost *formpost=NULL;
+ struct curl_httppost *lastptr=NULL;
+ struct curl_slist *headerlist=NULL;
+
+ // Add the POST variables here
+ while ((nEnd = sPost.find("##", nStart)) != std::string::npos) {
+ sTmp = sPost.substr(nStart, nEnd - nStart);
+ if ((nPos = sTmp.find("=")) == std::string::npos)
+ return 0;
+ sName = sTmp.substr(0, nPos);
+ sValue = sTmp.substr(nPos+1);
+ curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, sName.c_str(), CURLFORM_COPYCONTENTS, sValue.c_str(), CURLFORM_END);
+ nStart = nEnd + 2;
+ }
+ sTmp = sPost.substr(nStart);
+ if ((nPos = sTmp.find("=")) == std::string::npos)
+ return 0;
+ sName = sTmp.substr(0, nPos);
+ sValue = sTmp.substr(nPos+1);
+ curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, sName.c_str(), CURLFORM_COPYCONTENTS, sValue.c_str(), CURLFORM_END);
+
+ retval = curl.DoPost(url, sOutput, sReferer, formpost, headerlist);
+
+ curl_formfree(formpost);
+ curl_slist_free_all(headerlist);
+ return retval;
+}
+
+int cCurl::PostRaw(const char *url, const std::string &sPost, std::string *sOutput, const std::string &sReferer)
+{
+ init();
+
+ int retval;
+ struct curl_httppost *formpost=NULL;
+ struct curl_slist *headerlist=NULL;
+
+ curl_easy_setopt(handle, CURLOPT_POSTFIELDS, sPost.c_str());
+ curl_easy_setopt(handle, CURLOPT_POSTFIELDSIZE, 0); //FIXME: Should this be the size instead, in case this is binary string?
+
+ retval = curl.DoPost(url, sOutput, sReferer, formpost, headerlist);
+
+ curl_formfree(formpost);
+ curl_slist_free_all(headerlist);
+ return retval;
+}
+
+int cCurl::DoPost(const char *url, std::string *sOutput, const std::string &sReferer,
+ struct curl_httppost *formpost, struct curl_slist *headerlist)
+{
+ headerlist = curl_slist_append(headerlist, "Expect:");
+
+ // Now do the form post
+ curl_easy_setopt(handle, CURLOPT_URL, url);
+ if (sReferer != "")
+ curl_easy_setopt(handle, CURLOPT_REFERER, sReferer.c_str());
+ curl_easy_setopt(handle, CURLOPT_HTTPPOST, formpost);
+
+ curl_easy_setopt(handle, CURLOPT_WRITEDATA, 0); // Set option to write to string
+ sBuf = "";
+ if (curl_easy_perform(handle) == 0) {
+ *sOutput = sBuf;
+ return 1;
+ }
+ else {
+ // We have an error here mate!
+ *sOutput = "";
+ return 0;
+ }
+}
+
+int cCurl::SetCookieFile(char *filename)
+{
+ init();
+
+ if (curl_easy_setopt(handle, CURLOPT_COOKIEFILE, filename) != 0)
+ return 0;
+ if (curl_easy_setopt(handle, CURLOPT_COOKIEJAR, filename) != 0)
+ return 0;
+ return 1;
+}
+
+char* cCurl::EscapeUrl(const char *url)
+{
+ init();
+ return curl_easy_escape(handle, url , strlen(url));
+}
+
+void cCurl::Free(char* str)
+{
+ curl_free(str);
+}
+
+//***************************************************************************
+// Callbacks
+//***************************************************************************
+
+size_t cCurl::WriteMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data)
+{
+ size_t realsize = size * nmemb;
+ struct MemoryStruct* mem = (struct MemoryStruct*)data;
+
+ if (sysNotification)
+ sysNotification->check();
+
+ if (mem->memory)
+ mem->memory = (char*)realloc(mem->memory, mem->size + realsize + 1);
+ else
+ mem->memory = (char*)malloc(mem->size + realsize + 1);
+
+ if (mem->memory)
+ {
+ memcpy (&(mem->memory[mem->size]), ptr, realsize);
+ mem->size += realsize;
+ mem->memory[mem->size] = 0;
+ }
+
+ return realsize;
+}
+
+size_t cCurl::WriteHeaderCallback(char* ptr, size_t size, size_t nmemb, void* data)
+{
+ size_t realsize = size * nmemb;
+ struct MemoryStruct* mem = (struct MemoryStruct*)data;
+ char* p;
+
+ if (sysNotification)
+ sysNotification->check();
+
+ if (ptr)
+ {
+ // add to Header to map
+
+ std::string header(ptr);
+ std::size_t pos = header.find(": ");
+
+ if(pos != std::string::npos)
+ {
+ std::string name = header.substr(0, pos);
+ std::string value = header.substr(pos+2, std::string::npos);
+ mem->headers[name] = value;
+ }
+
+ // get filename
+ {
+ // Content-Disposition: attachment; filename="20140103_20140103_de_qy.zip"
+
+ const char* attribute = "Content-disposition: ";
+
+ if ((p = strcasestr((char*)ptr, attribute)))
+ {
+ if ((p = strcasestr(p, "filename=")))
+ {
+ p += strlen("filename=");
+
+ tell(4, "found filename at [%s]", p);
+
+ if (*p == '"')
+ p++;
+
+ sprintf(mem->name, "%s", p);
+
+ if ((p = strchr(mem->name, '\n')))
+ *p = 0;
+
+ if ((p = strchr(mem->name, '\r')))
+ *p = 0;
+
+ if ((p = strchr(mem->name, '"')))
+ *p = 0;
+
+ tell(4, "set name to '%s'", mem->name);
+ }
+ }
+ }
+
+ // since some sources update "ETag" an "Last-Modified:" without changing the contents
+ // we have to use "Content-Length:" to check for updates :(
+ {
+ const char* attribute = "Content-Length: ";
+
+ if ((p = strcasestr((char*)ptr, attribute)))
+ {
+ sprintf(mem->tag, "%s", p+strlen(attribute));
+
+ if ((p = strchr(mem->tag, '\n')))
+ *p = 0;
+
+ if ((p = strchr(mem->tag, '\r')))
+ *p = 0;
+
+ if ((p = strchr(mem->tag, '"')))
+ *p = 0;
+ }
+ }
+ }
+
+ return realsize;
+}
+
+//***************************************************************************
+// Download File
+//***************************************************************************
+
+int cCurl::downloadFile(const char* url, int& size, MemoryStruct* data, int timeout,
+ const char* userAgent, struct curl_slist* headerlist)
+{
+ long code;
+ CURLcode res = CURLE_OK;
+
+ size = 0;
+
+ init();
+
+ curl_easy_setopt(handle, CURLOPT_URL, url); // Specify URL to get
+ curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, yes);
+ curl_easy_setopt(handle, CURLOPT_UNRESTRICTED_AUTH, yes); // continue to send authentication (user+password) when following locations
+ curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); // Send all data to this function
+ curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void*)data); // Pass our 'data' struct to the callback function
+ curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, WriteHeaderCallback); // Send header to this function
+ curl_easy_setopt(handle, CURLOPT_WRITEHEADER, (void*)data); // Pass some header details to this struct
+ curl_easy_setopt(handle, CURLOPT_MAXFILESIZE, 100*1024*1024); // Set maximum file size to get (bytes)
+ curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 1); // No progress meter
+ curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1); // No signaling
+ curl_easy_setopt(handle, CURLOPT_TIMEOUT, timeout); // Set timeout
+ curl_easy_setopt(handle, CURLOPT_NOBODY, data->headerOnly ? 1 : 0); //
+ curl_easy_setopt(handle, CURLOPT_USERAGENT, userAgent); // Some servers don't like requests without a user-agent field
+ curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, "gzip"); //
+
+ if (headerlist)
+ curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headerlist);
+
+ // perform http-get
+
+ if ((res = curl_easy_perform(handle)) != 0)
+ {
+ data->clear();
+ tell(1, "Error, download failed; %s (%d)", curl_easy_strerror(res), res);
+
+ return fail;
+ }
+
+ curl_easy_getinfo(handle, CURLINFO_HTTP_CODE, &code);
+ data->statusCode = code;
+
+ if (code == 404)
+ {
+ data->clear();
+ return fail;
+ }
+
+ size = data->size;
+
+ return success;
+}
diff --git a/lib/curl.h b/lib/curl.h
new file mode 100644
index 0000000..475675c
--- /dev/null
+++ b/lib/curl.h
@@ -0,0 +1,77 @@
+/*
+ * curlfuncs.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __LIB_CURL__
+#define __LIB_CURL__
+
+#include <curl/curl.h>
+#include <curl/easy.h>
+
+#include <string>
+
+#include "common.h"
+#include "config.h"
+#include "configuration.h"
+
+#define CURL_USERAGENT "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; Mayukh's libcurl wrapper http://www.mayukhbose.com/)"
+
+//***************************************************************************
+// CURL
+//***************************************************************************
+
+class cCurl
+{
+ public:
+
+ cCurl();
+ ~cCurl();
+
+ int init(const char* httpproxy = "");
+ int exit();
+
+ static int create();
+ static int destroy();
+
+ int GetUrl(const char *url, std::string *sOutput, const std::string &sReferer="");
+ int GetUrlFile(const char *url, const char *filename, const std::string &sReferer="");
+ int SetCookieFile(char *filename);
+ int PostUrl(const char *url, const std::string &sPost, std::string *sOutput, const std::string &sReferer = "");
+ int PostRaw(const char *url, const std::string &sPost, std::string *sOutput, const std::string &sReferer = "");
+ int DoPost(const char *url, std::string *sOutput, const std::string &sReferer,
+ struct curl_httppost *formpost, struct curl_slist *headerlist);
+
+ char* EscapeUrl(const char *url);
+ void Free(char* str);
+
+ int downloadFile(const char* url, int& size, MemoryStruct* data, int timeout = 30,
+ const char* userAgent = CURL_USERAGENT, struct curl_slist *headerlist = 0);
+
+ // static stuff
+
+ static void setSystemNotification(cSystemNotification* s) { sysNotification = s; }
+ static std::string sBuf; // dirty
+
+ protected:
+
+ // data
+
+ CURL* handle;
+
+ static cSystemNotification* sysNotification;
+
+ // statics
+
+ static size_t WriteMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data);
+ static size_t WriteHeaderCallback(char* ptr, size_t size, size_t nmemb, void* data);
+
+ static int curlInitialized;
+};
+
+extern cCurl curl;
+
+//***************************************************************************
+#endif // __LIB_CURL__
diff --git a/lib/db.c b/lib/db.c
new file mode 100644
index 0000000..271c6ae
--- /dev/null
+++ b/lib/db.c
@@ -0,0 +1,1649 @@
+/*
+ * db.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <stdio.h>
+#include <errmsg.h>
+
+#include <map>
+
+#include "db.h"
+
+//***************************************************************************
+// DB Statement
+//***************************************************************************
+
+int cDbStatement::explain = no;
+
+cDbStatement::cDbStatement(cDbTable* aTable)
+{
+ table = aTable;
+ connection = table->getConnection();
+ stmtTxt = "";
+ stmt = 0;
+ inCount = 0;
+ outCount = 0;
+ inBind = 0;
+ outBind = 0;
+ affected = 0;
+ metaResult = 0;
+ bindPrefix = 0;
+ firstExec = yes;
+ buildErrors = 0;
+
+ callsPeriod = 0;
+ callsTotal = 0;
+ duration = 0;
+
+ if (connection)
+ connection->statements.append(this);
+}
+
+cDbStatement::cDbStatement(cDbConnection* aConnection, const char* sText)
+{
+ table = 0;
+ connection = aConnection;
+ stmtTxt = sText;
+ stmt = 0;
+ inCount = 0;
+ outCount = 0;
+ inBind = 0;
+ outBind = 0;
+ affected = 0;
+ metaResult = 0;
+ bindPrefix = 0;
+ firstExec = yes;
+
+ callsPeriod = 0;
+ callsTotal = 0;
+ duration = 0;
+ buildErrors = 0;
+
+ if (connection)
+ connection->statements.append(this);
+}
+
+cDbStatement::~cDbStatement()
+{
+ if (connection)
+ connection->statements.remove(this);
+
+ clear();
+}
+
+//***************************************************************************
+// Execute
+//***************************************************************************
+
+int cDbStatement::execute(int noResult)
+{
+ affected = 0;
+
+ if (!connection || !connection->getMySql())
+ return fail;
+
+ if (!stmt)
+ return connection->errorSql(connection, "execute(missing statement)");
+
+// if (explain && firstExec)
+// {
+// firstExec = no;
+
+// if (strstr(stmtTxt.c_str(), "select "))
+// {
+// MYSQL_RES* result;
+// MYSQL_ROW row;
+// string q = "explain " + stmtTxt;
+
+// if (connection->query(q.c_str()) != success)
+// connection->errorSql(connection, "explain ", 0);
+// else if ((result = mysql_store_result(connection->getMySql())))
+// {
+// while ((row = mysql_fetch_row(result)))
+// {
+// tell(0, "EXPLAIN: %s) %s %s %s %s %s %s %s %s %s",
+// row[0], row[1], row[2], row[3],
+// row[4], row[5], row[6], row[7], row[8], row[9]);
+// }
+
+// mysql_free_result(result);
+// }
+// }
+// }
+
+ // tell(0, "execute %d [%s]", stmt, stmtTxt.c_str());
+
+ double start = usNow();
+
+ if (mysql_stmt_execute(stmt))
+ return connection->errorSql(connection, "execute(stmt_execute)", stmt, stmtTxt.c_str());
+
+ duration += usNow() - start;
+ callsPeriod++;
+ callsTotal++;
+
+ // out binding - if needed
+
+ if (outCount && !noResult)
+ {
+ if (mysql_stmt_store_result(stmt))
+ return connection->errorSql(connection, "execute(store_result)", stmt, stmtTxt.c_str());
+
+ // fetch the first result - if any
+
+ if (mysql_stmt_affected_rows(stmt) > 0)
+ mysql_stmt_fetch(stmt);
+ }
+ else if (outCount)
+ {
+ mysql_stmt_store_result(stmt);
+ }
+
+ // result was stored (above) only if output (outCound) is expected,
+ // therefore we don't need to call freeResult() after insert() or update()
+
+ affected = mysql_stmt_affected_rows(stmt);
+
+ return success;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+int cDbStatement::getLastInsertId()
+{
+ MYSQL_RES* result = 0;
+ int insertId = na;
+
+ if ((result = mysql_store_result(connection->getMySql())) == 0 &&
+ mysql_field_count(connection->getMySql()) == 0 &&
+ mysql_insert_id(connection->getMySql()) != 0)
+ {
+ insertId = mysql_insert_id(connection->getMySql());
+ }
+
+ mysql_free_result(result);
+
+ return insertId;
+}
+
+int cDbStatement::getResultCount()
+{
+ mysql_stmt_store_result(stmt);
+
+ return mysql_stmt_affected_rows(stmt);
+}
+
+int cDbStatement::find()
+{
+ if (execute() != success)
+ return fail;
+
+ return getAffected() > 0 ? yes : no;
+}
+
+int cDbStatement::fetch()
+{
+ if (!mysql_stmt_fetch(stmt))
+ return yes;
+
+ return no;
+}
+
+int cDbStatement::freeResult()
+{
+ if (metaResult)
+ mysql_free_result(metaResult);
+
+ if (stmt)
+ mysql_stmt_free_result(stmt);
+
+ return success;
+}
+
+//***************************************************************************
+// Build Statements - new Interface
+//***************************************************************************
+
+int cDbStatement::build(const char* format, ...)
+{
+ if (format)
+ {
+ char* tmp;
+
+ va_list more;
+ va_start(more, format);
+ vasprintf(&tmp, format, more);
+
+ stmtTxt += tmp;
+ free(tmp);
+ }
+
+ return success;
+}
+
+int cDbStatement::bind(const char* fname, int mode, const char* delim)
+{
+ return bind(table->getValue(fname), mode, delim);
+}
+
+int cDbStatement::bind(cDbFieldDef* field, int mode, const char* delim)
+{
+ return bind(table->getValue(field), mode, delim);
+}
+
+int cDbStatement::bind(cDbTable* aTable, cDbFieldDef* field, int mode, const char* delim)
+{
+ return bind(aTable->getValue(field), mode, delim);
+}
+
+int cDbStatement::bind(cDbTable* aTable, const char* fname, int mode, const char* delim)
+{
+ return bind(aTable->getValue(fname), mode, delim);
+}
+
+int cDbStatement::bind(cDbValue* value, int mode, const char* delim)
+{
+ if (!value || !value->getField())
+ {
+ tell(0, "Error: Missing %s value", !value ? "bind" : "field of bind");
+ buildErrors++;
+ return fail;
+ }
+
+ if (delim)
+ stmtTxt += delim;
+
+ if (bindPrefix)
+ stmtTxt += bindPrefix;
+
+ if (mode & bndIn)
+ {
+ if (mode & bndSet)
+ stmtTxt += value->getDbName() + std::string(" =");
+
+ stmtTxt += " ?";
+ appendBinding(value, bndIn);
+ }
+ else if (mode & bndOut)
+ {
+ stmtTxt += value->getDbName();
+ appendBinding(value, bndOut);
+ }
+
+ return success;
+}
+
+int cDbStatement::bindAllOut(const char* delim, int incTypes, int excTypes)
+{
+ int n = 0;
+ std::map<std::string, cDbFieldDef*>::iterator f;
+ cDbTableDef* tableDef = table->getTableDef();
+
+ if (delim)
+ stmtTxt += delim;
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ {
+ if (!(incTypes & f->second->getType())) // Include Types
+ continue;
+
+ if (excTypes & f->second->getType()) // Exclude Types
+ continue;
+
+ bind(f->second, bndOut, n++ ? ", " : "");
+ }
+
+ return success;
+}
+
+int cDbStatement::bindCmp(const char* ctable, cDbValue* value,
+ const char* comp, const char* delim)
+{
+ if (delim) build("%s", delim);
+ if (ctable) build("%s.", ctable);
+
+ build("%s%s %s ?", bindPrefix ? bindPrefix : "", value->getDbName(), comp);
+
+ appendBinding(value, bndIn);
+
+ return success;
+}
+
+int cDbStatement::bindCmp(const char* ctable, cDbFieldDef* field, cDbValue* value,
+ const char* comp, const char* delim)
+{
+ cDbValue* vf = table->getRow()->getValue(field);
+ cDbValue* vv = value ? value : vf;
+
+ if (!vf || !vv)
+ {
+ buildErrors++;
+ return fail;
+ }
+
+ if (delim) build("%s", delim);
+ if (ctable) build("%s.", ctable);
+
+ build("%s%s %s ?", bindPrefix ? bindPrefix : "", vf->getDbName(), comp);
+
+ appendBinding(vv, bndIn);
+
+ return success;
+}
+
+int cDbStatement::bindCmp(const char* ctable, const char* fname, cDbValue* value,
+ const char* comp, const char* delim)
+{
+ cDbValue* vf = table->getRow()->getValue(fname);
+ cDbValue* vv = value ? value : vf;
+
+ if (!vf || !vv)
+ {
+ buildErrors++;
+ return fail;
+ }
+
+ if (delim) build("%s", delim);
+ if (ctable) build("%s.", ctable);
+
+ build("%s%s %s ?", bindPrefix ? bindPrefix : "", vf->getDbName(), comp);
+
+ appendBinding(vv, bndIn);
+
+ return success;
+}
+
+int cDbStatement::bindText(const char* text, cDbValue* value,
+ const char* comp, const char* delim)
+{
+ if (!value)
+ {
+ buildErrors++;
+ return fail;
+ }
+
+ if (delim) build("%s", delim);
+
+ build("%s %s ?", text, comp);
+
+ appendBinding(value, bndIn);
+
+ return success;
+}
+
+int cDbStatement::bindTextFree(const char* text, cDbValue* value, int mode)
+{
+ if (!value)
+ {
+ buildErrors++;
+ return fail;
+ }
+
+ build("%s", text);
+
+ if (mode & bndIn)
+ appendBinding(value, bndIn);
+
+ else if (mode & bndOut)
+ appendBinding(value, bndOut);
+
+ return success;
+}
+
+//***************************************************************************
+// Bind In Char - like <field> in ('A','B','C')
+//
+// expected string in cDbValue is: "A,B,C"
+//***************************************************************************
+
+int cDbStatement::bindInChar(const char* ctable, const char* fname,
+ cDbValue* value, const char* delim)
+{
+ cDbValue* vf = table->getRow()->getValue(fname);
+ cDbValue* vv = value ? value : vf;
+
+ if (!vf || !vv)
+ {
+ buildErrors++;
+ return fail;
+ }
+
+ build("%s find_in_set(cast(%s%s%s%s as char),?)",
+ delim ? delim : "",
+ bindPrefix ? bindPrefix : "",
+ ctable ? ctable : "",
+ ctable ? "." : "",
+ vf->getDbName());
+
+ appendBinding(vv, bndIn);
+
+ return success;
+}
+
+//***************************************************************************
+// Clear
+//***************************************************************************
+
+void cDbStatement::clear()
+{
+ stmtTxt = "";
+ affected = 0;
+
+ if (inCount)
+ {
+ free(inBind);
+ inCount = 0;
+ inBind = 0;
+ }
+
+ if (outCount)
+ {
+ free(outBind);
+ outCount = 0;
+ outBind = 0;
+ }
+
+ if (stmt)
+ {
+ mysql_stmt_free_result(stmt);
+ mysql_stmt_close(stmt);
+ stmt = 0;
+ }
+}
+
+//***************************************************************************
+// Append Binding
+//***************************************************************************
+
+int cDbStatement::appendBinding(cDbValue* value, BindType bt)
+{
+ int count = 0;
+ MYSQL_BIND** bindings = 0;
+ MYSQL_BIND* newBinding;
+
+ if (bt & bndIn)
+ {
+ count = ++inCount;
+ bindings = &inBind;
+ }
+ else if (bt & bndOut)
+ {
+ count = ++outCount;
+ bindings = &outBind;
+ }
+ else
+ return 0;
+
+ if (!*bindings)
+ *bindings = (MYSQL_BIND*)malloc(count * sizeof(MYSQL_BIND));
+ else
+ *bindings = (MYSQL_BIND*)srealloc(*bindings, count * sizeof(MYSQL_BIND));
+
+ newBinding = &((*bindings)[count-1]);
+
+ memset(newBinding, 0, sizeof(MYSQL_BIND));
+
+ if (value->getField()->getFormat() == ffAscii || value->getField()->getFormat() == ffText || value->getField()->getFormat() == ffMText)
+ {
+ newBinding->buffer_type = MYSQL_TYPE_STRING;
+ newBinding->buffer = value->getStrValueRef();
+ newBinding->buffer_length = value->getField()->getSize();
+ newBinding->length = value->getStrValueSizeRef();
+
+ newBinding->is_null = value->getNullRef();
+ newBinding->error = 0; // #TODO
+ }
+ else if (value->getField()->getFormat() == ffMlob)
+ {
+ newBinding->buffer_type = MYSQL_TYPE_BLOB;
+ newBinding->buffer = value->getStrValueRef();
+ newBinding->buffer_length = value->getField()->getSize();
+ newBinding->length = value->getStrValueSizeRef();
+
+ newBinding->is_null = value->getNullRef();
+ newBinding->error = 0; // #TODO
+ }
+ else if (value->getField()->getFormat() == ffFloat)
+ {
+ newBinding->buffer_type = MYSQL_TYPE_FLOAT;
+ newBinding->buffer = value->getFloatValueRef();
+
+ newBinding->length = 0; // #TODO
+ newBinding->is_null = value->getNullRef();
+ newBinding->error = 0; // #TODO
+ }
+ else if (value->getField()->getFormat() == ffDateTime)
+ {
+ newBinding->buffer_type = MYSQL_TYPE_DATETIME;
+ newBinding->buffer = value->getTimeValueRef();
+
+ newBinding->length = 0; // #TODO
+ newBinding->is_null = value->getNullRef();
+ newBinding->error = 0; // #TODO
+ }
+ else if (value->getField()->getFormat() == ffBigInt || value->getField()->getFormat() == ffUBigInt)
+ {
+ newBinding->buffer_type = MYSQL_TYPE_LONGLONG;
+ newBinding->buffer = value->getBigIntValueRef();
+ newBinding->is_unsigned = (value->getField()->getFormat() == ffUBigInt);
+
+ newBinding->length = 0;
+ newBinding->is_null = value->getNullRef();
+ newBinding->error = 0; // #TODO
+ }
+ else // ffInt, ffUInt
+ {
+ newBinding->buffer_type = MYSQL_TYPE_LONG;
+ newBinding->buffer = value->getIntValueRef();
+ newBinding->is_unsigned = (value->getField()->getFormat() == ffUInt);
+
+ newBinding->length = 0;
+ newBinding->is_null = value->getNullRef();
+ newBinding->error = 0; // #TODO
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Prepare Statement
+//***************************************************************************
+
+int cDbStatement::prepare()
+{
+ if (!connection->getMySql())
+ {
+ tell(0, "Error: Lost connection, can't prepare statement");
+ return fail;
+ }
+
+ if (!stmtTxt.length())
+ return fail;
+
+ if (buildErrors)
+ return fail;
+
+ stmt = mysql_stmt_init(connection->getMySql());
+
+ // prepare statement
+
+ if (mysql_stmt_prepare(stmt, stmtTxt.c_str(), stmtTxt.length()))
+ return connection->errorSql(connection, "prepare(stmt_prepare)", stmt, stmtTxt.c_str());
+
+ if (outBind)
+ {
+ if (mysql_stmt_bind_result(stmt, outBind))
+ return connection->errorSql(connection, "execute(bind_result)", stmt);
+ }
+
+ if (inBind)
+ {
+ if (mysql_stmt_bind_param(stmt, inBind))
+ return connection->errorSql(connection, "buildPrimarySelect(bind_param)", stmt);
+ }
+
+ tell(2, "Statement '%s' with (%ld) in parameters and (%d) out bindings prepared",
+ stmtTxt.c_str(), mysql_stmt_param_count(stmt), outCount);
+
+ return success;
+}
+
+//***************************************************************************
+// Show Statistic
+//***************************************************************************
+
+void cDbStatement::showStat()
+{
+ if (callsPeriod)
+ {
+ tell(0, "calls %4ld in %6.2fms; total %4ld [%s]",
+ callsPeriod, duration/1000, callsTotal, stmtTxt.c_str());
+
+ callsPeriod = 0;
+ duration = 0;
+ }
+}
+
+//***************************************************************************
+// cDbConnection statics
+//***************************************************************************
+
+char* cDbConnection::confPath = 0;
+char* cDbConnection::encoding = 0;
+char* cDbConnection::dbHost = strdup("localhost");
+int cDbConnection::dbPort = 3306;
+char* cDbConnection::dbUser = 0;
+char* cDbConnection::dbPass = 0;
+char* cDbConnection::dbName = 0;
+int cDbConnection::initThreads = 0;
+cMyMutex cDbConnection::initMutex;
+
+//***************************************************************************
+// Class cDbTable
+//***************************************************************************
+
+//***************************************************************************
+// Object
+//***************************************************************************
+
+cDbTable::cDbTable(cDbConnection* aConnection, const char* name)
+{
+ connection = aConnection;
+ holdInMemory = no;
+ attached = no;
+
+ row = 0;
+ stmtSelect = 0;
+ stmtInsert = 0;
+ stmtUpdate = 0;
+ lastInsertId = na;
+
+ tableDef = dbDict.getTable(name);
+
+ if (tableDef)
+ row = new cDbRow(tableDef);
+ else
+ tell(0, "Fatal: Table '%s' missing in dictionary '%s'!", name, dbDict.getPath());
+}
+
+cDbTable::~cDbTable()
+{
+ close();
+
+ delete row;
+}
+
+//***************************************************************************
+// Open / Close
+//***************************************************************************
+
+int cDbTable::open(int allowAlter)
+{
+ if (!tableDef || !row)
+ return abrt;
+
+ if (attach() != success)
+ {
+ tell(0, "Could not access database '%s:%d' (tried to open %s)",
+ connection->getHost(), connection->getPort(), TableName());
+
+ return fail;
+ }
+
+ return init(allowAlter);
+}
+
+int cDbTable::close()
+{
+ if (stmtSelect) { delete stmtSelect; stmtSelect = 0; }
+ if (stmtInsert) { delete stmtInsert; stmtInsert = 0; }
+ if (stmtUpdate) { delete stmtUpdate; stmtUpdate = 0; }
+
+ detach();
+
+ return success;
+}
+
+//***************************************************************************
+// Attach / Detach
+//***************************************************************************
+
+int cDbTable::attach()
+{
+ if (isAttached())
+ return success;
+
+ if (connection->attachConnection() != success)
+ {
+ tell(0, "Could not access database '%s:%d'",
+ connection->getHost(), connection->getPort());
+
+ return fail;
+ }
+
+ attached = yes;
+
+ return success;
+}
+
+int cDbTable::detach()
+{
+ if (isAttached())
+ {
+ connection->detachConnection();
+ attached = no;
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Init
+//***************************************************************************
+
+int cDbTable::init(int allowAlter)
+{
+ std::string str;
+ std::map<std::string, cDbFieldDef*>::iterator f;
+ int n = 0;
+
+ if (!isConnected())
+ return fail;
+
+ // check/create table ...
+
+ if (exist() && allowAlter)
+ validateStructure(allowAlter);
+
+ if (createTable() != success)
+ return fail;
+
+ // check/create indices
+
+ createIndices();
+
+ // ------------------------------
+ // prepare BASIC statements
+ // ------------------------------
+
+ // select by primary key ...
+
+ stmtSelect = new cDbStatement(this);
+
+ stmtSelect->build("select ");
+
+ n = 0;
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ stmtSelect->bind(f->second, bndOut, n++ ? ", " : "");
+
+ stmtSelect->build(" from %s where ", TableName());
+
+ n = 0;
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ {
+ if (!(f->second->getType() & ftPrimary))
+ continue;
+
+ stmtSelect->bind(f->second, bndIn | bndSet, n++ ? " and " : "");
+ }
+
+ stmtSelect->build(";");
+
+ if (stmtSelect->prepare() != success)
+ return fail;
+
+ // -----------------------------------------
+ // insert
+
+ stmtInsert = new cDbStatement(this);
+
+ stmtInsert->build("insert into %s set ", TableName());
+
+ n = 0;
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ {
+ // don't insert autoinc and calculated fields
+
+ if (f->second->getType() & ftAutoinc)
+ continue;
+
+ stmtInsert->bind(f->second, bndIn | bndSet, n++ ? ", " : "");
+ }
+
+ stmtInsert->build(";");
+
+ if (stmtInsert->prepare() != success)
+ return fail;
+
+ // -----------------------------------------
+ // update via primary key ...
+
+ stmtUpdate = new cDbStatement(this);
+
+ stmtUpdate->build("update %s set ", TableName());
+
+ n = 0;
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ {
+ // don't update PKey, autoinc and not used fields
+
+ if (f->second->getType() & ftPrimary ||
+ f->second->getType() & ftAutoinc)
+ continue;
+
+ if (strcasecmp(f->second->getName(), "inssp") == 0) // don't update the insert stamp
+ continue;
+
+ stmtUpdate->bind(f->second, bndIn | bndSet, n++ ? ", " : "");
+ }
+
+ stmtUpdate->build(" where ");
+
+ n = 0;
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ {
+ if (!(f->second->getType() & ftPrimary))
+ continue;
+
+ stmtUpdate->bind(f->second, bndIn | bndSet, n++ ? " and " : "");
+ }
+
+ stmtUpdate->build(";");
+
+ if (stmtUpdate->prepare() != success)
+ return fail;
+
+ return success;
+}
+
+//***************************************************************************
+// Check Table
+//***************************************************************************
+
+int cDbTable::exist(const char* name)
+{
+ if (isEmpty(name))
+ name = TableName();
+
+ if (!connection || !connection->getMySql())
+ return fail;
+
+ MYSQL_RES* result = mysql_list_tables(connection->getMySql(), name);
+ MYSQL_ROW tabRow = mysql_fetch_row(result);
+ mysql_free_result(result);
+
+ return tabRow ? yes : no;
+}
+
+//***************************************************************************
+// Validate Structure
+//***************************************************************************
+
+struct FieldInfo
+{
+ std::string columnFormat;
+ std::string description;
+ std::string def;
+};
+
+int cDbTable::validateStructure(int allowAlter)
+{
+ std::map<std::string, FieldInfo, _casecmp_> fields;
+ MYSQL_RES* result;
+ MYSQL_ROW row;
+ std::map<std::string, FieldInfo, _casecmp_>::iterator it;
+ int needDetach = no;
+
+ if (!allowAlter)
+ return done;
+
+ const char* select = "select column_name, column_type, column_comment, data_type, is_nullable, "
+ " character_maximum_length, column_default, numeric_precision "
+ " from information_schema.columns "
+ " where table_name = '%s' and table_schema= '%s'";
+
+ if (!isAttached())
+ {
+ needDetach = yes;
+
+ if (attach() != success)
+ return fail;
+ }
+
+ // ------------------------
+ // execute query
+
+ if (connection->query(select, TableName(), connection->getName()) != success)
+ {
+ connection->errorSql(getConnection(), "validateStructure()", 0);
+ if (needDetach) detach();
+ return fail;
+ }
+
+ // ------------------------
+ // process the result
+
+ if (!(result = mysql_store_result(connection->getMySql())))
+ {
+ connection->errorSql(getConnection(), "validateStructure()");
+ if (needDetach) detach();
+ return fail;
+ }
+
+ while ((row = mysql_fetch_row(result)))
+ {
+ fields[row[0]].columnFormat = row[1];
+ fields[row[0]].description = row[2];
+ fields[row[0]].def = row[6] ? row[6] : "";
+ }
+
+ mysql_free_result(result);
+
+ // --------------------------------------
+ // validate if all fields of dict are in
+ // table and check their format, ...
+
+ for (int i = 0; i < fieldCount(); i++)
+ {
+ char colType[100];
+
+ tell(4, "Check field '%s'", getField(i)->getName());
+
+ if (fields.find(getField(i)->getDbName()) == fields.end())
+ alterAddField(getField(i));
+
+ else
+ {
+ FieldInfo* fieldInfo = &fields[getField(i)->getDbName()];
+
+ getField(i)->toColumnFormat(colType);
+
+ if (strcasecmp(fieldInfo->columnFormat.c_str(), colType) != 0 ||
+ strcasecmp(fieldInfo->description.c_str(), getField(i)->getDescription()) != 0 ||
+ (strcasecmp(fieldInfo->def.c_str(), getField(i)->getDefault()) != 0 && !(getField(i)->getType() & ftPrimary)))
+ {
+ alterModifyField(getField(i));
+ }
+ }
+ }
+
+ // --------------------------------------
+ // check if table contains unused fields
+ // and report them
+
+ for (it = fields.begin(); it != fields.end(); it++)
+ {
+ if (!getRow()->getFieldByDbName(it->first.c_str()))
+ {
+ if (allowAlter == 2)
+ alterDropField(it->first.c_str());
+ else
+ tell(0, "Info: Field '%s' not used anymore, "
+ "to remove it call 'ALTER TABLE %s DROP COLUMN %s;' manually",
+ it->first.c_str(), TableName(), it->first.c_str());
+ }
+ }
+
+ if (needDetach) detach();
+
+ return success;
+}
+
+//***************************************************************************
+// Alter 'Modify Field'
+//***************************************************************************
+
+int cDbTable::alterModifyField(cDbFieldDef* def)
+{
+ char* statement;
+ char colType[100];
+
+ tell(0, " Info: Definition of field '%s.%s' modified, try to alter table",
+ TableName(), def->getName());
+
+ // alter table events modify column guest varchar(50)
+
+ asprintf(&statement, "alter table %s modify column %s %s comment '%s' %s%s%s",
+ TableName(),
+ def->getDbName(),
+ def->toColumnFormat(colType),
+ def->getDbDescription(),
+ !isEmpty(def->getDefault()) ? "default '" : "",
+ !isEmpty(def->getDefault()) ? def->getDefault() : "",
+ !isEmpty(def->getDefault()) ? "'" : ""
+ );
+
+ tell(1, "Execute [%s]", statement);
+
+ if (connection->query("%s", statement))
+ return connection->errorSql(getConnection(), "alterAddField()",
+ 0, statement);
+
+ free(statement);
+
+ return done;
+}
+
+//***************************************************************************
+// Alter 'Add Field'
+//***************************************************************************
+
+int cDbTable::alterAddField(cDbFieldDef* def)
+{
+ std::string statement;
+ char colType[100];
+
+ tell(0, "Info: Missing field '%s.%s', try to alter table",
+ TableName(), def->getName());
+
+ // alter table channelmap add column ord int(11) [after source]
+
+ statement = std::string("alter table ") + TableName() + std::string(" add column ")
+ + def->getDbName() + std::string(" ") + def->toColumnFormat(colType);
+
+ if (def->getFormat() != ffMlob)
+ {
+ if (def->getType() & ftAutoinc)
+ statement += " not null auto_increment";
+ else if (!isEmpty(def->getDefault()))
+ statement += " default '" + std::string(def->getDefault()) + "'";
+ }
+
+ if (!isEmpty(def->getDbDescription()))
+ statement += std::string(" comment '") + def->getDbDescription() + std::string("'");
+
+ if (def->getIndex() > 0)
+ statement += std::string(" after ") + getField(def->getIndex()-1)->getDbName();
+
+ tell(1, "Execute [%s]", statement.c_str());
+
+ if (connection->query("%s", statement.c_str()))
+ return connection->errorSql(getConnection(), "alterAddField()",
+ 0, statement.c_str());
+
+ return done;
+}
+
+//***************************************************************************
+// Alter 'Drop Field'
+//***************************************************************************
+
+int cDbTable::alterDropField(const char* name)
+{
+ char* statement;
+
+ tell(0, "Info: Unused field '%s', try to drop it", name);
+
+ // alter table channelmap add column ord int(11) [after source]
+
+ asprintf(&statement, "alter table %s drop column %s", TableName(), name);
+
+ tell(1, "Execute [%s]", statement);
+
+ if (connection->query("%s", statement))
+ return connection->errorSql(getConnection(), "alterDropField()",
+ 0, statement);
+
+ free(statement);
+
+ return done;
+}
+
+//***************************************************************************
+// Create Table
+//***************************************************************************
+
+int cDbTable::createTable()
+{
+ std::string statement;
+ std::string aKey;
+ int needDetach = no;
+
+ if (!tableDef || !row)
+ return abrt;
+
+ if (!isAttached())
+ {
+ needDetach = yes;
+
+ if (attach() != success)
+ return fail;
+ }
+
+ // table exists -> nothing to do
+
+ if (exist())
+ {
+ if (needDetach) detach();
+ return done;
+ }
+
+ tell(0, "Initialy creating table '%s'", TableName());
+
+ // build 'create' statement ...
+
+ statement = std::string("create table ") + TableName() + std::string("(");
+
+ for (int i = 0; i < fieldCount(); i++)
+ {
+ char colType[100];
+
+ if (i) statement += std::string(", ");
+
+ statement += std::string(getField(i)->getDbName()) + " " + std::string(getField(i)->toColumnFormat(colType));
+
+ if (getField(i)->getFormat() != ffMlob)
+ {
+ if (getField(i)->getType() & ftAutoinc)
+ statement += " not null auto_increment";
+ else if (!isEmpty(getField(i)->getDefault()))
+ statement += " default '" + std::string(getField(i)->getDefault()) + "'";
+ }
+
+ if (!isEmpty(getField(i)->getDbDescription()))
+ statement += std::string(" comment '") + getField(i)->getDbDescription() + std::string("'");
+ }
+
+ aKey = "";
+
+ for (int i = 0, n = 0; i < fieldCount(); i++)
+ {
+ if (getField(i)->getType() & ftPrimary)
+ {
+ if (n++) aKey += std::string(", ");
+ aKey += std::string(getField(i)->getDbName()) + " DESC";
+ }
+ }
+
+ if (aKey.length())
+ {
+ statement += std::string(", PRIMARY KEY(");
+ statement += aKey;
+ statement += ")";
+ }
+
+ aKey = "";
+
+ for (int i = 0, n = 0; i < fieldCount(); i++)
+ {
+ if (getField(i)->getType() & ftAutoinc && !(getField(i)->getType() & ftPrimary))
+ {
+ if (n++) aKey += std::string(", ");
+ aKey += std::string(getField(i)->getDbName()) + " DESC";
+ }
+ }
+
+ if (aKey.length())
+ {
+ statement += std::string(", KEY(");
+ statement += aKey;
+ statement += ")";
+ }
+
+ statement += std::string(") ENGINE=InnoDB ROW_FORMAT=DYNAMIC;");
+
+ tell(1, "%s", statement.c_str());
+
+ if (connection->query("%s", statement.c_str()))
+ {
+ if (needDetach) detach();
+ return connection->errorSql(getConnection(), "createTable()",
+ 0, statement.c_str());
+ }
+
+ if (needDetach) detach();
+
+ return success;
+}
+
+//***************************************************************************
+// Create Indices
+//***************************************************************************
+
+int cDbTable::createIndices()
+{
+ std::string statement;
+
+ tell(5, "Initialy checking indices for '%s'", TableName());
+
+ // check/create indexes
+
+ for (int i = 0; i < tableDef->indexCount(); i++)
+ {
+ cDbIndexDef* index = tableDef->getIndex(i);
+ int fCount;
+ std::string idxName;
+
+ if (!index->fieldCount())
+ continue;
+
+ // check
+
+ idxName = "idx" + std::string(index->getName());
+
+ checkIndex(idxName.c_str(), fCount);
+
+ if (fCount != index->fieldCount())
+ {
+ // create index
+
+ statement = "create index " + idxName;
+ statement += " on " + std::string(TableName()) + "(";
+
+ int n = 0;
+
+ for (int f = 0; f < index->fieldCount(); f++)
+ {
+ cDbFieldDef* fld = index->getField(f);
+
+ if (fld)
+ {
+ if (n++) statement += std::string(", ");
+ statement += fld->getDbName();
+ }
+ }
+
+ if (!n) continue;
+
+ statement += ");";
+ tell(1, "%s", statement.c_str());
+
+ if (connection->query("%s", statement.c_str()))
+ return connection->errorSql(getConnection(), "createIndices()",
+ 0, statement.c_str());
+ }
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Check Index
+//***************************************************************************
+
+int cDbTable::checkIndex(const char* idxName, int& fieldCount)
+{
+ enum IndexQueryFields
+ {
+ idTable,
+ idNonUnique,
+ idKeyName,
+ idSeqInIndex,
+ idColumnName,
+ idCollation,
+ idCardinality,
+ idSubPart,
+ idPacked,
+ idNull,
+ idIndexType,
+ idComment,
+ idIndexComment,
+
+ idCount
+ };
+
+ MYSQL_RES* result;
+ MYSQL_ROW row;
+
+ fieldCount = 0;
+
+ if (connection->query("show index from %s", TableName()) != success)
+ {
+ connection->errorSql(getConnection(), "checkIndex()", 0);
+
+ return fail;
+ }
+
+ if ((result = mysql_store_result(connection->getMySql())))
+ {
+ while ((row = mysql_fetch_row(result)))
+ {
+ tell(5, "%s: %-20s %s %s",
+ row[idTable], row[idKeyName],
+ row[idSeqInIndex], row[idColumnName]);
+
+ if (strcasecmp(row[idKeyName], idxName) == 0)
+ fieldCount++;
+ }
+
+ mysql_free_result(result);
+
+ return success;
+ }
+
+ connection->errorSql(getConnection(), "checkIndex()");
+
+ return fail;
+}
+
+//***************************************************************************
+// Copy Values
+//***************************************************************************
+
+void cDbTable::copyValues(cDbRow* r, int typesFilter)
+{
+ std::map<std::string, cDbFieldDef*>::iterator f;
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ {
+ cDbFieldDef* fld = f->second;
+
+ if (r->isNull(fld)) // skip where source field is NULL
+ continue;
+
+ if (!(typesFilter & fld->getType())) // Filter
+ continue;
+
+ switch (fld->getFormat())
+ {
+ case ffAscii:
+ case ffText:
+ case ffMText:
+ case ffMlob:
+ row->setValue(fld, r->getStrValue(fld));
+ break;
+
+ case ffFloat:
+ row->setValue(fld, r->getFloatValue(fld));
+ break;
+
+ case ffDateTime:
+ row->setValue(fld, r->getTimeValue(fld));
+ break;
+
+ case ffBigInt:
+ case ffUBigInt:
+ row->setBigintValue(fld, r->getBigintValue(fld));
+ break;
+
+ case ffInt:
+ case ffUInt:
+ row->setValue(fld, r->getIntValue(fld));
+ break;
+
+ default:
+ tell(0, "Fatal unhandled field type %d", fld->getFormat());
+ }
+ }
+}
+
+//***************************************************************************
+// SQL Error
+//***************************************************************************
+
+int cDbConnection::errorSql(cDbConnection* connection, const char* prefix,
+ MYSQL_STMT* stmt, const char* stmtTxt)
+{
+ if (!connection || !connection->mysql)
+ {
+ tell(0, "SQL-Error in '%s'", prefix);
+ return fail;
+ }
+
+ int error = mysql_errno(connection->mysql);
+ char* conErr = 0;
+ char* stmtErr = 0;
+
+ if (error == CR_SERVER_LOST ||
+ error == CR_SERVER_GONE_ERROR ||
+ error == CR_INVALID_CONN_HANDLE ||
+ error == CR_COMMANDS_OUT_OF_SYNC ||
+ error == CR_SERVER_LOST_EXTENDED ||
+ error == CR_STMT_CLOSED ||
+ error == CR_CONN_UNKNOW_PROTOCOL ||
+ error == CR_UNSUPPORTED_PARAM_TYPE ||
+ error == CR_NO_PREPARE_STMT ||
+ error == CR_SERVER_HANDSHAKE_ERR ||
+ error == CR_WRONG_HOST_INFO ||
+ error == CR_OUT_OF_MEMORY ||
+ error == CR_IPSOCK_ERROR ||
+ error == CR_SOCKET_CREATE_ERROR ||
+ error == CR_CONNECTION_ERROR ||
+ error == CR_TCP_CONNECTION ||
+ error == CR_PARAMS_NOT_BOUND ||
+ error == CR_CONN_HOST_ERROR ||
+ error == CR_SSL_CONNECTION_ERROR
+
+ // to be continued - not all errors should result in a reconnect ...
+
+ )
+ {
+ connectDropped = yes;
+ }
+
+ if (error)
+ asprintf(&conErr, "%s (%d) ", mysql_error(connection->mysql), error);
+
+ if (stmt || stmtTxt)
+ asprintf(&stmtErr, "'%s' [%s]",
+ stmt ? mysql_stmt_error(stmt) : "",
+ stmtTxt ? stmtTxt : "");
+
+ tell(0, "SQL-Error in '%s' - %s%s", prefix,
+ conErr ? conErr : "", stmtErr ? stmtErr : "");
+
+ free(conErr);
+ free(stmtErr);
+
+ if (connectDropped)
+ tell(0, "Fatal, lost connection to mysql server, aborting pending actions");
+
+ return fail;
+}
+
+//***************************************************************************
+// Delete Where
+//***************************************************************************
+
+int cDbTable::deleteWhere(const char* where, ...)
+{
+ std::string stmt;
+ char* tmp;
+ va_list more;
+
+ if (!connection || !connection->getMySql())
+ return fail;
+
+ va_start(more, where);
+ vasprintf(&tmp, where, more);
+
+ stmt = "delete from " + std::string(TableName()) + " where " + std::string(tmp);
+
+ free(tmp);
+
+ if (connection->query("%s", stmt.c_str()))
+ return connection->errorSql(connection, "deleteWhere()", 0, stmt.c_str());
+
+ return success;
+}
+
+//***************************************************************************
+// Coiunt Where
+//***************************************************************************
+
+int cDbTable::countWhere(const char* where, int& count, const char* what)
+{
+ std::string tmp;
+ MYSQL_RES* res;
+ MYSQL_ROW data;
+
+ count = 0;
+
+ if (isEmpty(what))
+ what = "count(1)";
+
+ if (!isEmpty(where))
+ tmp = "select " + std::string(what) + " from " + std::string(TableName()) + " where " + std::string(where);
+ else
+ tmp = "select " + std::string(what) + " from " + std::string(TableName());
+
+ if (connection->query("%s", tmp.c_str()))
+ return connection->errorSql(connection, "countWhere()", 0, tmp.c_str());
+
+ if ((res = mysql_store_result(connection->getMySql())))
+ {
+ data = mysql_fetch_row(res);
+
+ if (data)
+ count = atoi(data[0]);
+
+ mysql_free_result(res);
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Truncate
+//***************************************************************************
+
+int cDbTable::truncate()
+{
+ std::string tmp;
+
+ tmp = "delete from " + std::string(TableName());
+
+ if (connection->query("%s", tmp.c_str()))
+ return connection->errorSql(connection, "truncate()", 0, tmp.c_str());
+
+ return success;
+}
+
+
+//***************************************************************************
+// Store
+//***************************************************************************
+
+int cDbTable::store()
+{
+ int found;
+
+ // insert or just update ...
+
+ if (stmtSelect->execute(/*noResult =*/ yes) != success)
+ {
+ connection->errorSql(connection, "store()");
+ return no;
+ }
+
+ found = stmtSelect->getAffected() == 1;
+ stmtSelect->freeResult();
+
+ if (found)
+ return update();
+ else
+ return insert();
+}
+
+//***************************************************************************
+// Insert
+//***************************************************************************
+
+int cDbTable::insert(time_t inssp)
+{
+ std::map<std::string, cDbFieldDef*>::iterator f;
+ lastInsertId = na;
+
+ if (!stmtInsert)
+ {
+ tell(0, "Fatal missing insert statement\n");
+ return fail;
+ }
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ {
+ cDbFieldDef* fld = f->second;
+
+ if (strcasecmp(fld->getName(), "updsp") == 0 || strcasecmp(fld->getName(), "inssp") == 0)
+ setValue(fld, inssp ? inssp : time(0));
+
+ else if (getValue(fld)->isNull() && !isEmpty(fld->getDefault()))
+ setValue(fld, fld->getDefault());
+ }
+
+ if (stmtInsert->execute())
+ return fail;
+
+ lastInsertId = stmtInsert->getLastInsertId();
+
+ return stmtInsert->getAffected() == 1 ? success : fail;
+}
+
+//***************************************************************************
+// Update
+//***************************************************************************
+
+int cDbTable::update(time_t updsp)
+{
+ std::map<std::string, cDbFieldDef*>::iterator f;
+
+ if (!stmtUpdate)
+ {
+ tell(0, "Fatal missing update statement\n");
+ return fail;
+ }
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ {
+ cDbFieldDef* fld = f->second;
+
+ if (strcasecmp(fld->getName(), "updsp") == 0)
+ setValue(fld, updsp ? updsp : time(0));
+
+ else if (getValue(fld)->isNull() && !isEmpty(fld->getDefault()))
+ setValue(fld, fld->getDefault());
+ }
+
+ if (stmtUpdate->execute())
+ return fail;
+
+ return stmtUpdate->getAffected() == 1 ? success : fail;
+}
+
+//***************************************************************************
+// Find
+//***************************************************************************
+
+int cDbTable::find()
+{
+ if (!stmtSelect)
+ return no;
+
+ if (stmtSelect->execute() != success)
+ {
+ connection->errorSql(connection, "find()");
+ return no;
+ }
+
+ return stmtSelect->getAffected() == 1 ? yes : no;
+}
+
+//***************************************************************************
+// Find via Statement
+//***************************************************************************
+
+int cDbTable::find(cDbStatement* stmt)
+{
+ if (!stmt)
+ return no;
+
+ if (stmt->execute() != success)
+ {
+ connection->errorSql(connection, "find(stmt)");
+ return no;
+ }
+
+ return stmt->getAffected() > 0 ? yes : no;
+}
+
+//***************************************************************************
+// Fetch
+//***************************************************************************
+
+int cDbTable::fetch(cDbStatement* stmt)
+{
+ if (!stmt)
+ return no;
+
+ return stmt->fetch();
+}
+
+//***************************************************************************
+// Reset Fetch
+//***************************************************************************
+
+void cDbTable::reset(cDbStatement* stmt)
+{
+ if (stmt)
+ stmt->freeResult();
+}
diff --git a/lib/db.h b/lib/db.h
new file mode 100644
index 0000000..e16aeb8
--- /dev/null
+++ b/lib/db.h
@@ -0,0 +1,1370 @@
+/*
+ * db.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __DB_H
+#define __DB_H
+
+#include <linux/unistd.h>
+
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <errno.h>
+
+#include <mysql/mysql.h>
+
+#include <list>
+
+#include "common.h"
+#include "dbdict.h"
+
+class cDbTable;
+class cDbConnection;
+
+//***************************************************************************
+// cDbValue
+//***************************************************************************
+
+class cDbValue : public cDbService
+{
+ public:
+
+ cDbValue(cDbFieldDef* f = 0)
+ {
+ field = 0;
+ strValue = 0;
+ ownField = 0;
+ changed = 0;
+
+ if (f) setField(f);
+ }
+
+ cDbValue(const char* name, FieldFormat format, int size)
+ {
+ strValue = 0;
+ changed = 0;
+ ownField = new cDbFieldDef(name, name, format, size, ftData, 0);
+
+ field = ownField;
+ strValue = (char*)calloc(field->getSize()+TB, sizeof(char));
+
+ clear();
+ }
+
+ virtual ~cDbValue()
+ {
+ free();
+ }
+
+ void free()
+ {
+ clear();
+ ::free(strValue);
+ strValue = 0;
+
+ if (ownField)
+ {
+ delete ownField;
+ ownField = 0;
+ }
+
+ field = 0;
+ }
+
+ void clear()
+ {
+ if (strValue)
+ *strValue = 0;
+
+ strValueSize = 0;
+ numValue = 0;
+ longlongValue = 0;
+ floatValue = 0;
+ memset(&timeValue, 0, sizeof(timeValue));
+
+ nullValue = 1;
+ changed = 0;
+ }
+
+ void clearChanged()
+ {
+ changed = 0;
+ }
+
+ virtual void setField(cDbFieldDef* f)
+ {
+ free();
+ field = f;
+
+ if (field)
+ strValue = (char*)calloc(field->getSize()+TB, sizeof(char));
+ }
+
+ virtual cDbFieldDef* getField() { return field; }
+ virtual const char* getName() { return field->getName(); }
+ virtual const char* getDbName() { return field->getDbName(); }
+
+ void setNull()
+ {
+ int c = changed;
+ int n = nullValue;
+
+ clear();
+ changed = c;
+
+ if (!n)
+ changed++;
+ }
+
+ void __attribute__ ((format(printf, 2, 3))) sPrintf(const char* format, ...)
+ {
+ va_list more;
+ char* buf = 0;
+
+ if (!format)
+ return ;
+
+ va_start(more, format);
+ vasprintf(&buf, format, more);
+
+ setValue(buf);
+
+ ::free(buf);
+ }
+
+ void setValue(const char* value, int size = 0)
+ {
+ int modified = no;
+
+ if (field->getFormat() != ffAscii && field->getFormat() != ffText &&
+ field->getFormat() != ffMText && field->getFormat() != ffMlob)
+ {
+ tell(0, "Setting invalid field format for '%s', expected ASCII, TEXT or MLOB",
+ field->getName());
+ return;
+ }
+
+ if (field->getFormat() == ffMlob && !size)
+ {
+ tell(0, "Missing size for MLOB field '%s'", field->getName());
+ return;
+ }
+
+ if (value && size)
+ {
+ if (size > field->getSize())
+ {
+ tell(0, "Warning, size of %d for '%s' exeeded, got %d bytes!",
+ field->getSize(), field->getName(), size);
+
+ size = field->getSize();
+ }
+
+ if (memcmp(strValue, value, size) != 0 || isNull())
+ modified = yes;
+
+ clear();
+ memcpy(strValue, value, size);
+ strValue[size] = 0;
+ strValueSize = size;
+ nullValue = 0;
+ }
+
+ else if (value)
+ {
+ if (strlen(value) > (size_t)field->getSize())
+ tell(0, "Warning, size of %d for '%s' exeeded (needed %ld) [%s]",
+ field->getSize(), field->getName(), (long)strlen(value), value);
+
+ if (strncmp(strValue, value, strlen(value)) != 0 || isNull())
+ modified = yes;
+
+ clear();
+ sprintf(strValue, "%.*s", field->getSize(), value);
+ strValueSize = strlen(strValue);
+ nullValue = 0;
+ }
+
+ if (modified) // increment changed after calling clear()
+ changed++;
+ }
+
+ void setCharValue(char value)
+ {
+ char tmp[2] = "";
+ tmp[0] = value;
+ tmp[1] = 0;
+ setValue(tmp);
+ }
+
+ void setValue(int value)
+ {
+ setValue((long)value);
+ }
+
+ void setValue(long value)
+ {
+ if (field->getFormat() == ffInt || field->getFormat() == ffUInt)
+ {
+ if (numValue != value || isNull())
+ changed++;
+
+ numValue = value;
+ nullValue = 0;
+ }
+ else if (field->getFormat() == ffDateTime)
+ {
+ struct tm tm;
+ time_t v = value;
+ time_t o = getTimeValue();
+
+ memset(&tm, 0, sizeof(tm));
+ localtime_r(&v, &tm);
+
+ timeValue.year = tm.tm_year + 1900;
+ timeValue.month = tm.tm_mon + 1;
+ timeValue.day = tm.tm_mday;
+
+ timeValue.hour = tm.tm_hour;
+ timeValue.minute = tm.tm_min;
+ timeValue.second = tm.tm_sec;
+
+ nullValue = 0;
+
+ if (o != getTimeValue())
+ changed++;
+ }
+ else
+ {
+ tell(0, "Setting invalid field format for '%s'", field->getName());
+ }
+ }
+
+ void setValue(double value)
+ {
+ if (field->getFormat() == ffInt || field->getFormat() == ffUInt)
+ {
+ if (numValue != value || isNull())
+ changed++;
+
+ numValue = value;
+ nullValue = 0;
+ }
+ else if (field->getFormat() == ffBigInt || field->getFormat() == ffUBigInt)
+ {
+ if (longlongValue != value || isNull())
+ changed++;
+
+ longlongValue = value;
+ nullValue = 0;
+ }
+ else if (field->getFormat() == ffFloat)
+ {
+ if (floatValue != value || isNull())
+ changed++;
+
+ floatValue = value;
+ nullValue = 0;
+ }
+ else
+ {
+ tell(0, "Setting invalid field format for '%s'", field->getName());
+ }
+ }
+
+ void setBigintValue(int64_t value)
+ {
+ if (field->getFormat() == ffInt || field->getFormat() == ffUInt)
+ {
+ if (numValue != value)
+ changed++;
+
+ numValue = value;
+ nullValue = 0;
+ }
+
+ else if (field->getFormat() == ffBigInt || field->getFormat() == ffUBigInt)
+ {
+ if (longlongValue != value)
+ changed++;
+
+ longlongValue = value;
+ nullValue = 0;
+ }
+ }
+
+ int hasValue(long value)
+ {
+ if (field->getFormat() == ffInt || field->getFormat() == ffUInt)
+ return numValue == value;
+
+ if (field->getFormat() == ffDateTime)
+ return no; // to be implemented!
+
+ tell(0, "Setting invalid field format for '%s'", field->getName());
+
+ return no;
+ }
+
+ int hasValue(double value)
+ {
+ if (field->getFormat() == ffInt || field->getFormat() == ffUInt)
+ return numValue == value;
+
+ if (field->getFormat() == ffBigInt || field->getFormat() == ffUBigInt)
+ return longlongValue == value;
+
+ if (field->getFormat() == ffFloat)
+ return floatValue == value;
+
+ tell(0, "Setting invalid field format for '%s'", field->getName());
+
+ return no;
+ }
+
+ int hasValue(const char* value)
+ {
+ if (!value)
+ value = "";
+
+ if (field->getFormat() != ffAscii && field->getFormat() != ffText &&
+ field->getFormat() != ffMText && field->getFormat() != ffMlob)
+ {
+ tell(0, "Checking invalid field format for '%s', expected ASCII or TEXT",
+ field->getName());
+ return no;
+ }
+
+ return strcmp(getStrValue(), value) == 0;
+ }
+
+ int hasCharValue(char value)
+ {
+ if (field->getFormat() != ffAscii)
+ {
+ tell(0, "Checking invalid field format for '%s', expected ASCII or TEXT",
+ field->getName());
+ return no;
+ }
+
+ return getStrValueSize() == 1 && toupper(getCharValue()) == toupper(value);
+ }
+
+ time_t getTimeValue()
+ {
+ struct tm tm;
+ memset(&tm, 0, sizeof(tm));
+
+ tm.tm_isdst = -1; // force DST auto detect
+ tm.tm_year = timeValue.year - 1900;
+ tm.tm_mon = timeValue.month - 1;
+ tm.tm_mday = timeValue.day;
+
+ tm.tm_hour = timeValue.hour;
+ tm.tm_min = timeValue.minute;
+ tm.tm_sec = timeValue.second;
+
+ return mktime(&tm);
+ }
+
+ unsigned long* getStrValueSizeRef() { return &strValueSize; }
+ unsigned long getStrValueSize() { return strValueSize; }
+ const char* getStrValue() { return !isNull() && strValue ? strValue : ""; }
+ char getCharValue() { return !isNull() && strValue ? strValue[0] : 0; }
+ long getIntValue() { return !isNull() ? numValue : 0; }
+
+ int64_t getBigintValue()
+ {
+ if (isNull())
+ return 0;
+
+ if (field->getFormat() == ffBigInt || field->getFormat() == ffUBigInt)
+ return longlongValue;
+
+ return numValue;
+ }
+
+ float getFloatValue() { return !isNull() ? floatValue : 0; }
+ int isNull() { return nullValue; }
+ int getChanges() { return changed; }
+
+ int isEmpty()
+ {
+ if (isNull())
+ return yes;
+
+ if (field->getFormat() == ffInt || field->getFormat() == ffUInt)
+ return numValue == 0;
+ else if (field->getFormat() == ffDateTime)
+ return no;
+ else if (field->getFormat() == ffAscii || field->getFormat() == ffText ||
+ field->getFormat() == ffMText || field->getFormat() == ffMlob)
+ return ::isEmpty(strValue);
+ else if (field->getFormat() == ffFloat)
+ return floatValue == 0;
+
+ return no;
+ }
+
+ char* getStrValueRef() { return strValue; }
+ long* getIntValueRef() { return &numValue; }
+ int64_t* getBigIntValueRef() { return &longlongValue; }
+ MYSQL_TIME* getTimeValueRef() { return &timeValue; }
+ float* getFloatValueRef() { return &floatValue; }
+ my_bool* getNullRef() { return &nullValue; }
+
+ private:
+
+ cDbFieldDef* ownField;
+ cDbFieldDef* field;
+ long numValue;
+ int64_t longlongValue;
+ float floatValue;
+ MYSQL_TIME timeValue;
+ char* strValue;
+ unsigned long strValueSize;
+ my_bool nullValue;
+ int changed;
+};
+
+//***************************************************************************
+// cDbStatement
+//***************************************************************************
+
+class cDbStatement : public cDbService
+{
+ public:
+
+ cDbStatement(cDbTable* aTable);
+ cDbStatement(cDbConnection* aConnection, const char* sText = "");
+ virtual ~cDbStatement();
+
+ int execute(int noResult = no);
+ int find();
+ int fetch();
+ int freeResult();
+ void clear();
+
+ // interface
+
+ virtual int __attribute__ ((format(printf, 2, 3))) build(const char* format, ...);
+
+ void setBindPrefix(const char* p) { bindPrefix = p; }
+ void clrBindPrefix() { bindPrefix = 0; }
+ int bind(const char* fname, int mode, const char* delim = 0);
+ int bind(cDbValue* value, int mode, const char* delim = 0);
+ int bind(cDbTable* aTable, cDbFieldDef* field, int mode, const char* delim);
+ int bind(cDbTable* aTable, const char* fname, int mode, const char* delim);
+ int bind(cDbFieldDef* field, int mode, const char* delim = 0);
+ int bindAllOut(const char* delim = 0, int incTypes = ftData | ftPrimary, int excTypes = 0);
+
+ int bindCmp(const char* ctable, cDbValue* value,
+ const char* comp, const char* delim = 0);
+ int bindCmp(const char* ctable, cDbFieldDef* field, cDbValue* value,
+ const char* comp, const char* delim = 0);
+ int bindCmp(const char* ctable, const char* fname, cDbValue* value,
+ const char* comp, const char* delim = 0);
+ int bindText(const char* text, cDbValue* value,
+ const char* comp, const char* delim = 0);
+ int bindTextFree(const char* text, cDbValue* value, int mode = bndIn);
+
+ int bindInChar(const char* ctable, const char* fname,
+ cDbValue* value = 0, const char* delim = 0);
+
+ int appendBinding(cDbValue* value, BindType bt); // use this interface method seldom from external and with care!
+
+ // ..
+
+ int prepare();
+ int getAffected() { return affected; }
+ int getResultCount();
+ int getLastInsertId();
+ const char* asText() { return stmtTxt.c_str(); }
+ cDbTable* getTable() { return table; }
+ void showStat();
+
+ // data
+
+ static int explain; // debug explain
+
+ private:
+
+ std::string stmtTxt;
+ MYSQL_STMT* stmt;
+ int affected;
+ cDbConnection* connection;
+ cDbTable* table;
+ int inCount;
+ MYSQL_BIND* inBind; // to db
+ int outCount;
+ MYSQL_BIND* outBind; // from db (result)
+ MYSQL_RES* metaResult;
+ const char* bindPrefix;
+ int firstExec; // debug explain
+ int buildErrors;
+
+ unsigned long callsPeriod;
+ unsigned long callsTotal;
+ double duration;
+};
+
+//***************************************************************************
+// cDbStatements
+//***************************************************************************
+
+class cDbStatements
+{
+ public:
+
+ cDbStatements() { statisticPeriod = time(0); }
+ ~cDbStatements() {};
+
+ void append(cDbStatement* s) { statements.push_back(s); }
+ void remove(cDbStatement* s) { statements.remove(s); }
+
+ void showStat(const char* name)
+ {
+ tell(0, "Statement statistic of last %ld seconds from '%s':", time(0) - statisticPeriod, name);
+
+ for (std::list<cDbStatement*>::iterator it = statements.begin() ; it != statements.end(); ++it)
+ {
+ if (*it)
+ (*it)->showStat();
+ }
+
+ statisticPeriod = time(0);
+ }
+
+ private:
+
+ time_t statisticPeriod;
+ std::list<cDbStatement*> statements;
+};
+
+//***************************************************************************
+// Class Database Row
+//***************************************************************************
+
+#define GET_FIELD(name) \
+ cDbFieldDef* f = tableDef->getField(name); \
+ if (!f) \
+ { \
+ tell(0, "Fatal: Field '%s.%s' not defined (missing in dictionary)", tableDef->getName(), name); \
+ return ; \
+ } \
+
+#define GET_FIELD_RES(name, def) \
+ cDbFieldDef* f = tableDef->getField(name); \
+ if (!f) \
+ { \
+ tell(0, "Fatal: Field '%s.%s' not defined (missing in dictionary)", tableDef->getName(), name); \
+ return def; \
+ } \
+
+class cDbRow : public cDbService
+{
+ public:
+
+ cDbRow(cDbTableDef* t)
+ {
+ set(t);
+ }
+
+ cDbRow(const char* name)
+ {
+ cDbTableDef* t = dbDict.getTable(name);
+
+ if (t)
+ set(t);
+ else
+ tell(0, "Fatal: Table '%s' missing in dictionary '%s'!", name, dbDict.getPath());
+ }
+
+ virtual ~cDbRow() { delete[] dbValues; }
+
+ void set(cDbTableDef* t)
+ {
+ std::map<std::string, cDbFieldDef*>::iterator f;
+
+ tableDef = t;
+ dbValues = new cDbValue[tableDef->fieldCount()];
+
+ for (f = tableDef->dfields.begin(); f != tableDef->dfields.end(); f++)
+ dbValues[f->second->getIndex()].setField(f->second);
+ }
+
+ void clear()
+ {
+ for (int f = 0; f < tableDef->fieldCount(); f++)
+ dbValues[f].clear();
+ }
+
+ void clearChanged()
+ {
+ for (int f = 0; f < tableDef->fieldCount(); f++)
+ dbValues[f].clearChanged();
+ }
+
+ int getChanges()
+ {
+ int count = 0;
+
+ for (int f = 0; f < tableDef->fieldCount(); f++)
+ count += dbValues[f].getChanges();
+
+ return count;
+ }
+
+ std::string getChangedFields()
+ {
+ std::string s = "";
+
+ for (int f = 0; f < tableDef->fieldCount(); f++)
+ {
+ if (dbValues[f].getChanges())
+ {
+ if (s.length())
+ s += ",";
+
+ s += dbValues[f].getName() + std::string("=");
+
+ if (dbValues[f].getField()->hasFormat(ffInt) || dbValues[f].getField()->hasFormat(ffUInt))
+ s += num2Str(dbValues[f].getIntValue());
+ else
+ s += dbValues[f].getStrValue();
+ }
+ }
+
+ return s;
+ }
+
+ virtual cDbFieldDef* getField(int id) { return tableDef->getField(id); }
+ virtual cDbFieldDef* getField(const char* name) { return tableDef->getField(name); }
+ virtual cDbFieldDef* getFieldByDbName(const char* dbname) { return tableDef->getFieldByDbName(dbname); }
+ virtual int fieldCount() { return tableDef->fieldCount(); }
+
+ void setValue(cDbFieldDef* f, const char* value,
+ int size = 0) { dbValues[f->getIndex()].setValue(value, size); }
+ void setValue(cDbFieldDef* f, int value) { dbValues[f->getIndex()].setValue(value); }
+ void setValue(cDbFieldDef* f, long value) { dbValues[f->getIndex()].setValue(value); }
+ void setValue(cDbFieldDef* f, double value) { dbValues[f->getIndex()].setValue(value); }
+ void setBigintValue(cDbFieldDef* f, int64_t value) { dbValues[f->getIndex()].setBigintValue(value); }
+ void setCharValue(cDbFieldDef* f, char value) { dbValues[f->getIndex()].setCharValue(value); }
+
+ void setValue(const char* n, const char* value,
+ int size = 0) { GET_FIELD(n); dbValues[f->getIndex()].setValue(value, size); }
+ void setValue(const char* n, int value) { GET_FIELD(n); dbValues[f->getIndex()].setValue(value); }
+ void setValue(const char* n, long value) { GET_FIELD(n); dbValues[f->getIndex()].setValue(value); }
+ void setValue(const char* n, double value) { GET_FIELD(n); dbValues[f->getIndex()].setValue(value); }
+ void setBigintValue(const char* n, int64_t value) { GET_FIELD(n); dbValues[f->getIndex()].setBigintValue(value); }
+ void setCharValue(const char* n, char value) { GET_FIELD(n); dbValues[f->getIndex()].setCharValue(value); }
+
+ int hasValue(cDbFieldDef* f, const char* value) const { return dbValues[f->getIndex()].hasValue(value); }
+ int hasCharValue(cDbFieldDef* f, char value) const { return dbValues[f->getIndex()].hasCharValue(value); }
+ int hasValue(cDbFieldDef* f, long value) const { return dbValues[f->getIndex()].hasValue(value); }
+ int hasValue(cDbFieldDef* f, double value) const { return dbValues[f->getIndex()].hasValue(value); }
+
+ int hasValue(const char* n, const char* value) const { GET_FIELD_RES(n, no); return dbValues[f->getIndex()].hasValue(value); }
+ int hasCharValue(const char* n, char value) const { GET_FIELD_RES(n, no); return dbValues[f->getIndex()].hasCharValue(value); }
+ int hasValue(const char* n, long value) const { GET_FIELD_RES(n, no); return dbValues[f->getIndex()].hasValue(value); }
+ int hasValue(const char* n, double value) const { GET_FIELD_RES(n, no); return dbValues[f->getIndex()].hasValue(value); }
+
+ cDbValue* getValue(cDbFieldDef* f) { return &dbValues[f->getIndex()]; }
+ cDbValue* getValue(const char* n) { GET_FIELD_RES(n, 0); return &dbValues[f->getIndex()]; }
+
+ time_t getTimeValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getTimeValue(); }
+ const char* getStrValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getStrValue(); }
+ long getIntValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getIntValue(); }
+ int64_t getBigintValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getBigintValue(); }
+ float getFloatValue(cDbFieldDef* f) const { return dbValues[f->getIndex()].getFloatValue(); }
+ int isNull(cDbFieldDef* f) const { return dbValues[f->getIndex()].isNull(); }
+
+ const char* getStrValue(const char* n) const { GET_FIELD_RES(n, ""); return dbValues[f->getIndex()].getStrValue(); }
+ long getIntValue(const char* n) const { GET_FIELD_RES(n, 0); return dbValues[f->getIndex()].getIntValue(); }
+ int64_t getBigintValue(const char* n) const { GET_FIELD_RES(n, 0); return dbValues[f->getIndex()].getBigintValue(); }
+ float getFloatValue(const char* n) const { GET_FIELD_RES(n, 0); return dbValues[f->getIndex()].getFloatValue(); }
+ int isNull(const char* n) const { GET_FIELD_RES(n, yes); return dbValues[f->getIndex()].isNull(); }
+
+ cDbTableDef* getTableDef() { return tableDef; }
+
+ protected:
+
+ cDbTableDef* tableDef;
+ cDbValue* dbValues;
+};
+
+//***************************************************************************
+// Connection
+//***************************************************************************
+
+class cDbConnection
+{
+ public:
+
+ cDbConnection()
+ {
+ mysql = 0;
+ attached = 0;
+ inTact = no;
+ connectDropped = yes;
+ }
+
+ virtual ~cDbConnection()
+ {
+ close();
+ }
+
+ int isConnected() { return getMySql() != 0; }
+
+ int attachConnection()
+ {
+ static int first = yes;
+
+ if (!mysql)
+ {
+ connectDropped = yes;
+
+ tell(0, "Calling mysql_init(%ld)", syscall(__NR_gettid));
+
+ if (!(mysql = mysql_init(0)))
+ return errorSql(this, "attachConnection(init)");
+
+ if (!mysql_real_connect(mysql, dbHost,
+ dbUser, dbPass, dbName, dbPort, 0, 0))
+ {
+ close();
+
+ tell(0, "Error, connecting to database at '%s' on port (%d) failed",
+ dbHost, dbPort);
+
+ return fail;
+ }
+
+ connectDropped = no;
+
+ // init encoding
+
+ if (encoding && *encoding)
+ {
+ if (mysql_set_character_set(mysql, encoding))
+ errorSql(this, "init(character_set)");
+
+ if (first)
+ {
+ tell(0, "SQL client character now '%s'", mysql_character_set_name(mysql));
+ first = no;
+ }
+ }
+ }
+
+ attached++;
+
+ return success;
+ }
+
+ void detachConnection()
+ {
+ attached--;
+
+ if (!attached)
+ close();
+ }
+
+ void close()
+ {
+ if (mysql)
+ {
+ tell(0, "Closing mysql connection and calling mysql_thread_end(%ld)", syscall(__NR_gettid));
+
+ mysql_close(mysql);
+ mysql_thread_end();
+ mysql = 0;
+ attached = 0;
+ }
+ }
+
+ int check()
+ {
+ if (!isConnected())
+ return fail;
+
+ query("SELECT SYSDATE();");
+ queryReset();
+
+ return isConnected() ? success : fail;
+ }
+
+ virtual int __attribute__ ((format(printf, 2, 3))) query(const char* format, ...)
+ {
+ va_list more;
+
+ if (!format)
+ return fail;
+
+ va_start(more, format);
+
+ return vquery(format, more);
+ }
+
+ virtual int __attribute__ ((format(printf, 3, 4))) query(int& count, const char* format, ...)
+ {
+ int status;
+ va_list more;
+
+ count = 0;
+
+ if (!format)
+ return fail;
+
+ va_start(more, format);
+
+ if ((status = vquery(format, more)) == success)
+ {
+ MYSQL_RES* res;
+ MYSQL_ROW data;
+
+ // get affected rows ..
+
+ if ((res = mysql_store_result(getMySql())))
+ {
+ data = mysql_fetch_row(res);
+
+ if (data)
+ count = atoi(data[0]);
+
+ mysql_free_result(res);
+ }
+ }
+
+ return status;
+ }
+
+ virtual int vquery(const char* format, va_list more)
+ {
+ int status = 1;
+ MYSQL* h = getMySql();
+
+ if (h && format)
+ {
+ char* stmt;
+
+ vasprintf(&stmt, format, more);
+
+ if ((status = mysql_query(h, stmt)))
+ errorSql(this, stmt);
+
+ free(stmt);
+ }
+
+ return status ? fail : success;
+ }
+
+ virtual void queryReset()
+ {
+ if (getMySql())
+ {
+ MYSQL_RES* result = mysql_use_result(getMySql());
+ mysql_free_result(result);
+ }
+ }
+
+ // escapeSqlString - only need to be used in string statements not in bind values!!
+
+ virtual std::string escapeSqlString(const char* str)
+ {
+ std::string result = "";
+
+ if (!isConnected())
+ return result;
+
+ int length = strlen(str);
+ int bufferSize = length*2 + TB;
+
+ char* buffer = (char*)malloc(bufferSize);
+ mysql_real_escape_string(getMySql(), buffer, str, length);
+ result = buffer;
+ free(buffer);
+
+ return result;
+ }
+
+ virtual int executeSqlFile(const char* file)
+ {
+ FILE* f;
+ int res;
+ char* buffer;
+ int size = 1000;
+ int nread = 0;
+
+ if (!getMySql())
+ return fail;
+
+ if (!(f = fopen(file, "r")))
+ {
+ tell(0, "Fatal: Can't execute sql file '%s'; Error was '%s'", file, strerror(errno));
+ return fail;
+ }
+
+ buffer = (char*)malloc(size+1);
+
+ while ((res = fread(buffer+nread, 1, 1000, f)))
+ {
+ nread += res;
+ size += 1000;
+ buffer = srealloc(buffer, size+1);
+ }
+
+ fclose(f);
+ buffer[nread] = 0;
+
+ // execute statement
+
+ tell(2, "Executing '%s'", buffer);
+
+ if (query("%s", buffer))
+ {
+ free(buffer);
+ return errorSql(this, "executeSqlFile()");
+ }
+
+ free(buffer);
+
+ return success;
+ }
+
+ virtual int startTransaction()
+ {
+ inTact = yes;
+ return query("START TRANSACTION");
+ }
+
+ virtual int commit()
+ {
+ inTact = no;
+ return query("COMMIT");
+ }
+
+ virtual int rollback()
+ {
+ inTact = no;
+ return query("ROLLBACK");
+ }
+
+ virtual int inTransaction() { return inTact; }
+
+ MYSQL* getMySql()
+ {
+ if (connectDropped)
+ close();
+
+ return mysql;
+ }
+
+ int getAttachedCount() { return attached; }
+ void showStat(const char* name = "") { statements.showStat(name); }
+ int errorSql(cDbConnection* mysql, const char* prefix, MYSQL_STMT* stmt = 0, const char* stmtTxt = 0);
+
+ // data
+
+ cDbStatements statements; // all statements of this connection
+
+ // --------------
+ // static stuff
+
+ // set/get connecting data
+
+ static void setHost(const char* s) { free(dbHost); dbHost = strdup(s); }
+ static const char* getHost() { return dbHost; }
+ static void setName(const char* s) { free(dbName); dbName = strdup(s); }
+ static const char* getName() { return dbName; }
+ static void setUser(const char* s) { free(dbUser); dbUser = strdup(s); }
+ static const char* getUser() { return dbUser; }
+ static void setPass(const char* s) { free(dbPass); dbPass = strdup(s); }
+ static const char* getPass() { return dbPass; }
+ static void setPort(int port) { dbPort = port; }
+ static int getPort() { return dbPort; }
+ static void setEncoding(const char* enc) { free(encoding); encoding = strdup(enc); }
+ static const char* getEncoding() { return encoding; }
+ static void setConfPath(const char* cpath) { free(confPath); confPath = strdup(cpath); }
+ static const char* getConfPath() { return confPath; }
+
+ // -----------------------------------------------------------
+ // init() and exit() must exactly called 'once' per process
+
+ static int init()
+ {
+ int status = success;
+
+ initMutex.Lock();
+
+ if (!initThreads)
+ {
+ tell(1, "Info: Calling mysql_library_init()");
+
+ if (mysql_library_init(0, 0, 0))
+ {
+ tell(0, "Error: mysql_library_init() failed");
+ status = fail;
+ }
+ }
+ else
+ {
+ tell(1, "Info: Skipping calling mysql_library_init(), it's already done!");
+ }
+
+ initThreads++;
+ initMutex.Unlock();
+
+ return status;
+ }
+
+ static int exit()
+ {
+ initMutex.Lock();
+
+ initThreads--;
+
+ if (!initThreads)
+ {
+ tell(1, "Info: Released the last usage of mysql_lib, calling mysql_library_end() now");
+ mysql_library_end();
+
+ free(dbHost);
+ free(dbUser);
+ free(dbPass);
+ free(dbName);
+ free(encoding);
+ free(confPath);
+ }
+ else
+ {
+ tell(1, "Info: The mysql_lib is still in use, skipping mysql_library_end() call");
+ }
+
+ initMutex.Unlock();
+
+ return done;
+ }
+
+ private:
+
+ MYSQL* mysql;
+
+ int initialized;
+ int attached;
+ int inTact;
+ int connectDropped;
+
+ static cMyMutex initMutex;
+ static int initThreads;
+
+ static char* encoding;
+ static char* confPath;
+
+ // connecting data
+
+ static char* dbHost;
+ static int dbPort;
+ static char* dbName; // database name
+ static char* dbUser;
+ static char* dbPass;
+};
+
+//***************************************************************************
+// cDbTable
+//***************************************************************************
+
+class cDbTable : public cDbService
+{
+ public:
+
+ cDbTable(cDbConnection* aConnection, const char* name);
+ virtual ~cDbTable();
+
+ virtual const char* TableName() { return tableDef ? tableDef->getName() : "<unknown>"; }
+ virtual int fieldCount() { return tableDef->fieldCount(); }
+ cDbFieldDef* getField(int f) { return tableDef->getField(f); }
+ cDbFieldDef* getField(const char* name) { return tableDef->getField(name); }
+
+ virtual int open(int allowAlter = 0); // 0 - off, 1 - on, 2 on with allow drop unused columns
+ virtual int close();
+ virtual int attach();
+ virtual int detach();
+ int isAttached() { return attached; }
+
+ virtual int find();
+ virtual void reset() { reset(stmtSelect); }
+
+ virtual int find(cDbStatement* stmt);
+ virtual int fetch(cDbStatement* stmt);
+ virtual void reset(cDbStatement* stmt);
+
+ virtual int insert(time_t inssp = 0);
+ virtual int update(time_t updsp = 0);
+ virtual int store();
+
+ virtual int __attribute__ ((format(printf, 2, 3))) deleteWhere(const char* where, ...);
+ virtual int countWhere(const char* where, int& count, const char* what = 0);
+ virtual int truncate();
+
+ // interface to cDbRow
+
+ void clear() { row->clear(); }
+ void clearChanged() { row->clearChanged(); }
+ int getChanges() { return row->getChanges(); }
+ std::string getChangedFields() { return row->getChangedFields(); }
+ void setValue(cDbFieldDef* f, const char* value, int size = 0) { row->setValue(f, value, size); }
+ void setValue(cDbFieldDef* f, int value) { row->setValue(f, value); }
+ void setValue(cDbFieldDef* f, long value) { row->setValue(f, value); }
+ void setValue(cDbFieldDef* f, double value) { row->setValue(f, value); }
+ void setBigintValue(cDbFieldDef* f, int64_t value) { row->setBigintValue(f, value); }
+ void setCharValue(cDbFieldDef* f, char value) { row->setCharValue(f, value); }
+
+ void setValue(const char* n, const char* value, int size = 0) { row->setValue(n, value, size); }
+ void setValue(const char* n, int value) { row->setValue(n, value); }
+ void setValue(const char* n, long value) { row->setValue(n, value); }
+ void setValue(const char* n, double value) { row->setValue(n, value); }
+ void setBigintValue(const char* n, int64_t value) { row->setBigintValue(n, value); }
+ void setCharValue(const char* n, char value) { row->setCharValue(n, value); }
+
+ void copyValues(cDbRow* r, int types = ftData);
+
+ int hasValue(cDbFieldDef* f, const char* value) { return row->hasValue(f, value); }
+ int hasCharValue(cDbFieldDef* f, char value) { return row->hasCharValue(f, value); }
+ int hasValue(cDbFieldDef* f, long value) { return row->hasValue(f, value); }
+ int hasValue(cDbFieldDef* f, double value) { return row->hasValue(f, value); }
+
+ int hasValue(const char* n, const char* value) { return row->hasValue(n, value); }
+ int hasCharValue(const char* n, char value) { return row->hasCharValue(n, value); }
+ int hasValue(const char* n, long value) { return row->hasValue(n, value); }
+ int hasValue(const char* n, double value) { return row->hasValue(n, value); }
+
+ const char* getStrValue(cDbFieldDef* f) const { return row->getStrValue(f); }
+ long getIntValue(cDbFieldDef* f) const { return row->getIntValue(f); }
+ int64_t getBigintValue(cDbFieldDef* f) const { return row->getBigintValue(f); }
+ float getFloatValue(cDbFieldDef* f) const { return row->getFloatValue(f); }
+ int isNull(cDbFieldDef* f) const { return row->isNull(f); }
+
+ const char* getStrValue(const char* n) const { return row->getStrValue(n); }
+ long getIntValue(const char* n) const { return row->getIntValue(n); }
+ int64_t getBigintValue(const char* n) const { return row->getBigintValue(n); }
+ float getFloatValue(const char* n) const { return row->getFloatValue(n); }
+ int isNull(const char* n) const { return row->isNull(n); }
+
+ cDbValue* getValue(cDbFieldDef* f) { return row->getValue(f); }
+ cDbValue* getValue(const char* fname) { return row->getValue(fname); }
+ int init(cDbValue*& dbvalue, const char* fname) { dbvalue = row->getValue(fname); return dbvalue ? success : fail; }
+ cDbRow* getRow() { return row; }
+
+ cDbTableDef* getTableDef() { return tableDef; }
+ cDbConnection* getConnection() { return connection; }
+ MYSQL* getMySql() { return connection->getMySql(); }
+ int isConnected() { return connection && connection->getMySql(); }
+
+ int getLastInsertId() { return lastInsertId; }
+
+ virtual int exist(const char* name = 0);
+ virtual int validateStructure(int allowAlter = 1); // 0 - off, 1 - on, 2 on with allow drop unused columns
+ virtual int createTable();
+ virtual int createIndices();
+
+ protected:
+
+ virtual int init(int allowAlter = 0); // 0 - off, 1 - on, 2 on with allow drop unused columns
+ virtual int checkIndex(const char* idxName, int& fieldCount);
+ virtual int alterModifyField(cDbFieldDef* def);
+ virtual int alterAddField(cDbFieldDef* def);
+ virtual int alterDropField(const char* name);
+
+ // data
+
+ cDbRow* row;
+ int holdInMemory; // hold table additionally in memory (not implemented yet)
+ int attached;
+ int lastInsertId;
+
+ cDbConnection* connection;
+ cDbTableDef* tableDef;
+
+ // basic statements
+
+ cDbStatement* stmtSelect;
+ cDbStatement* stmtInsert;
+ cDbStatement* stmtUpdate;
+};
+
+//***************************************************************************
+// cDbView
+//***************************************************************************
+
+class cDbView : public cDbService
+{
+ public:
+
+ cDbView(cDbConnection* c, const char* aName)
+ {
+ connection = c;
+ name = strdup(aName);
+ }
+
+ ~cDbView() { free(name); }
+
+ int exist()
+ {
+ if (connection->getMySql())
+ {
+ MYSQL_RES* result = mysql_list_tables(connection->getMySql(), name);
+ MYSQL_ROW tabRow = mysql_fetch_row(result);
+ mysql_free_result(result);
+
+ return tabRow ? yes : no;
+ }
+
+ return no;
+ }
+
+ int create(const char* path, const char* sqlFile)
+ {
+ int status;
+ char* file = 0;
+
+ asprintf(&file, "%s/%s", path, sqlFile);
+
+ tell(0, "Creating view '%s' using definition in '%s'",
+ name, file);
+
+ status = connection->executeSqlFile(file);
+
+ free(file);
+
+ return status;
+ }
+
+ int drop()
+ {
+ tell(0, "Drop view '%s'", name);
+
+ return connection->query("drop view %s", name);
+ }
+
+ protected:
+
+ cDbConnection* connection;
+ char* name;
+};
+
+//***************************************************************************
+// cDbProcedure
+//***************************************************************************
+
+class cDbProcedure : public cDbService
+{
+ public:
+
+ cDbProcedure(cDbConnection* c, const char* aName, ProcType pt = ptProcedure)
+ {
+ connection = c;
+ type = pt;
+ name = strdup(aName);
+ }
+
+ ~cDbProcedure() { free(name); }
+
+ const char* getName() { return name; }
+
+ int call(int ll = 1)
+ {
+ if (!connection || !connection->getMySql())
+ return fail;
+
+ cDbStatement stmt(connection);
+
+ tell(ll, "Calling '%s'", name);
+
+ stmt.build("call %s", name);
+
+ if (stmt.prepare() != success || stmt.execute() != success)
+ return fail;
+
+ tell(ll, "'%s' suceeded", name);
+
+ return success;
+ }
+
+ int created()
+ {
+ if (!connection || !connection->getMySql())
+ return fail;
+
+ cDbStatement stmt(connection);
+
+ stmt.build("show %s status where name = '%s'",
+ type == ptProcedure ? "procedure" : "function", name);
+
+ if (stmt.prepare() != success || stmt.execute() != success)
+ {
+ tell(0, "%s check of '%s' failed",
+ type == ptProcedure ? "Procedure" : "Function", name);
+ return no;
+ }
+ else
+ {
+ if (stmt.getResultCount() != 1)
+ return no;
+ }
+
+ return yes;
+ }
+
+ int create(const char* path)
+ {
+ int status;
+ char* file = 0;
+
+ asprintf(&file, "%s/%s.sql", path, name);
+
+ tell(1, "Creating %s '%s'",
+ type == ptProcedure ? "procedure" : "function", name);
+
+ status = connection->executeSqlFile(file);
+
+ free(file);
+
+ return status;
+ }
+
+ int drop()
+ {
+ tell(1, "Drop %s '%s'", type == ptProcedure ? "procedure" : "function", name);
+
+ return connection->query("drop %s %s", type == ptProcedure ? "procedure" : "function", name);
+ }
+
+ static int existOnFs(const char* path, const char* name)
+ {
+ int state;
+ char* file = 0;
+
+ asprintf(&file, "%s/%s.sql", path, name);
+ state = fileExists(file);
+
+ free(file);
+
+ return state;
+ }
+
+ protected:
+
+ cDbConnection* connection;
+ ProcType type;
+ char* name;
+
+};
+
+//***************************************************************************
+#endif //__DB_H
diff --git a/lib/dbdict.c b/lib/dbdict.c
new file mode 100644
index 0000000..1052a13
--- /dev/null
+++ b/lib/dbdict.c
@@ -0,0 +1,527 @@
+/*
+ * dbdict.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "errno.h"
+
+#include "common.h"
+#include "dbdict.h"
+
+//***************************************************************************
+// Get Token
+//***************************************************************************
+
+int getToken(const char*& p, char* token, int size, char delimiter = ' ', char end = ',')
+{
+ char* dest = token;
+ int num = 0;
+ int insideStr = no;
+
+ while (*p && (*p == ' ' || *p == delimiter))
+ p++;
+
+ if (*p == end || isEmpty(p))
+ return fail;
+
+ while (*p && num < size && !((*p == delimiter || *p == end) && !insideStr))
+ {
+ if (*p == '"')
+ {
+ insideStr = !insideStr;
+ p++;
+ }
+ else
+ {
+ *dest++ = *p++;
+ num++;
+ }
+ }
+
+ *dest = 0;
+
+ return success;
+}
+
+//***************************************************************************
+// cDbService
+//***************************************************************************
+
+const char* cDbService::formats[] =
+{
+ "INT",
+ "INT",
+ "BIGINT",
+ "BIGINT",
+ "VARCHAR",
+ "TEXT",
+ "MEDIUMTEXT",
+ "MEDIUMBLOB",
+ "FLOAT",
+ "DATETIME",
+
+ 0
+};
+
+const char* cDbService::toString(FieldFormat t)
+{
+ return formats[t];
+}
+
+const char* cDbService::dictFormats[] =
+{
+ "int",
+ "uint",
+ "bigint",
+ "ubigint",
+ "ascii",
+ "text",
+ "mtext",
+ "mlob",
+ "float",
+ "datetime",
+
+ 0
+};
+
+cDbService::FieldFormat cDbService::toDictFormat(const char* format)
+{
+ for (int i = 0; i < ffCount; i++)
+ if (strcasecmp(dictFormats[i], format) == 0)
+ return (FieldFormat)i;
+
+ return ffUnknown;
+}
+
+cDbService::TypeDef cDbService::types[] =
+{
+ { ftData, "data" },
+ { ftPrimary, "primary" },
+ { ftMeta, "meta" },
+ { ftAutoinc, "autoinc" },
+
+ { ftUnknown, "" }
+};
+
+int cDbService::toType(const char* theTypes)
+{
+ const int sizeTokenMax = 100;
+ char token[sizeTokenMax+TB];
+ const char* p = theTypes;
+
+ int type = ftNo;
+
+ // field can of more than one type -> bitmask
+
+ while (getToken(p, token, sizeTokenMax, '|') == success)
+ {
+ for (int i = 0; types[i].type != ftUnknown; i++)
+ {
+ if (strcasecmp(types[i].name, token) == 0)
+ {
+ type |= types[i].type;
+ continue;
+ }
+ }
+ }
+
+ return type;
+}
+
+const char* cDbService::toName(FieldType type, char* buf)
+{
+ *buf = 0;
+
+ // field can of more than one type -> bitmask !!
+
+ for (int i = 0; types[i].type != ftUnknown; i++)
+ {
+ if (types[i].type & type)
+ {
+ if (!isEmpty(buf))
+ strcat(buf, "|");
+
+ strcat(buf, types[i].name);
+ continue;
+ }
+ }
+
+ return buf;
+}
+
+//***************************************************************************
+//***************************************************************************
+// Class cDbDict
+//***************************************************************************
+
+cDbDict dbDict;
+
+//***************************************************************************
+// Object
+//***************************************************************************
+
+cDbDict::cDbDict()
+{
+ curTable = 0;
+ inside = no;
+ path = 0;
+ fieldFilter = 0; // 0 -> filter off use all fields
+ fltFromNameFct = 0;
+}
+
+cDbDict::~cDbDict()
+{
+ std::map<std::string, cDbTableDef*>::iterator t;
+
+ while ((t = tables.begin()) != tables.end())
+ {
+ if (t->second)
+ delete t->second;
+
+ tables.erase(t);
+ }
+
+ free(path);
+}
+
+//***************************************************************************
+// Get Table
+//***************************************************************************
+
+cDbTableDef* cDbDict::getTable(const char* aName)
+{
+ std::map<std::string, cDbTableDef*>::iterator t;
+
+ if ((t = tables.find(aName)) != tables.end())
+ return t->second;
+
+ return 0;
+}
+
+//***************************************************************************
+// Init
+//***************************************************************************
+
+int cDbDict::init(cDbFieldDef*& field, const char* tname, const char* fname)
+{
+ cDbTableDef* table = getTable(tname);
+
+ if (table)
+ if ((field = table->getField(fname)))
+ return success;
+
+ tell(0, "Fatal: Can't init field %s.%s, not found in dictionary", tname, fname);
+
+ return fail;
+}
+
+//***************************************************************************
+// In
+//***************************************************************************
+
+int cDbDict::in(const char* file, int filter)
+{
+ FILE* f;
+ char* line = 0;
+ size_t size = 0;
+
+ if (isEmpty(file))
+ return fail;
+
+ fieldFilter = filter;
+ asprintf(&path, "%s", file);
+
+ f = fopen(path, "r");
+
+ if (!f)
+ {
+ tell(0, "Error: Can' open file '%s', error was '%s'", path, strerror(errno));
+ return fail;
+ }
+
+ while (getline(&line, &size, f) > 0)
+ {
+ char* p = strstr(line, "//");
+
+ if (p) *p = 0;
+
+ allTrim(line);
+
+ if (isEmpty(line))
+ continue;
+
+ if (atLine(line) != success)
+ {
+ tell(0, "Error: Found unexpected definition '%s', aborting", line);
+ free(path);
+ return fail;
+ }
+ }
+
+ fclose(f);
+ free(line);
+
+ return success;
+}
+
+//***************************************************************************
+// Forget
+//***************************************************************************
+
+void cDbDict::forget()
+{
+ std::map<std::string, cDbTableDef*>::iterator t;
+
+ while ((t = tables.begin()) != tables.end())
+ {
+ if (t->second)
+ delete t->second;
+
+ tables.erase(t);
+ }
+
+ free(path);
+
+ curTable = 0;
+ inside = no;
+ path = 0;
+ fieldFilter = 0; // 0 -> filter off use all fields
+ fltFromNameFct = 0;
+}
+
+//***************************************************************************
+// Show
+//***************************************************************************
+
+void cDbDict::show()
+{
+ std::map<std::string, cDbTableDef*>::iterator t;
+
+ for (t = tables.begin(); t != tables.end(); t++)
+ {
+ tell(0, "-------------------------------------------------------------------------------------------------");
+ tell(0, "Table '%s'", t->first.c_str());
+ tell(0, "-------------------------------------------------------------------------------------------------");
+ t->second->show();
+ }
+}
+
+//***************************************************************************
+// At Line
+//***************************************************************************
+
+int cDbDict::atLine(const char* line)
+{
+ int status = success;
+ static int prsTable = no;
+ static int prsIndex = no;
+
+ const char* p;
+
+ if (strncasecmp(line, "Table ", 6) == 0)
+ {
+ char tableName[100];
+
+ prsTable = yes;
+ curTable = 0;
+ p = line + strlen("Table ");
+ strcpy(tableName, p);
+ allTrim(tableName);
+
+ if (!getTable(tableName))
+ {
+ curTable = new cDbTableDef(tableName);
+ tables[tableName] = curTable;
+ }
+ else
+ tell(0, "Fatal: Table '%s' doubly defined", tableName);
+ }
+ else if (strncasecmp(line, "Index ", 6) == 0)
+ {
+ prsIndex = yes;
+ p = line + strlen("Table ");
+ }
+
+ else if (strchr(line, '{'))
+ inside = yes;
+
+ else if (strchr(line, '}'))
+ {
+ inside = no;
+ prsTable = no;
+ prsIndex = no;
+ }
+
+ else if (inside && prsTable)
+ status += parseField(line);
+
+ else if (inside && prsIndex)
+ status += parseIndex(line);
+
+ else
+ tell(0, "Info: Ignoring extra line [%s]", line);
+
+ return status;
+}
+
+//***************************************************************************
+// Parse Filter
+//***************************************************************************
+
+int cDbDict::parseFilter(cDbFieldDef* f, const char* value)
+{
+ const int sizeNameMax = 50;
+ char name[sizeNameMax+TB];
+ const char* v = value;
+
+ f->filter = 0;
+
+ while (getToken(v, name, sizeNameMax, '|') == success)
+ {
+ if (isalpha(*name) && fltFromNameFct)
+ f->filter |= fltFromNameFct(name);
+ else
+ f->filter |= atoi(name);
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Parse Field
+//***************************************************************************
+
+int cDbDict::parseField(const char* line)
+{
+ const int sizeTokenMax = 100;
+ char token[sizeTokenMax+TB];
+ const char* p = line;
+
+ cDbFieldDef* f = new cDbFieldDef;
+ f->filter = 0xFFFF;
+
+ if (!curTable)
+ return fail;
+
+ // first parse fixed part of field definition up to the 'type'
+
+ for (int i = 0; i < dtCount; i++)
+ {
+ if (getToken(p, token, sizeTokenMax) != success)
+ {
+ if (i >= dtType) // all after type is optional (filter, ...)
+ break;
+
+ delete f;
+ tell(0, "Error: Can't parse line [%s]", line);
+ return fail;
+ }
+
+ if (i == dtCount-1 && strchr(token, ','))
+ *(strchr(token, ',')) = 0;
+
+ switch (i)
+ {
+ case dtName: f->name = strdup(token); break;
+ case dtDescription: f->setDescription(token); break;
+ case dtDbName: f->dbname = strdup(token); break;
+ case dtFormat: f->format = toDictFormat(token); break;
+ case dtSize: f->size = atoi(token); break;
+ case dtType: f->type = toType(token); break;
+ }
+ }
+
+ // second ... parse dynamic part of the field definition like filter and default
+
+ while (getToken(p, token, sizeTokenMax) == success)
+ {
+ char content[sizeTokenMax+TB];
+
+ if (getToken(p, content, sizeTokenMax) != success || isEmpty(content))
+ {
+ tell(0, "Error: Skipping token '%s' missing content!", token);
+ break;
+ }
+
+ if (strcasecmp(token, "filter") == 0)
+ parseFilter(f, content);
+
+ else if (strcasecmp(token, "default") == 0)
+ f->def = strdup(content);
+
+ else
+ tell(0, "Warning: Skipping unexpected token '%s'", token);
+ }
+
+ if (!f->isValid())
+ {
+ tell(0, "Error: Can't parse line [%s], invalid field configuration", line);
+ delete f;
+ return fail;
+ }
+
+ if (f->filterMatch(fieldFilter))
+ {
+ f->index = curTable->fieldCount();
+ curTable->addField(f);
+ }
+ else
+ {
+ tell(2, "Info: Ignoring field '%s' due to filter configiuration",
+ f->getName());
+ delete f;
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Parse Index
+//***************************************************************************
+
+int cDbDict::parseIndex(const char* line)
+{
+ const int sizeTokenMax = 100;
+ char token[sizeTokenMax+TB];
+ const char* p = line;
+ int done = no;
+
+ if (!curTable)
+ return fail;
+
+ cDbIndexDef* index = new cDbIndexDef();
+
+ for (int i = 0; !done && i < 20; i++)
+ {
+ if (getToken(p, token, sizeTokenMax) != success)
+ {
+ if (i <= idtFields)
+ {
+ delete index;
+ tell(0, "Error: Can't parse line [%s]", line);
+ return fail;
+ }
+
+ break;
+ }
+
+ if (strchr(token, ','))
+ {
+ done = yes;
+ *(strchr(token, ',')) = 0;
+ }
+
+ if (i == idtName)
+ index->setName(token);
+ else if (i == idtDescription)
+ index->setDescription(token);
+ else if (i >= idtFields && i < idtFields+20)
+ index->addField(curTable->getField(token));
+ }
+
+ curTable->addIndex(index);
+
+ return success;
+}
diff --git a/lib/dbdict.h b/lib/dbdict.h
new file mode 100644
index 0000000..2999390
--- /dev/null
+++ b/lib/dbdict.h
@@ -0,0 +1,471 @@
+/*
+ * dbdict.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __DBDICT_H
+#define __DBDICT_H
+
+#include <stdio.h>
+
+#include <vector>
+#include <map>
+#include <string>
+
+class cDbDict;
+
+typedef int (*FilterFromName)(const char* name);
+
+//***************************************************************************
+// _casecmp_
+//***************************************************************************
+
+class _casecmp_
+{
+ public:
+
+ bool operator() (const std::string& a, const std::string& b) const
+ {
+ return strcasecmp(a.c_str(), b.c_str()) < 0;
+ }
+};
+
+//***************************************************************************
+// cDbService
+//***************************************************************************
+
+class cDbService
+{
+ public:
+
+ enum Misc
+ {
+ maxIndexFields = 20
+ };
+
+ enum FieldFormat
+ {
+ ffUnknown = na,
+
+ ffInt,
+ ffUInt,
+ ffBigInt,
+ ffUBigInt,
+ ffAscii, // -> VARCHAR
+ ffText,
+ ffMText,
+ ffMlob, // -> MEDIUMBLOB
+ ffFloat,
+ ffDateTime,
+
+ ffCount
+ };
+
+ enum FieldType
+ {
+ ftUnknown = na,
+ ftNo = 0,
+
+ ftData = 1,
+ ftPrimary = 2,
+ ftMeta = 4,
+ ftAutoinc = 8,
+
+ ftAll = ftData | ftPrimary | ftMeta
+ };
+
+ enum BindType
+ {
+ bndIn = 0x001,
+ bndOut = 0x002,
+ bndSet = 0x004
+ };
+
+ enum ProcType
+ {
+ ptProcedure,
+ ptFunction
+ };
+
+ struct TypeDef
+ {
+ FieldType type;
+ const char* name;
+ };
+
+ static const char* toString(FieldFormat t);
+ static FieldFormat toDictFormat(const char* format);
+ static const char* formats[];
+ static const char* dictFormats[];
+
+ static int toType(const char* type);
+ static const char* toName(FieldType type, char* buf);
+ static TypeDef types[];
+};
+
+typedef cDbService cDBS;
+
+//***************************************************************************
+// cDbFieldDef
+//***************************************************************************
+
+class cDbFieldDef : public cDbService
+{
+ public:
+
+ friend class cDbDict;
+
+ cDbFieldDef()
+ {
+ name = 0;
+ dbname = 0,
+ format = ffUnknown;
+ size = na;
+ type = ftUnknown;
+ description = 0;
+ dbdescription = 0;
+ filter = 0xFFFF;
+ def = 0;
+ }
+
+ cDbFieldDef(const char* n, const char* dn, FieldFormat f, int s, int t, int flt = 0xFFFF)
+ {
+ name = strdup(n);
+ dbname = strdup(dn);
+ format = f;
+ size = s;
+ type = t;
+ filter = flt;
+ description = 0;
+ dbdescription = 0;
+ def = 0;
+ }
+
+ ~cDbFieldDef() { free(name); free(dbname); free(description); free(dbdescription); free(def); }
+
+ int getIndex() { return index; }
+ const char* getName() { return name; }
+ int hasName(const char* n) { return strcasecmp(n, name) == 0; }
+ int hasDbName(const char* n) { return strcasecmp(n, dbname) == 0; }
+ const char* getDescription() { return description; }
+ const char* getDbDescription() { return dbdescription; }
+ const char* getDbName() { return dbname; }
+ int getSize() { return size; }
+ FieldFormat getFormat() { return format; }
+ int getType() { return type; }
+ const char* getDefault() { return def ? def : ""; }
+ int getFilter() { return filter; }
+ int filterMatch(int f) { return !f || filter & f; }
+ int hasType(int types) { return types & type; }
+ int hasFormat(int f) { return format == f; }
+
+ int isString() { return format == ffAscii || format == ffText ||
+ format == ffMText || format == ffMlob; }
+ int isInt() { return format == ffInt || format == ffUInt; }
+ int isBigInt() { return format == ffBigInt || format == ffUBigInt; }
+ int isFloat() { return format == ffFloat; }
+ int isDateTime() { return format == ffDateTime; }
+
+ void setDescription(const char* desc)
+ {
+ description = strdup(desc);
+ dbdescription = strdup(strReplace("'", "\\'", description).c_str());
+ }
+
+ const char* toColumnFormat(char* buf) // column type to be used for create/alter
+ {
+ if (!buf)
+ return 0;
+
+ sprintf(buf, "%s", toString(format));
+
+ if (format != ffMlob)
+ {
+ if (!size)
+ {
+ if (format == ffAscii)
+ size = 100;
+ else if (format == ffInt || format == ffUInt || format == ffBigInt || format == ffUBigInt)
+ size = 11;
+ else if (format == ffFloat)
+ size = 10;
+ }
+
+ if (format == ffFloat)
+ sprintf(eos(buf), "(%d,%d)", size/10 + size%10, size%10); // 62 -> 8,2
+ else if (format == ffInt || format == ffUInt || format == ffBigInt || format == ffUBigInt || format == ffAscii)
+ sprintf(eos(buf), "(%d)", size);
+
+ if (format == ffUInt || format == ffUBigInt)
+ sprintf(eos(buf), " unsigned");
+ }
+
+ return buf;
+ }
+
+ int isValid()
+ {
+ if (!name) { tell(0, "Missing field name"); return no; }
+ if (!dbname) { tell(0, "Missing fields database name"); return no; }
+ if (size == na) { tell(0, "Missing field size"); return no; }
+ if (type == ftUnknown) { tell(0, "Missing or invalid field type"); return no; }
+ if (format == ffUnknown) { tell(0, "Missing or invalid field format"); return no; }
+
+ return yes;
+ }
+
+ void show()
+ {
+ char colFmt[100];
+ char fType[100];
+ char tmp[100];
+
+ sprintf(fType, "(%s)", toName((FieldType)type, tmp));
+
+ tell(0, "%-20s %-25s %-17s %-20s (0x%04X) default '%s' '%s'", name, dbname,
+ toColumnFormat(colFmt), fType, filter, notNull(def, ""), description);
+ }
+
+ protected:
+
+ char* name;
+ char* dbname;
+ char* description;
+ char* dbdescription;
+ FieldFormat format;
+ int size;
+ int index;
+ int type;
+ int filter; // bitmask (defaults to 0xFFFF)
+ char* def;
+};
+
+//***************************************************************************
+// cDbIndexDef
+//***************************************************************************
+
+class cDbIndexDef
+{
+ public:
+
+ cDbIndexDef() { name = 0; description = 0; }
+ ~cDbIndexDef() { free(name); free(description); }
+
+ void setName(const char* n) { free(name); name = strdup(n); }
+ const char* getName() { return name; }
+
+ void setDescription(const char* d) { free(description); description = strdup(d); }
+ const char* getDescription() { return description; }
+
+ int fieldCount() { return dfields.size(); }
+ void addField(cDbFieldDef* f) { dfields.push_back(f); }
+ cDbFieldDef* getField(int i) { return dfields[i]; }
+
+ void show()
+ {
+ std::string s = "";
+
+ for (uint i = 0; i < dfields.size(); i++)
+ s += dfields[i]->getName() + std::string(" ");
+
+ s.erase(s.find_last_not_of(' ')+1);
+
+ tell(0, "Index %-25s (%s)", getName(), s.c_str());
+ }
+
+ protected:
+
+ char* name;
+ char* description;
+ std::vector<cDbFieldDef*> dfields; // index fields
+};
+
+//***************************************************************************
+// cDbTableDef
+//***************************************************************************
+
+class cDbTableDef : public cDbService
+{
+ public:
+
+ friend class cDbRow;
+ friend class cDbTable;
+ friend class cDbStatement;
+
+ cDbTableDef(const char* n) { name = strdup(n); }
+
+ ~cDbTableDef()
+ {
+ for (uint i = 0; i < indices.size(); i++)
+ delete indices[i];
+
+ indices.clear();
+
+ free(name);
+ clear();
+ }
+
+ const char* getName() { return name; }
+ int fieldCount() { return dfields.size(); }
+ cDbFieldDef* getField(int id) { return _dfields[id]; }
+
+ cDbFieldDef* getField(const char* fname, int silent = no)
+ {
+ std::map<std::string, cDbFieldDef*, _casecmp_>::iterator f;
+
+ if ((f = dfields.find(fname)) != dfields.end())
+ return f->second;
+
+ if (!silent)
+ tell(0, "Fatal: Missing definition of field '%s.%s' in dictionary!", name, fname);
+
+ return 0;
+ }
+
+ cDbFieldDef* getFieldByDbName(const char* dbname)
+ {
+ std::map<std::string, cDbFieldDef*, _casecmp_>::iterator it;
+
+ for (it = dfields.begin(); it != dfields.end(); it++)
+ {
+ if (it->second->hasDbName(dbname))
+ return it->second;
+ }
+
+ tell(5, "Fatal: Missing definition of field '%s.%s' in dictionary!", name, dbname);
+
+ return 0;
+ }
+
+ void addField(cDbFieldDef* f)
+ {
+ if (dfields.find(f->getName()) == dfields.end())
+ {
+ dfields[f->getName()] = f; // add to named map
+ _dfields.push_back(f); // add to indexed list
+ }
+ else
+ tell(0, "Fatal: Field '%s.%s' doubly defined", getName(), f->getName());
+ }
+
+ int indexCount() { return indices.size(); }
+ cDbIndexDef* getIndex(int i) { return indices[i]; }
+ void addIndex(cDbIndexDef* i) { indices.push_back(i); }
+
+ void clear()
+ {
+ std::map<std::string, cDbFieldDef*>::iterator f;
+
+ while ((f = dfields.begin()) != dfields.end())
+ {
+ if (f->second)
+ delete f->second;
+
+ dfields.erase(f);
+ }
+ }
+
+ void show()
+ {
+ // show fields
+
+ for (uint i = 0; i < _dfields.size(); i++)
+ _dfields[i]->show();
+
+ // show indices
+
+ if (!indices.size())
+ {
+ tell(0, " ");
+ return;
+ }
+
+ tell(0, "-----------------------------------------------------");
+ tell(0, "Indices from '%s'", getName());
+ tell(0, "-----------------------------------------------------");
+
+ for (uint i = 0; i < indices.size(); i++)
+ indices[i]->show();
+
+ tell(0, " ");
+ }
+
+ private:
+
+ char* name;
+ std::vector<cDbIndexDef*> indices;
+
+ // FiledDefs stored as list to have access via index
+ std::vector<cDbFieldDef*> _dfields;
+
+ // same FiledDef references stored as a map to have access via name
+ std::map<std::string, cDbFieldDef*, _casecmp_> dfields;
+};
+
+//***************************************************************************
+// cDbDict
+//***************************************************************************
+
+class cDbDict : public cDbService
+{
+ public:
+
+ // declarations
+
+ enum TableDictToken
+ {
+ dtName,
+ dtDescription,
+ dtDbName,
+ dtFormat,
+ dtSize,
+ dtType,
+
+ dtCount
+ };
+
+ enum IndexDictToken
+ {
+ idtName,
+ idtDescription,
+ idtFields
+ };
+
+ cDbDict();
+ virtual ~cDbDict();
+
+ int in(const char* file, int filter = 0);
+ void setFilterFromNameFct(FilterFromName fct) { fltFromNameFct = fct; }
+
+ cDbTableDef* getTable(const char* name);
+ void show();
+ int init(cDbFieldDef*& field, const char* tname, const char* fname);
+ const char* getPath() { return path ? path : ""; }
+ void forget();
+
+ std::map<std::string, cDbTableDef*>::iterator getFirstTableIterator() { return tables.begin(); }
+ std::map<std::string, cDbTableDef*>::iterator getTableEndIterator() { return tables.end(); }
+
+ protected:
+
+ int atLine(const char* line);
+ int parseField(const char* line);
+ int parseIndex(const char* line);
+ int parseFilter(cDbFieldDef* f, const char* value);
+
+ // data
+
+ int inside;
+ cDbTableDef* curTable;
+ std::map<std::string, cDbTableDef*, _casecmp_> tables;
+ char* path;
+ int fieldFilter;
+ FilterFromName fltFromNameFct;
+};
+
+extern cDbDict dbDict;
+
+//***************************************************************************
+#endif // __DBDICT_H
diff --git a/lib/demo.c b/lib/demo.c
new file mode 100644
index 0000000..0c4c234
--- /dev/null
+++ b/lib/demo.c
@@ -0,0 +1,531 @@
+/*
+ * demo.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "config.h"
+#include "common.h"
+
+#include "db.h"
+#include "epgservice.h"
+
+cDbConnection* connection = 0;
+const char* logPrefix = "";
+
+//***************************************************************************
+// Init Connection
+//***************************************************************************
+
+void initConnection()
+{
+ cDbConnection::init();
+
+ cDbConnection::setHost("localhost");
+ // cDbConnection::setHost("192.168.200.101");
+ cDbConnection::setPort(3306);
+ cDbConnection::setName("epg2vdr");
+ cDbConnection::setUser("epg2vdr");
+ cDbConnection::setPass("epg");
+
+ cDbConnection::setConfPath("/etc/epgd/");
+ cDbConnection::setEncoding("utf8");
+
+ connection = new cDbConnection();
+}
+
+void exitConnection()
+{
+ cDbConnection::exit();
+
+ if (connection)
+ delete connection;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+int demoStatement()
+{
+ int status = success;
+
+ cDbTable* eventsDb = new cDbTable(connection, "events");
+
+ tell(0, "------------------- attach table ---------------");
+
+ // open table (attach)
+
+ if (eventsDb->open() != success)
+ return fail;
+
+ tell(0, "---------------- prepare select statement -------------");
+
+ // vorbereiten (prepare) eines statement, am besten einmal bei programmstart!
+ // ----------
+ // select eventid, compshorttext, episodepart, episodelang
+ // from events
+ // where eventid > ?
+
+ cDbStatement* selectByCompTitle = new cDbStatement(eventsDb);
+
+ status += selectByCompTitle->build("select ");
+ status += selectByCompTitle->bind("EventId", cDBS::bndOut);
+ status += selectByCompTitle->bind("ChannelId", cDBS::bndOut, ", ");
+ status += selectByCompTitle->bind("Title", cDBS::bndOut, ", ");
+ status += selectByCompTitle->build(" from %s where ", eventsDb->TableName());
+ status += selectByCompTitle->bindCmp(0, eventsDb->getField("EventId"), 0, ">");
+
+ status += selectByCompTitle->prepare(); // prepare statement
+
+ if (status != success)
+ {
+ // prepare sollte MySQL fehler ausgegeben haben!
+
+ delete eventsDb;
+ delete selectByCompTitle;
+
+ return fail;
+ }
+
+ tell(0, "------------------ prepare done ----------------------");
+
+ tell(0, "------------------ create some rows ----------------------");
+
+ eventsDb->clear(); // alle values löschen
+
+ for (int i = 0; i < 10; i++)
+ {
+ char* title;
+ asprintf(&title, "title %d", i);
+
+ eventsDb->setValue(eventsDb->getField("EventId"), 800 + i * 100);
+ eventsDb->setValue(eventsDb->getField("ChannelId"), "xxx-yyyy-zzz");
+ eventsDb->setValue(eventsDb->getField("Title"), title);
+
+ eventsDb->store(); // store -> select mit anschl. update oder insert je nachdem ob dier PKey bereits vorhanden ist
+ // eventsDb->insert(); // sofern man schon weiß das es ein insert ist
+ // eventsDb->update(); // sofern man schon weiß das der Datensatz vorhanden ist
+
+ free(title);
+ }
+
+ tell(0, "------------------ done ----------------------");
+
+ tell(0, "-------- select all where eventid > 1000 -------------");
+
+ eventsDb->clear(); // alle values löschen
+ eventsDb->setValue(eventsDb->getField("EventId"), 1000);
+
+ for (int f = selectByCompTitle->find(); f; f = selectByCompTitle->fetch())
+ {
+ tell(0, "id: %ld", eventsDb->getIntValue(eventsDb->getField("EventId")));
+ tell(0, "channel: %s", eventsDb->getStrValue(eventsDb->getField("ChannelId")));
+ tell(0, "titel: %s", eventsDb->getStrValue(eventsDb->getField("Title")));
+ }
+
+ // freigeben der Ergebnissmenge !!
+
+ selectByCompTitle->freeResult();
+
+ // folgendes am programmende
+
+ delete eventsDb; // implizietes close (detach)
+ delete selectByCompTitle; // statement freigeben (auch gegen die DB)
+
+ return success;
+}
+
+//***************************************************************************
+// Join
+//***************************************************************************
+
+// #define F_INIT(a,b) cDbFieldDef* a##b = 0; dbDict.init(a##b, #a, #b)
+
+int joinDemo()
+{
+ int status = success;
+
+ // grundsätzlich genügt hier auch eine Tabelle, für die anderen sind cDbValue Instanzen außreichend
+ // so ist es etwas einfacher die cDbValues zu initialisieren.
+ // Ich habe statische "virtual FieldDef* getFieldDef(int f)" Methode in der Tabellenklassen geplant
+ // um ohne Instanz der cTable ein Feld einfach initialisieren zu können
+
+ cDbTable* eventsDb = new cDbTable(connection, "events");
+ cDbTable* imageRefDb = new cDbTable(connection, "imagerefs");
+ cDbTable* imageDb = new cDbTable(connection, "images");
+
+ cDbTable* timerDb = new cDbTable(connection, "timers");
+ timerDb->open(yes);
+ delete timerDb;
+
+ // init dict fields as needed (normaly done once at programm start)
+ // init and using the pointer improve the speed since the lookup via
+ // the name is dine only once
+
+ // F_INIT(events, EventId); // ergibt: cDbFieldDef* eventsEventId; dbDict.init(eventsEventId, "events", "EventId");
+
+ tell(0, "----------------- open table connection ---------------");
+
+ // open tables (attach)
+
+ if (eventsDb->open() != success)
+ return fail;
+
+ if (imageDb->open() != success)
+ return fail;
+
+ if (imageRefDb->open() != success)
+ return fail;
+
+ tell(0, "---------------- prepare select statement -------------");
+
+ // all images
+
+ cDbStatement* selectAllImages = new cDbStatement(imageRefDb);
+
+ // prepare fields
+
+ cDbValue imageUpdSp;
+ cDbValue imageSize;
+ cDbValue masterId;
+
+ cDbFieldDef imageSizeDef("image", "image", cDBS::ffUInt, 999, cDBS::ftData, 0); // eine Art ein Feld zu erzeugen
+ imageSize.setField(&imageSizeDef);
+ imageUpdSp.setField(imageDb->getField("UpdSp"));
+ masterId.setField(eventsDb->getField("MasterId"));
+
+ // select e.masterid, r.imagename, r.eventid, r.lfn, length(i.image)
+ // from imagerefs r, images i, events e
+ // where i.imagename = r.imagename
+ // and e.eventid = r.eventid
+ // and (i.updsp > ? or r.updsp > ?)
+
+ selectAllImages->build("select ");
+ selectAllImages->setBindPrefix("e.");
+ selectAllImages->bind(&masterId, cDBS::bndOut);
+ selectAllImages->setBindPrefix("r.");
+ selectAllImages->bind("ImgName", cDBS::bndOut, ", ");
+ selectAllImages->bind("EventId", cDBS::bndOut, ", ");
+ selectAllImages->bind("Lfn", cDBS::bndOut, ", ");
+ selectAllImages->setBindPrefix("i.");
+ selectAllImages->build(", length(");
+ selectAllImages->bind(&imageSize, cDBS::bndOut);
+ selectAllImages->build(")");
+ selectAllImages->clrBindPrefix();
+ selectAllImages->build(" from %s r, %s i, %s e where ",
+ imageRefDb->TableName(), imageDb->TableName(), eventsDb->TableName());
+ selectAllImages->build("e.%s = r.%s and i.%s = r.%s and (",
+ "EventId", // eventsEventId->getDbName(),
+ imageRefDb->getField("EventId")->getDbName(),
+ "imagename", // direkt den DB Feldnamen verwenden -> nicht so schön da nicht dynamisch
+ imageRefDb->getField("ImgName")->getDbName()); // ordentlich via dictionary übersetzt -> schön ;)
+ selectAllImages->bindCmp("i", &imageUpdSp, ">");
+ selectAllImages->build(" or ");
+ selectAllImages->bindCmp("r", imageRefDb->getField("UpdSp"), 0, ">");
+ selectAllImages->build(")");
+
+ status += selectAllImages->prepare();
+
+ if (status != success)
+ {
+ // prepare sollte MySQL Fehler ausgegeben haben!
+
+ delete eventsDb;
+ delete imageDb;
+ delete imageRefDb;
+ delete selectAllImages;
+
+ return fail;
+ }
+
+ tell(0, "------------------ prepare done ----------------------");
+
+
+ tell(0, "------------------ select ----------------------");
+
+ time_t since = 0; //time(0) - 60 * 60;
+ imageRefDb->clear();
+ imageRefDb->setValue(imageRefDb->getField("UpdSp"), since);
+ imageUpdSp.setValue(since);
+
+ for (int res = selectAllImages->find(); res; res = selectAllImages->fetch())
+ {
+ // so kommst du an die Werte der unterschiedlichen Tabellen
+
+ int eventid = masterId.getIntValue();
+ const char* imageName = imageRefDb->getStrValue("ImgName");
+ int lfn = imageRefDb->getIntValue(imageRefDb->getField("Lfn"));
+ int size = imageSize.getIntValue();
+
+ tell(0, "Found (%d) '%s', %d, %d", eventid, imageName, lfn, size);
+ }
+
+ tell(0, "------------------ done ----------------------");
+
+ // freigeben der Ergebnissmenge !!
+
+ selectAllImages->freeResult();
+
+
+ // folgendes am programmende
+
+ delete eventsDb; // implizites close (detach)
+ delete imageDb;
+ delete imageRefDb;
+ delete selectAllImages; // statement freigeben (auch gegen die DB)
+
+ return success;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+int insertDemo()
+{
+ cDbTable* timerDb = new cDbTable(connection, "timers");
+ if (timerDb->open() != success) return fail;
+
+ timerDb->clear();
+ timerDb->setValue("EventId", 4444444);
+
+ if (timerDb->insert() == success)
+ {
+ tell(0, "Insert succeeded with ID %d", timerDb->getLastInsertId());
+ }
+
+ delete timerDb;
+
+ return done;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+int findUseEvent()
+{
+ cDbTable* useeventsDb = new cDbTable(connection, "useevents");
+ if (useeventsDb->open() != success) return fail;
+
+ // select event by useid
+
+ tell(0, "========================================");
+
+ cDbStatement* selectEventById = new cDbStatement(useeventsDb);
+
+ // select * from eventsview
+ // where useid = ?
+ // and updflg in (.....)
+
+ selectEventById->build("select ");
+ selectEventById->bindAllOut();
+ selectEventById->build(" from %s where ", useeventsDb->TableName());
+ selectEventById->bind("USEID", cDBS::bndIn | cDBS::bndSet);
+ selectEventById->build(" and %s in (%s)",
+ useeventsDb->getField("UPDFLG")->getDbName(),
+ Us::getNeeded());
+
+ selectEventById->prepare();
+
+ tell(0, "========================================");
+
+ useeventsDb->clear();
+ useeventsDb->setValue("USEID", 1146680);
+
+ const char* flds[] =
+ {
+ "ACTOR",
+ "AUDIO",
+ "CATEGORY",
+ "COUNTRY",
+ "DIRECTOR",
+ "FLAGS",
+ "GENRE",
+ "INFO",
+ "MUSIC",
+ "PRODUCER",
+ "SCREENPLAY",
+ "SHORTREVIEW",
+ "TIPP",
+ "TOPIC",
+ "YEAR",
+ "RATING",
+ "NUMRATING",
+ "MODERATOR",
+ "OTHER",
+ "GUEST",
+ "CAMERA",
+
+ 0
+ };
+
+ if (selectEventById->find())
+ {
+ FILE* f;
+ char* fileName = 0;
+
+ asprintf(&fileName, "%s/info.epg2vdr", ".");
+
+ if (!(f = fopen(fileName, "w")))
+ {
+ selectEventById->freeResult();
+ tell(0, "Error opening file '%s' failed, %s", fileName, strerror(errno));
+ free(fileName);
+
+ return fail;
+ }
+
+ tell(0, "Storing event details to '%s'", fileName);
+
+ for (int i = 0; flds[i]; i++)
+ {
+ cDbFieldDef* field = useeventsDb->getField(flds[i]);
+
+ if (!field)
+ {
+ tell(0, "Warning: Field '%s' not found at table '%s'", flds[i], useeventsDb->TableName());
+ continue;
+ }
+
+ if (field->getFormat() == cDbService::ffAscii ||
+ field->getFormat() == cDbService::ffText ||
+ field->getFormat() == cDbService::ffMText)
+ {
+ fprintf(f, "%s: %s\n", flds[i], useeventsDb->getStrValue(flds[i]));
+ }
+ else
+ {
+ fprintf(f, "%s: %ld\n", flds[i], useeventsDb->getIntValue(flds[i]));
+ }
+ }
+
+ selectEventById->freeResult();
+ free(fileName);
+ fclose(f);
+ }
+
+ tell(0, "========================================");
+
+ selectEventById->freeResult();
+
+ return done;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+int updateRecordingDirectory()
+{
+ int ins = 0;
+
+ cDbTable* recordingDirDb = new cDbTable(connection, "recordingdirs");
+ if (recordingDirDb->open() != success) return fail;
+
+ // char* dir = strdup("more~Marvel's Agents of S.H.I.E.L.D.~xxx.ts");
+ char* dir = strdup("aaaaa~bbbbbb~ccccc.ts");
+ char* pos = strrchr(dir, '~');
+
+ if (pos)
+ {
+ *pos = 0;
+
+ for (int level = 0; level < 3; level++)
+ {
+ recordingDirDb->clear();
+
+ recordingDirDb->setValue("VDRUUID", "foobar");
+ recordingDirDb->setValue("DIRECTORY", dir);
+
+ if (!recordingDirDb->find())
+ {
+ ins++;
+ recordingDirDb->store();
+ }
+
+ recordingDirDb->reset();
+
+ char* pos = strrchr(dir, '~');
+ if (pos) *pos=0;
+ }
+ }
+
+ tell(0, "inserted %d directories", ins);
+
+ delete recordingDirDb;
+ free(dir);
+
+ return ins;
+}
+
+//***************************************************************************
+// Main
+//***************************************************************************
+
+int main(int argc, char** argv)
+{
+ cEpgConfig::logstdout = yes;
+ cEpgConfig::loglevel = 2;
+
+ const char* path = "/etc/epgd/epg.dat";
+
+ if (argc > 1)
+ path = argv[1];
+
+ // read deictionary
+
+ dbDict.setFilterFromNameFct(toFieldFilter);
+
+ if (dbDict.in(path, ffEpgd) != success)
+ {
+ tell(0, "Invalid dictionary configuration, aborting!");
+ return 1;
+ }
+
+ cUserTimes userTimes;
+
+ tell(0, "--------------");
+ //userTimes.clear();
+ tell(0, "--------------");
+
+ userTimes.add("@Now", "What's on now?") ;
+ userTimes.add("@Next", "What's on next?" );
+ userTimes.add("!20:15", "prime time");
+ userTimes.add("22:20", "late night");
+ userTimes.add("00:00");
+
+ tell(0, "--------------");
+
+ for (int i = 0; i < 6; i++)
+ {
+ cUserTimes::UserTime* t = userTimes.next();
+
+ tell(0, "%d - %s (%s) %s", t->getHHMM(), t->getTitle(), t->getHHMMStr(), t->isHighlighted() ? "highlighted" : "");
+ }
+
+ return 0;
+
+ // dbDict.show();
+
+ initConnection();
+
+ // demoStatement();
+ // joinDemo();
+ // insertDemo();
+
+ tell(0, "uuid: '%s'", getUniqueId());
+
+ tell(0, "- - - - - - - - - - - - - - - - - ");
+
+ //updateRecordingDirectory();
+ findUseEvent();
+
+ tell(0, "- - - - - - - - - - - - - - - - - ");
+
+ exitConnection();
+
+ return 0;
+}
diff --git a/lib/demo.dat b/lib/demo.dat
new file mode 100644
index 0000000..5ae9123
--- /dev/null
+++ b/lib/demo.dat
@@ -0,0 +1,17 @@
+
+
+Table _test
+{
+ ID "" id UInt 0 Primary|Autoinc,
+ VDRUUID "" vdruuid Ascii 40 Primary,
+
+ INSSP "" inssp Int 0 Meta,
+ UPDSP "" updsp Int 0 Meta,
+
+ EVENTID "" eventid UInt 0 Data,
+ CHANNELID "" channelid Ascii 50 Data default none,
+
+ SOURCE "" source Ascii 20 Data default WEB filter osd|webif|10,
+ STATE "'D'eleted, 'R'unning, 'F'inished" state Ascii 1 Data default -,
+ INFO "error reason if state is failed" info Ascii 255 Data,
+}
diff --git a/lib/epgservice.c b/lib/epgservice.c
new file mode 100644
index 0000000..69e5a8e
--- /dev/null
+++ b/lib/epgservice.c
@@ -0,0 +1,121 @@
+/*
+ * epgservice.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "epgservice.h"
+
+//***************************************************************************
+// Timer State / Action
+//***************************************************************************
+
+const char* toName(TimerState s)
+{
+ switch (s)
+ {
+ case tsPending: return "pending";
+ case tsRunning: return "running";
+ case tsFinished: return "finished";
+ case tsDeleted: return "deleted";
+ case tsError: return "failed";
+ case tsIgnore: return "ignore";
+ case tsUnknown: return "unknown";
+ }
+
+ return "unknown";
+}
+
+const char* toName(TimerAction a, int nice)
+{
+ switch (a)
+ {
+ case taCreate: return "create";
+ case taModify: return "modify";
+ case taAdjust: return "adjust";
+ case taDelete: return "delete";
+ case taAssumed: return nice ? "-" : "assumed";
+ case taFailed: return "failed";
+ case taReject: return "reject";
+ }
+
+ return nice ? "-" : "unknown";
+}
+
+//***************************************************************************
+// cEpgdState
+//***************************************************************************
+
+const char* cEpgdState::states[] =
+{
+ "init",
+ "standby",
+ "stopped",
+
+ "busy (events)",
+ "busy (match)",
+ "busy (scraping)",
+ "busy (images)",
+
+ 0
+};
+
+const char* cEpgdState::toName(cEpgdState::State s)
+{
+ if (!isValid(s))
+ return "unknown";
+
+ return states[s];
+}
+
+cEpgdState::State cEpgdState::toState(const char* name)
+{
+ for (int i = 0; i < esCount; i++)
+ if (strcmp(states[i], name) == 0)
+ return (State)i;
+
+ return esUnknown;
+}
+
+//***************************************************************************
+// Field Filter
+//***************************************************************************
+
+FieldFilterDef fieldFilters[] =
+{
+ { ffAll, "all" },
+ { ffEpgd, "epgd" },
+ { ffEpgHttpd, "httpd" },
+ { ffEpg2Vdr, "epg2vdr" },
+ { ffScraper2Vdr, "scraper" },
+
+ { 0, 0 }
+};
+
+const char* toName(FieldFilter f)
+{
+ for (int i = 0; fieldFilters[i].name; i++)
+ if (fieldFilters[i].filter == f)
+ return fieldFilters[i].name;
+
+ return "unknown";
+}
+
+int toFieldFilter(const char* name)
+{
+ for (int i = 0; fieldFilters[i].name; i++)
+ if (strcasecmp(fieldFilters[i].name, name) == 0)
+ return fieldFilters[i].filter;
+
+ return ffAll;
+}
+
+//***************************************************************************
+// User Rights Check
+//***************************************************************************
+
+int hasUserMask(unsigned int rights, UserMask mask)
+{
+ return rights & mask;
+}
diff --git a/lib/epgservice.h b/lib/epgservice.h
new file mode 100644
index 0000000..969d17d
--- /dev/null
+++ b/lib/epgservice.h
@@ -0,0 +1,468 @@
+/*
+ * epgservice.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __EPGSERVICE_H
+#define __EPGSERVICE_H
+
+#include <list>
+
+#include "common.h"
+
+#define EPG_PLUGIN_SEM_KEY 0x3db00001
+
+//***************************************************************************
+// Globals
+//***************************************************************************
+
+typedef unsigned long long tEventId;
+
+enum EpgServiceMisc
+{
+ sizeMaxParameterValue = 150 // should match the field size in parameters table
+};
+
+enum FieldFilter
+{
+ ffAll = 0xFFFF,
+ ffEpgd = 1,
+ ffEpgHttpd = 2,
+ ffEpg2Vdr = 4,
+ ffScraper2Vdr = 8,
+
+ ffCount = 5
+};
+
+struct FieldFilterDef
+{
+ int filter;
+ const char* name;
+};
+
+const char* toName(FieldFilter f);
+int toFieldFilter(const char* name);
+
+enum SearchFields
+{
+ sfNone = 0, // off
+ sfTitle = 1,
+ sfFolge = 2,
+ sfDescription = 4
+};
+
+enum SearchMode
+{
+ smExact = 1,
+ smRegexp,
+ smLike,
+ smContained
+};
+
+enum TimerNamingMode
+{
+ tnmDefault = 0, // naming would done by VDR
+
+ // naming of following modes handled by recording.py an can 'configured' there
+
+ tnmAuto = 1, // autodetect if 'constabel', 'serie' or 'normal movie'
+ tnmConstabel = 2, // naming in constabel series style with season, number, ..
+ tnmSerie = 3, // series style, like Title/Subtitle
+ tnmCategorized = 4, // sorted in sub folders which are auto-named by category
+ tnmUser = 5 // user defined mode 'to implement in recording.py'
+
+};
+
+enum RecordingState
+{
+ rsFinished = 'F',
+ rsRecording = 'R',
+ rsDeleted = 'D'
+};
+
+enum TimerState
+{
+ tsUnknown = 'U',
+ tsPending = 'P',
+ tsRunning = 'R', // timer is recording
+ tsFinished = 'F',
+ tsDeleted = 'D',
+ tsError = 'E',
+ tsIgnore = '-' // ignore in timer menu -> already tooked 'any-VDR' timer
+};
+
+const char* toName(TimerState s);
+
+enum TimerAction
+{
+ taCreate = 'C',
+ taModify = 'M',
+ taAdjust = 'J',
+ taDelete = 'D',
+ taAssumed = 'A',
+ taFailed = 'F',
+ taReject = 'T'
+};
+
+enum TimerType
+{
+ ttRecord = 'R', // Aufnahme-Timer
+ ttView = 'V', // Umschalt-Timer
+ ttSearch = 'S' // Such-Timer
+};
+
+const char* toName(TimerAction a, int nice = no);
+
+enum TimerDoneState
+{
+ tdsTimerRequested = 'Q', // timer already requested by epgd/webif
+ tdsTimerCreated = 'C', // timer created by VDR
+ tdsTimerCreateFailed = 'f', // create/delete of timer failed by VDR
+
+ tdsRecordingDone = 'R', // Recording finished successfull
+ tdsRecordingFailed = 'F', // Recording failed
+
+ tdsTimerDeleted = 'D', // timer deleted by user
+ tdsTimerRejected = 'J' // timer rejected due to user action or timer conflict
+};
+
+enum UserMask
+{
+ umNone = 0x0,
+
+ umNologin = 0x1, // the right without a session
+
+ umConfig = 0x2,
+ umConfigEdit = 0x4,
+ umConfigUsers = 0x8,
+
+ umFree1 = 0x10,
+ umFree2 = 0x20,
+
+ umTimer = 0x40,
+ umTimerEdit = 0x80,
+ umSearchTimer = 0x100,
+ umSearchTimerEdit = 0x200,
+
+ umFree3 = 0x400,
+ umFree4 = 0x800,
+
+ umFsk = 0x1000,
+
+ umFree5 = 0x2000,
+ umFree6 = 0x4000,
+
+ umRecordings = 0x8000,
+ umRecordingsEdit = 0x10000,
+
+ umAll = 0xFFFFFFFF & ~umNologin
+};
+
+int hasUserMask(unsigned int rights, UserMask mask);
+
+//***************************************************************************
+// cEpgdState
+//***************************************************************************
+
+class cEpgdState
+{
+ public:
+
+ enum State
+ {
+ esUnknown = na,
+
+ esInit,
+ esStandby,
+ esStopped,
+
+ // handler pause on this states!
+
+ esBusy,
+ esBusyEvents = esBusy,
+ esBusyMatch,
+ esBusyScraping,
+
+ // handler don't pause on this states!
+
+ esBusyImages,
+
+ esCount
+ };
+
+ static const char* toName(State s);
+ static State toState(const char* name);
+ static int isValid(State s) { return s > esUnknown && s < esCount; }
+
+ static const char* states[];
+};
+
+typedef cEpgdState Es;
+
+//***************************************************************************
+// cEventState
+//***************************************************************************
+
+class cEventState
+{
+ public:
+
+ enum State
+ {
+ // add to VDRs EPG => wird von allen VDR angezeigt
+
+ usActive = 'A', // => Aktiv
+ usLink = 'L', // => DVB Event welches auf ein externes zeigt
+ usPassthrough = 'P', // => Durchgeleitetes Event: vdr:000
+
+ // remove from VDRs EPG => Löschsignal wird an alle vdr gesendet
+
+ usChanged = 'C', // => war mal aktiv wurde jedoch später zum Target,seine eigene ID muss also aus dem epg gelöscht werden. C ist also eine Ausprägung von T
+ usDelete = 'D', // => Sender oder Providerseitig gelöscht
+ usRemove = 'R', // => von uns gelöscht weil wir uns für ein anderes Event entschieden haben, zB eins mit Bildern oder Serieninformationen.
+
+ // don't care for VDRs EPG => werden von den VDRs nicht beachtet und nicht mal gesehen.
+
+ usInactive = 'I', // => inaktiv
+ usTarget = 'T', // => verknüftes Event zum Link, wird unter seiner ID mithilfe des Link im vdr geführt
+ usMergeSpare = 'S' // => ersatzevent welches für Multimerge ur Verfügung steht
+ };
+
+ // get lists for SQL 'in' statements
+
+ static const char* getDeletable() { return "'A','L','P','R','I'"; } // epg plugins
+ static const char* getNeeded() { return "'A','L','P','C','D','R'"; } // epg2vdr
+ static const char* getVisible() { return "'A','L','P'"; } // epghttpd
+
+ // checks
+
+ static int isNeeded(char c) { return strchr("ALPCDR", c) != 0; } // epgd2vdr
+ static int isRemove(char c) { return strchr("CDR", c) != 0; } // epgd2vdr
+
+};
+
+typedef cEventState Us; // remove later, not uses anymore
+
+//***************************************************************************
+// cUserTimes
+//***************************************************************************
+
+class cUserTimes
+{
+ public:
+
+ enum Mode
+ {
+ mUnknown = na,
+ mNow,
+ mNext,
+ mTime,
+ mSearch
+ };
+
+ struct UserTime
+ {
+ UserTime(const char* strTime, const char* strTitle = 0)
+ {
+ highlighted = yes;
+ mode = mUnknown;
+ *hhmmStr = 0;
+ title = 0;
+ hhmm = 0;
+ search = 0;
+ setTime(strTime, strTitle);
+ }
+
+ UserTime(const UserTime& cp)
+ {
+ highlighted = cp.highlighted;
+ mode = cp.mode;
+ hhmm = cp.hhmm;
+ strcpy(hhmmStr, cp.hhmmStr);
+ title = strdup(cp.title);
+ search = cp.search ? strdup(cp.search) : 0;
+ }
+
+ ~UserTime() { free(title); free(search); }
+
+ void setTime(const char* strTime, const char* strTitle)
+ {
+ hhmm = 0;
+ *hhmmStr = 0;
+
+ if (strTime[0] == '!')
+ {
+ highlighted = no;
+ strTime++;
+ }
+
+ if (strchr(strTime, ':'))
+ {
+ hhmm = atoi(strTime) * 100 + atoi(strchr(strTime, ':')+1);
+ sprintf(hhmmStr, "%02d:%02d", hhmm / 100, hhmm % 100);
+ mode = mTime;
+ }
+ else if (*strTime == '@')
+ {
+ highlighted = no;
+
+ if (strcmp(strTime, "@Now") == 0)
+ mode = mNow;
+ else if (strcmp(strTime, "@Next") == 0)
+ mode = mNext;
+ else
+ mode = mSearch;
+ }
+
+ // title
+
+ free(title);
+ title = 0;
+
+ if (!isEmpty(strTitle))
+ title = strdup(strTitle);
+ else if (!isEmpty(hhmmStr))
+ asprintf(&title, "%02d:%02d", hhmm / 100, hhmm % 100);
+
+ // search
+
+ if (*strTime == '@')
+ {
+ free(search);
+ search = strdup(strTime+1);
+ }
+ }
+
+ int getHHMM() const { return hhmm; }
+ const char* getHHMMStr() const { return hhmmStr; }
+ const char* getTitle() const { return title; }
+ const char* getSearch() const { return search; }
+ int getMode() const { return mode; }
+ const char* getHelpKey() const { return title; }
+ int isHighlighted() const { return highlighted; }
+
+ time_t getTime()
+ {
+ struct tm tmnow;
+ time_t now = time(0);
+
+ localtime_r(&now, &tmnow);
+ tmnow.tm_hour = hhmm / 100;
+ tmnow.tm_min = hhmm % 100;
+ tmnow.tm_sec = 0;
+
+ time_t ltime = mktime(&tmnow);
+
+ if (ltime < time(0)-tmeSecondsPerHour)
+ ltime += tmeSecondsPerDay;
+
+ return ltime;
+ }
+
+ int mode;
+ int highlighted;
+ char* title;
+ char* search;
+ int hhmm;
+ char hhmmStr[5+TB];
+ };
+
+ cUserTimes()
+ {
+ clear();
+ it = times.end();
+ }
+
+ void clear()
+ {
+ times.clear();
+ }
+
+ void add(const char* strTime, const char* strTitle = 0)
+ {
+ UserTime ut(strTime, strTitle);
+ times.push_back(ut);
+ }
+
+ UserTime* first()
+ {
+ it = times.begin();
+
+ if (it == times.end())
+ return 0;
+
+ return &(*it);
+ }
+
+ UserTime* next()
+ {
+ if (it == times.end())
+ it = times.begin();
+ else
+ it++;
+
+ if (it == times.end())
+ it = times.begin();
+
+ return &(*it);
+ }
+
+ UserTime* getFirst()
+ {
+ std::list<UserTime>::iterator i;
+
+ i = times.begin();
+
+ return &(*i);
+ }
+
+ UserTime* getNext()
+ {
+ std::list<UserTime>::iterator i = it;
+
+ if (i == times.end())
+ i = times.begin();
+ else
+ i++;
+
+ if (i == times.end())
+ i = times.begin();
+
+ return &(*i);
+ }
+
+ UserTime* current() { return &(*it); }
+
+ private:
+
+ std::list<UserTime>::iterator it;
+ std::list<UserTime> times;
+};
+
+//***************************************************************************
+// EPG Services
+//***************************************************************************
+
+#define EPG2VDR_UUID_SERVICE "epg2vdr-UuidService-v1.0"
+
+struct Epg2vdr_UUID_v1_0
+{
+ const char* uuid;
+};
+
+#define MYSQL_INIT_EXIT "Mysql_Init_Exit-v1.0"
+
+enum MysqlInitExitAction
+{
+ mieaInit = 0,
+ mieaExit = 1
+};
+
+struct Mysql_Init_Exit_v1_0
+{
+ MysqlInitExitAction action;
+};
+
+#endif // __EPGSERVICE_H
diff --git a/lib/imgtools.c b/lib/imgtools.c
new file mode 100644
index 0000000..63007e5
--- /dev/null
+++ b/lib/imgtools.c
@@ -0,0 +1,217 @@
+/*
+ * imgtools.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "imgtools.h"
+
+//***************************************************************************
+// Image converting stuff
+//***************************************************************************
+
+int fromJpeg(Imlib_Image& image, unsigned char* buffer, int size)
+{
+ struct jpeg_decompress_struct cinfo;
+ struct jpeg_error_mgr jerr;
+ int w, h;
+ DATA8 *ptr, *line[16], *data;
+ DATA32 *ptr2, *dest;
+ int x, y;
+
+ cinfo.err = jpeg_std_error(&jerr);
+
+ jpeg_create_decompress(&cinfo);
+ jpeg_mem_src(&cinfo, buffer, size);
+ jpeg_read_header(&cinfo, TRUE);
+ cinfo.do_fancy_upsampling = FALSE;
+ cinfo.do_block_smoothing = FALSE;
+
+ jpeg_start_decompress(&cinfo);
+
+ w = cinfo.output_width;
+ h = cinfo.output_height;
+
+ image = imlib_create_image(w, h);
+ imlib_context_set_image(image);
+
+ dest = ptr2 = imlib_image_get_data();
+ data = (DATA8*)malloc(w * 16 * cinfo.output_components);
+
+ for (int i = 0; i < cinfo.rec_outbuf_height; i++)
+ line[i] = data + (i * w * cinfo.output_components);
+
+ for (int l = 0; l < h; l += cinfo.rec_outbuf_height)
+ {
+ jpeg_read_scanlines(&cinfo, line, cinfo.rec_outbuf_height);
+ int scans = cinfo.rec_outbuf_height;
+
+ if (h - l < scans)
+ scans = h - l;
+
+ ptr = data;
+
+ for (y = 0; y < scans; y++)
+ {
+ for (x = 0; x < w; x++)
+ {
+ *ptr2 = (0xff000000) | ((ptr[0]) << 16) | ((ptr[1]) << 8) | (ptr[2]);
+ ptr += cinfo.output_components;
+ ptr2++;
+ }
+ }
+ }
+
+ free(data);
+
+ imlib_image_put_back_data(dest);
+
+ jpeg_finish_decompress(&cinfo);
+ jpeg_destroy_decompress(&cinfo);
+
+ return success;
+}
+
+long toJpeg(Imlib_Image image, MemoryStruct* data, int quality)
+{
+ struct jpeg_compress_struct cinfo;
+ struct jpeg_error_mgr jerr;
+ DATA32* ptr;
+ DATA8* buf;
+
+ imlib_context_set_image(image);
+
+ data->clear();
+
+ cinfo.err = jpeg_std_error(&jerr);
+
+ jpeg_create_compress(&cinfo);
+ jpeg_mem_dest(&cinfo, (unsigned char**)(&data->memory), &data->size);
+
+ cinfo.image_width = imlib_image_get_width();
+ cinfo.image_height = imlib_image_get_height();
+ cinfo.input_components = 3;
+ cinfo.in_color_space = JCS_RGB;
+
+ jpeg_set_defaults(&cinfo);
+ jpeg_set_quality(&cinfo, quality, TRUE);
+ jpeg_start_compress(&cinfo, TRUE);
+
+ // get data pointer
+
+ if (!(ptr = imlib_image_get_data_for_reading_only()))
+ return 0;
+
+ // allocate a small buffer to convert image data */
+
+ buf = (DATA8*)malloc(imlib_image_get_width() * 3 * sizeof(DATA8));
+
+ while (cinfo.next_scanline < cinfo.image_height)
+ {
+ // convert scanline from ARGB to RGB packed
+
+ for (int j = 0, i = 0; i < imlib_image_get_width(); i++)
+ {
+ buf[j++] = ((*ptr) >> 16) & 0xff;
+ buf[j++] = ((*ptr) >> 8) & 0xff;
+ buf[j++] = ((*ptr)) & 0xff;
+
+ ptr++;
+ }
+
+ // write scanline
+
+ jpeg_write_scanlines(&cinfo, (JSAMPROW*)(&buf), 1);
+ }
+
+ free(buf);
+ jpeg_finish_compress(&cinfo);
+ jpeg_destroy_compress(&cinfo);
+
+ return data->size;
+}
+
+int scaleImageToJpegBuffer(Imlib_Image image, MemoryStruct* data, int width, int height)
+{
+ if (width && height)
+ {
+ Imlib_Image scaledImage;
+
+ imlib_context_set_image(image);
+
+ int imgWidth = imlib_image_get_width();
+ int imgHeight = imlib_image_get_height();
+ double ratio = (double)imgWidth / (double)imgHeight;
+
+ if ((double)width/(double)imgWidth < (double)height/(double)imgHeight)
+ height = (int)((double)width / ratio);
+ else
+ width = (int)((double)height * ratio);
+
+ scaledImage = imlib_create_image(width, height);
+ imlib_context_set_image(scaledImage);
+
+ imlib_context_set_color(240, 240, 240, 255);
+ imlib_image_fill_rectangle(0, 0, width, height);
+
+ imlib_blend_image_onto_image(image, 0, 0, 0,
+ imgWidth, imgHeight, 0, 0,
+ width, height);
+
+ toJpeg(scaledImage, data, 70);
+
+ imlib_context_set_image(scaledImage);
+ imlib_free_image();
+
+ tell(2, "Scaled image to %d/%d, now %d bytes", width, height, (int)data->size);
+ }
+ else
+ {
+ toJpeg(image, data, 70);
+ }
+
+ return success;
+}
+
+int scaleJpegBuffer(MemoryStruct* data, int width, int height)
+{
+ Imlib_Image image;
+
+ fromJpeg(image, (unsigned char*)data->memory, data->size);
+
+ scaleImageToJpegBuffer(image, data, width, height);
+
+ imlib_context_set_image(image);
+ imlib_free_image();
+
+ return success;
+}
+
+//***************************************************************************
+// Str Imlib Error
+//***************************************************************************
+
+const char* strImlibError(Imlib_Load_Error err)
+{
+ switch (err)
+ {
+ case IMLIB_LOAD_ERROR_NONE:
+ case IMLIB_LOAD_ERROR_FILE_DOES_NOT_EXIST: return "File does not exist";
+ case IMLIB_LOAD_ERROR_FILE_IS_DIRECTORY: return "File is directory";
+ case IMLIB_LOAD_ERROR_PERMISSION_DENIED_TO_READ: return "Permission denied to read";
+ case IMLIB_LOAD_ERROR_NO_LOADER_FOR_FILE_FORMAT: return "No loader for file format";
+ case IMLIB_LOAD_ERROR_PATH_TOO_LONG: return "Path too long";
+ case IMLIB_LOAD_ERROR_PATH_COMPONENT_NON_EXISTANT: return "Path component non existant";
+ case IMLIB_LOAD_ERROR_PATH_COMPONENT_NOT_DIRECTORY: return "Path component not directory";
+ case IMLIB_LOAD_ERROR_PATH_POINTS_OUTSIDE_ADDRESS_SPACE: return "Path points outside address space";
+ case IMLIB_LOAD_ERROR_TOO_MANY_SYMBOLIC_LINKS: return "Too many symbolic links";
+ case IMLIB_LOAD_ERROR_OUT_OF_MEMORY: return "Out of memory";
+ case IMLIB_LOAD_ERROR_OUT_OF_FILE_DESCRIPTORS: return "Out of file descriptors";
+ case IMLIB_LOAD_ERROR_PERMISSION_DENIED_TO_WRITE: return "Permission denied to write";
+ case IMLIB_LOAD_ERROR_OUT_OF_DISK_SPACE: return "Out of disk space";
+ case IMLIB_LOAD_ERROR_UNKNOWN: return "Unknown format";
+ }
+
+ return "";
+}
diff --git a/lib/imgtools.h b/lib/imgtools.h
new file mode 100644
index 0000000..315afaf
--- /dev/null
+++ b/lib/imgtools.h
@@ -0,0 +1,31 @@
+/*
+ * imgtools.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __IMGTOOLS_H
+#define __IMGTOOLS_H
+
+#include <stdio.h>
+
+#define X_DISPLAY_MISSING 1
+
+#include <jpeglib.h>
+#include <Imlib2.h>
+
+#include "common.h"
+
+//***************************************************************************
+// Image Manipulating
+//***************************************************************************
+
+int fromJpeg(Imlib_Image& image, unsigned char* buffer, int size);
+long toJpeg(Imlib_Image image, MemoryStruct* data, int quality);
+int scaleImageToJpegBuffer(Imlib_Image image, MemoryStruct* data, int width = 0, int height = 0);
+int scaleJpegBuffer(MemoryStruct* data, int width = 0, int height = 0);
+const char* strImlibError(Imlib_Load_Error err);
+
+//***************************************************************************
+#endif // __IMGTOOLS_H
diff --git a/lib/json.c b/lib/json.c
new file mode 100644
index 0000000..31a14e0
--- /dev/null
+++ b/lib/json.c
@@ -0,0 +1,164 @@
+/*
+ * json.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "json.h"
+
+#ifdef USEJSON
+
+const char* charset = "utf-8"; // #TODO, move to configuration?
+
+//***************************************************************************
+// Copy JSON Object to Data Buffer
+//***************************************************************************
+
+int json2Data(json_t* obj, MemoryStruct* data, const char* encoding)
+{
+ int status = success;
+
+ // will be freed by data's dtor
+
+ data->memory = json_dumps(obj, JSON_PRESERVE_ORDER);
+ data->size = strlen(data->memory);
+ sprintf(data->contentType, "application/json; charset=%s", charset);
+
+ // gzip content if supported by browser ...
+
+ if (data->size && encoding && strstr(encoding, "gzip"))
+ status = data->toGzip();
+
+ return status;
+}
+
+//***************************************************************************
+// Add field to json object
+//***************************************************************************
+
+int addFieldToJson(json_t* obj, cDbValue* value, int ignoreEmpty, const char* extName)
+{
+ char* name;
+
+ if (!value || !value->getField())
+ return fail;
+
+ name = strdup(extName ? extName : value->getField()->getName());
+ toCase(cLower, name); // use always lower case names in json
+
+ switch (value->getField()->getFormat())
+ {
+ case cDBS::ffAscii:
+ case cDBS::ffText:
+ case cDBS::ffMText:
+ if (!ignoreEmpty || !isEmpty(value->getStrValue()))
+ json_object_set_new(obj, name, json_string(value->getStrValue()));
+ break;
+
+ case cDBS::ffInt:
+ case cDBS::ffUInt:
+ json_object_set_new(obj, name, json_integer(value->getIntValue()));
+ break;
+
+ case cDBS::ffFloat:
+ json_object_set_new(obj, name, json_real(value->getFloatValue()));
+ break;
+
+ default:
+ break;
+ }
+
+ free(name);
+
+ return success;
+}
+
+//***************************************************************************
+// Add field to json object
+//***************************************************************************
+
+int addFieldToJson(json_t* obj, cDbTable* table, const char* fname,
+ int ignoreEmpty, const char* extName)
+{
+ return addFieldToJson(obj, table->getValue(fname), ignoreEmpty, extName);
+}
+
+//***************************************************************************
+// Get Field From Json
+// - if a default is required put it into the row
+// before calling this function
+//***************************************************************************
+
+int getFieldFromJson(json_t* obj, cDbRow* row, const char* fname, const char* extName)
+{
+ cDbValue* value = row->getValue(fname);
+ char* jname;
+
+ if (!value)
+ return fail;
+
+ jname = strdup(!isEmpty(extName) ? extName : value->getField()->getName());
+ toCase(cLower, jname); // use always lower case names in json
+
+ switch (value->getField()->getFormat())
+ {
+ case cDBS::ffAscii:
+ case cDBS::ffText:
+ case cDBS::ffMText:
+ {
+ const char* v = getStringFromJson(obj, jname, "");
+ if (!isEmpty(v) || !value->isEmpty())
+ value->setValue(v);
+ break;
+ }
+
+ case cDBS::ffInt:
+ case cDBS::ffUInt:
+ {
+ int v = getIntFromJson(obj, jname, na);
+ if (v != na || !value->isEmpty())
+ value->setValue(v);
+ break;
+ }
+// #TODO to be implemented
+// case cDBS::ffFloat:
+// {
+// double v = getFloatFromJson(obj, jname, na);
+// if (v != na) value->setValue(v);
+// break;
+// }
+
+ default:
+ break;
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Get Elements
+//***************************************************************************
+
+const char* getStringFromJson(json_t* obj, const char* name, const char* def)
+{
+ json_t* o = json_object_get(obj, name);
+
+ if (!o)
+ return def;
+
+ return json_string_value(o);
+}
+
+int getIntFromJson(json_t* obj, const char* name, int def)
+{
+ json_t* o = json_object_get(obj, name);
+
+ if (!o)
+ return def;
+
+ return json_integer_value(o);
+}
+
+//***************************************************************************
+#endif // USEJSON
diff --git a/lib/json.h b/lib/json.h
new file mode 100644
index 0000000..dd66a93
--- /dev/null
+++ b/lib/json.h
@@ -0,0 +1,36 @@
+/*
+ * json.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __JSON_H
+#define __JSON_H
+
+//***************************************************************************
+// Include
+//***************************************************************************
+
+#ifdef USEJSON
+
+#include <jansson.h>
+
+#include "db.h"
+
+//***************************************************************************
+// JSON Helper Functions
+//***************************************************************************
+
+int json2Data(json_t* obj, MemoryStruct* data, const char* encoding = 0);
+
+int addFieldToJson(json_t* obj, cDbTable* table, const char* fname, int ignoreEmpty = yes, const char* extName = 0);
+int addFieldToJson(json_t* obj, cDbValue* value, int ignoreEmpty = yes, const char* extName = 0);
+int getFieldFromJson(json_t* obj, cDbRow* row, const char* fname, const char* extName = 0);
+
+const char* getStringFromJson(json_t* obj, const char* name, const char* def = 0);
+int getIntFromJson(json_t* obj, const char* name, int def = na);
+
+#endif // USEJSON
+
+#endif // __JSON_H
diff --git a/lib/python.c b/lib/python.c
new file mode 100644
index 0000000..2ec2175
--- /dev/null
+++ b/lib/python.c
@@ -0,0 +1,356 @@
+/*
+ * python.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "python.h"
+
+cDbTable* Python::globalEventsDb = 0;
+int Python::globalNamingMode = 0;
+const char* Python::globalTmplExpression = "";
+
+//***************************************************************************
+// Static Interface Methods (Table events)
+//***************************************************************************
+
+PyObject* Python::eventTitle(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("TITLE"));
+}
+
+PyObject* Python::eventShortText(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("SHORTTEXT"));
+}
+
+PyObject* Python::eventStartTime(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("i", 0);
+
+ return Py_BuildValue("i", globalEventsDb->getIntValue("STARTTIME"));
+}
+
+PyObject* Python::eventYear(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("YEAR"));
+}
+
+PyObject* Python::eventCategory(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("CATEGORY"));
+}
+
+PyObject* Python::episodeName(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODENAME"));
+}
+
+PyObject* Python::episodeShortName(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODESHORTNAME"));
+}
+
+PyObject* Python::episodePartName(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODEPARTNAME"));
+}
+
+PyObject* Python::extracol1(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODEEXTRACOL1"));
+}
+
+PyObject* Python::extracol2(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODEEXTRACOL2"));
+}
+
+PyObject* Python::extracol3(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("s", "");
+
+ return Py_BuildValue("s", globalEventsDb->getStrValue("EPISODEEXTRACOL3"));
+}
+
+PyObject* Python::episodeSeason(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("i", na);
+
+ return Py_BuildValue("i", globalEventsDb->getIntValue("EPISODESEASON"));
+}
+
+PyObject* Python::episodePart(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("i", na);
+
+ return Py_BuildValue("i", globalEventsDb->getIntValue("EPISODEPART"));
+}
+
+PyObject* Python::episodeNumber(PyObject* /*self*/, PyObject* /*args*/)
+{
+ if (!globalEventsDb)
+ return Py_BuildValue("i", na);
+
+ return Py_BuildValue("i", globalEventsDb->getIntValue("EPISODENUMBER"));
+}
+
+PyObject* Python::namingMode(PyObject* /*self*/, PyObject* /*args*/)
+{
+ return Py_BuildValue("i", globalNamingMode);
+}
+
+PyObject* Python::tmplExpression(PyObject* /*self*/, PyObject* /*args*/)
+{
+ return Py_BuildValue("s", globalTmplExpression);
+}
+
+//***************************************************************************
+// The Methods
+//***************************************************************************
+
+PyMethodDef Python::eventMethods[] =
+{
+ { "title", Python::eventTitle, METH_VARARGS, "Return the events ..." },
+ { "shorttext", Python::eventShortText, METH_VARARGS, "Return the events ..." },
+ { "starttime", Python::eventStartTime, METH_VARARGS, "Return the events ..." },
+ { "year", Python::eventYear, METH_VARARGS, "Return the events ..." },
+ { "category", Python::eventCategory, METH_VARARGS, "Return the events ..." },
+
+ { "episodname", Python::episodeName, METH_VARARGS, "Return the events ..." },
+ { "shortname", Python::episodeShortName, METH_VARARGS, "Return the events ..." },
+ { "partname", Python::episodePartName, METH_VARARGS, "Return the events ..." },
+
+ { "season", Python::episodeSeason, METH_VARARGS, "Return the events ..." },
+ { "part", Python::episodePart, METH_VARARGS, "Return the events ..." },
+ { "number", Python::episodeNumber, METH_VARARGS, "Return the events ..." },
+
+ { "extracol1", Python::extracol1, METH_VARARGS, "Return the events ..." },
+ { "extracol2", Python::extracol2, METH_VARARGS, "Return the events ..." },
+ { "extracol3", Python::extracol3, METH_VARARGS, "Return the events ..." },
+
+ { "namingmode", Python::namingMode, METH_VARARGS, "Return the ..." },
+ { "tmplExpression", Python::tmplExpression, METH_VARARGS, "Return the ..." },
+
+ { 0, 0, 0, 0 }
+};
+
+#if PY_MAJOR_VERSION >= 3
+
+PyModuleDef Python::moduledef =
+{
+ PyModuleDef_HEAD_INIT, // m_base
+ "event", // m_name - name of module
+ 0, // m_doc - module documentation, may be NULL
+ -1, // m_size - size of per-interpreter state of the module, or -1 if the module keeps state in global variables.
+ eventMethods, // m_methods
+ 0, // m_slots - array of slot definitions for multi-phase initialization, terminated by a {0, NULL} entry.
+ // when using single-phase initialization, m_slots must be NULL.
+ 0, // traverseproc m_traverse - traversal function to call during GC traversal of the module object, or NULL if not needed.
+ 0, // inquiry m_clear - clear function to call during GC clearing of the module object, or NULL if not needed.
+ 0 // freefunc m_free - function to call during deallocation of the module object or NULL if not needed.
+};
+
+#endif
+
+//***************************************************************************
+// Object
+//***************************************************************************
+
+Python::Python(const char* aFile, const char* aFunction)
+{
+ pFunc = 0;
+ pModule = 0;
+ result = 0;
+ file = strdup(aFile);
+ function = strdup(aFunction);
+}
+
+Python::~Python()
+{
+ exit();
+
+ free(result);
+ free(file);
+ free(function);
+}
+
+//***************************************************************************
+// Init / Exit
+//***************************************************************************
+
+int Python::init(const char* modulePath)
+{
+ PyObject* pName;
+
+ tell(0, "Initialize python script '%s/%s.py'", modulePath, file);
+
+ // register event methods
+
+#if PY_MAJOR_VERSION >= 3
+ PyImport_AppendInittab("event", &PyInitEvent);
+ Py_Initialize(); // initialize the Python interpreter
+ pName = PyUnicode_FromString(file);
+#else
+ Py_Initialize(); // initialize the Python interpreter
+ Py_InitModule("event", eventMethods);
+ pName = PyString_FromString(file);
+#endif
+
+ // add search path for Python modules
+
+ if (modulePath)
+ {
+ char* p;
+ asprintf(&p, "sys.path.append(\"%s\")", modulePath);
+ PyRun_SimpleString("import sys");
+ PyRun_SimpleString(p);
+ free(p);
+ }
+
+ // import the module
+
+ pModule = PyImport_Import(pName);
+ Py_DECREF(pName);
+
+ if (!pModule)
+ {
+ showError();
+ tell(0, "Failed to load '%s.py'", file);
+
+ return fail;
+ }
+
+ pFunc = PyObject_GetAttrString(pModule, function);
+
+ // pFunc is a new reference
+
+ if (!pFunc || !PyCallable_Check(pFunc))
+ {
+ if (PyErr_Occurred())
+ showError();
+
+ tell(0, "Cannot find function '%s'", function);
+
+ return fail;
+ }
+
+ return success;
+}
+
+int Python::exit()
+{
+ if (pFunc)
+ Py_XDECREF(pFunc);
+
+ if (pModule)
+ Py_DECREF(pModule);
+
+ Py_Finalize();
+
+ return success;
+}
+
+//***************************************************************************
+// Execute
+//***************************************************************************
+
+int Python::execute(cDbTable* eventsDb, int namingmode, const char* tmplExpression)
+{
+ PyObject* pValue;
+
+ free(result);
+ result = 0;
+
+ globalEventsDb = eventsDb;
+ globalNamingMode = namingmode;
+ globalTmplExpression = tmplExpression;
+
+ pValue = PyObject_CallObject(pFunc, 0);
+
+ if (!pValue)
+ {
+ showError();
+ tell(0, "Python: Call of function '%s()' failed", function);
+
+ return fail;
+ }
+
+#if PY_MAJOR_VERSION >= 3
+ // PyObject* strExc = PyObject_Repr(pValue);
+ // PyObject* pyStr = PyUnicode_AsEncodedString(strExc, "utf-8", "replace");
+ PyObject* pyStr = PyUnicode_AsEncodedString(pValue, "utf-8", "replace");
+ result = strdup(PyBytes_AsString(pyStr));
+ // Py_XDECREF(strExc);
+
+#else
+ result = strdup(PyString_AsString(pValue));
+#endif
+
+ tell(3, "Result of call: %s", result);
+
+ Py_DECREF(pValue);
+
+ return success;
+}
+
+//***************************************************************************
+// Error
+//***************************************************************************
+
+void Python::showError()
+{
+ const char* error = "";
+ PyObject *ptype = 0, *pvalue = 0, *ptraceback = 0;
+
+ PyErr_Fetch(&ptype, &pvalue, &ptraceback);
+
+#if PY_MAJOR_VERSION >= 3
+ PyObject* strExc = PyObject_Repr(pvalue); // Now a unicode object
+ PyObject* pyStr = PyUnicode_AsEncodedString(strExc, "utf-8", "Error ~");
+ error = PyBytes_AsString(pyStr);
+
+ Py_XDECREF(strExc);
+ Py_XDECREF(pyStr);
+#else
+ error = PyString_AsString(pvalue);
+#endif
+
+ tell(0, "Python error was: %s", error);
+
+ Py_DECREF(pvalue);
+ Py_DECREF(ptype);
+ Py_XDECREF(ptraceback);
+}
diff --git a/lib/python.h b/lib/python.h
new file mode 100644
index 0000000..eb50964
--- /dev/null
+++ b/lib/python.h
@@ -0,0 +1,78 @@
+/*
+ * python.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __PYTHON_H
+#define __PYTHON_H
+
+#include <Python.h>
+
+#include "db.h"
+
+//***************************************************************************
+// Class Python
+//***************************************************************************
+
+class Python
+{
+ public:
+
+ Python(const char* aFile, const char* aFunction);
+ ~Python();
+
+ int init(const char* modulePath = 0);
+ int exit();
+
+ int execute(cDbTable* eventsDb, int namingmode, const char* tmplExpression);
+
+ const char* getResult() { return result ? result : ""; }
+
+ protected:
+
+ static PyObject* eventTitle(PyObject* self, PyObject* args);
+ static PyObject* eventShortText(PyObject* self, PyObject* args);
+ static PyObject* eventStartTime(PyObject* self, PyObject* args);
+ static PyObject* eventYear(PyObject* self, PyObject* args);
+ static PyObject* eventCategory(PyObject* self, PyObject* args);
+
+ static PyObject* episodeName(PyObject* self, PyObject* args);
+ static PyObject* episodeShortName(PyObject* self, PyObject* args);
+ static PyObject* episodePartName(PyObject* self, PyObject* args);
+ static PyObject* extracol1(PyObject* self, PyObject* args);
+ static PyObject* extracol2(PyObject* self, PyObject* args);
+ static PyObject* extracol3(PyObject* self, PyObject* args);
+ static PyObject* episodeSeason(PyObject* self, PyObject* args);
+ static PyObject* episodePart(PyObject* self, PyObject* args);
+ static PyObject* episodeNumber(PyObject* self, PyObject* args);
+ static PyObject* namingMode(PyObject* self, PyObject* args);
+ static PyObject* tmplExpression(PyObject* self, PyObject* args);
+
+ void showError();
+
+ // data
+
+ PyObject* pModule;
+ PyObject* pFunc;
+
+ char* file;
+ char* function;
+ char* result;
+
+ // static stuff
+
+ static cDbTable* globalEventsDb;
+ static int globalNamingMode;
+ static const char* globalTmplExpression;
+ static PyMethodDef eventMethods[];
+
+#if PY_MAJOR_VERSION >= 3
+ static PyObject* PyInitEvent() { return PyModule_Create(&Python::moduledef); }
+ static PyModuleDef moduledef;
+#endif
+};
+
+//***************************************************************************
+#endif // __PYTHON_H
diff --git a/lib/pytst.c b/lib/pytst.c
new file mode 100644
index 0000000..9bd8eb6
--- /dev/null
+++ b/lib/pytst.c
@@ -0,0 +1,122 @@
+
+#include "python.h"
+
+
+#include "config.h"
+#include "common.h"
+#include "db.h"
+#include "epgservice.h"
+
+
+cDbTable* eventsDb = 0;
+cDbConnection* connection = 0;
+const char* logPrefix = "";
+
+//***************************************************************************
+// Init / Exit
+//***************************************************************************
+
+void initConnection()
+{
+ cDbConnection::init();
+
+ cDbConnection::setEncoding("utf8");
+ cDbConnection::setHost("localhost");
+
+ cDbConnection::setPort(3306);
+ cDbConnection::setName("epg2vdr");
+ cDbConnection::setUser("epg2vdr");
+ cDbConnection::setPass("epg");
+ cDbConnection::setConfPath("/etc/epgd/");
+
+ connection = new cDbConnection();
+}
+
+void exitConnection()
+{
+ cDbConnection::exit();
+
+ if (connection)
+ delete connection;
+}
+
+int init()
+{
+ eventsDb = new cDbTable(connection, "useevents");
+ if (eventsDb->open() != success) return fail;
+
+ return success;
+}
+
+int exit()
+{
+ delete eventsDb;
+ return done;
+}
+
+//***************************************************************************
+// Main
+//***************************************************************************
+
+int main(int argc, char** argv)
+{
+ cEpgConfig::logstdout = yes;
+ cEpgConfig::loglevel = 0;
+ int namingmode = tnmAuto;
+
+ if (argc < 3)
+ {
+ tell(0, "Usage: pytst <channnelid> <eventid> [<namingmode>]");
+ return 1;
+ }
+
+ if (argc >= 4)
+ namingmode = atoi(argv[3]);
+
+ // at first allpy locale !!
+
+ setlocale(LC_CTYPE, "");
+
+ // read dictionary
+
+ if (dbDict.in("/etc/epgd/epg.dat") != success)
+ {
+ tell(0, "Invalid dictionary configuration, aborting!");
+ return 1;
+ }
+
+ initConnection();
+ init();
+
+ eventsDb->clear();
+ eventsDb->setValue("CNTSOURCE", "tvm");
+ eventsDb->setValue("CHANNELID", argv[1]);
+ eventsDb->setBigintValue("CNTEVENTID", atol(argv[2]));
+
+ if (!eventsDb->find())
+ {
+ tell(0, "Event %s/%ld not found", argv[1], atol(argv[2]));
+ return 1;
+ }
+
+ tell(2, "Event '%s/%s' found",
+ eventsDb->getStrValue("TITLE"), eventsDb->getStrValue("SHORTTEXT"));
+
+ // Python stuff ..
+
+ Python py("recording", "name");
+
+ if (py.init("/etc/epgd") != success)
+ {
+ tell(0, "Init of python failed!");
+ return 1;
+ }
+
+ if (py.execute(eventsDb, namingmode) == success)
+ tell(0, "Info: The recording name calculated by 'recording.py' (with namingmode %d) is '%s'", namingmode, py.getResult());
+
+ py.exit();
+ exitConnection();
+
+ return 0;
+}
diff --git a/lib/searchtimer.c b/lib/searchtimer.c
new file mode 100644
index 0000000..971230f
--- /dev/null
+++ b/lib/searchtimer.c
@@ -0,0 +1,1373 @@
+/*
+ * searchtimer.c
+ *
+ * See the README file for copyright information
+ *
+ */
+
+#include "python.h"
+#include "searchtimer.h"
+
+//***************************************************************************
+//
+//***************************************************************************
+
+int cSearchTimer::searchField[] =
+{
+ sfTitle,
+ sfFolge,
+ sfDescription,
+ 0
+};
+
+const char* cSearchTimer::searchFieldName[] =
+{
+ "TITLE",
+ "SHORTTEXT",
+ "COMPLONGDESCRIPTION",
+ 0
+};
+
+int cSearchTimer::repeadCheckField[] =
+{
+ sfTitle,
+ sfFolge,
+ sfDescription,
+
+ 0
+};
+
+const char* cSearchTimer::repeadCheckFieldName[] =
+{
+ "COMPTITLE", // <- "EPISODECOMPNAME" <- "EPISODECOMPSHORTNAME"
+ "COMPSHORTTEXT", // <- "EPISODECOMPPARTNAME"
+ "COMPLONGDESCRIPTION",
+
+ 0
+};
+
+//***************************************************************************
+// Class Search Timer
+//***************************************************************************
+
+cSearchTimer::cSearchTimer()
+ : startValue("START", cDBS::ffInt, 10),
+ endValue("END", cDBS::ffInt, 10)
+{
+ connection = 0;
+ useeventsDb = 0;
+ searchtimerDb = 0;
+ timerDb = 0;
+ timersDoneDb = 0;
+ mapDb = 0;
+ vdrDb = 0;
+
+ selectChannelFromMap = 0;
+ selectDoneTimer = 0;
+ selectActiveSearchtimers = 0;
+ selectSearchtimerMaxModSp = 0;
+ selectAllTimer = 0;
+ selectTimerByEvent = 0;
+ // selectConflictingTimers = 0;
+
+ ptyRecName = 0;
+ lastSearchTimerUpdate = 0;
+}
+
+cSearchTimer::~cSearchTimer()
+{
+ delete ptyRecName;
+}
+
+int cSearchTimer::init(const char* confDir)
+{
+ ptyRecName = new Python("recording", "name");
+
+ if (ptyRecName->init(confDir) != success)
+ {
+ tell(0, "Init of python script recording.py failed, aborting");
+ return fail;
+ }
+
+ return done;
+}
+
+int cSearchTimer::initDb()
+{
+ int status = success;
+
+ exitDb();
+
+ connection = new cDbConnection();
+
+ useeventsDb = new cDbTable(connection, "useevents");
+ if ((status = useeventsDb->open() != success)) return status;
+
+ searchtimerDb = new cDbTable(connection, "searchtimers");
+ if (searchtimerDb->open() != success) return fail;
+
+ timerDb = new cDbTable(connection, "timers");
+ if (timerDb->open() != success) return fail;
+
+ timersDoneDb = new cDbTable(connection, "timersdone");
+ if (timersDoneDb->open() != success) return fail;
+
+ mapDb = new cDbTable(connection, "channelmap");
+ if ((status = mapDb->open()) != success) return status;
+
+ vdrDb = new cDbTable(connection, "vdrs");
+ if ((status = vdrDb->open()) != success) return status;
+
+ // ----------
+ // select ...
+ // from searchtimers
+ // where state <> 'D' and active > 0
+ // and (type = 'R' or type is null)
+
+ selectActiveSearchtimers = new cDbStatement(searchtimerDb);
+
+ selectActiveSearchtimers->build("select ");
+ selectActiveSearchtimers->bindAllOut();
+ selectActiveSearchtimers->bind("UPDSP", cDBS::bndOut, ", ");
+ selectActiveSearchtimers->bind("INSSP", cDBS::bndOut, ", ");
+ selectActiveSearchtimers->build(" from %s where %s <> 'D' and %s > 0 and (%s = 'R' or %s is null)",
+ searchtimerDb->TableName(),
+ searchtimerDb->getField("STATE")->getDbName(),
+ searchtimerDb->getField("ACTIVE")->getDbName(),
+ searchtimerDb->getField("TYPE")->getDbName(),
+ searchtimerDb->getField("TYPE")->getDbName());
+
+ status += selectActiveSearchtimers->prepare();
+
+ // --------------------
+ // select max(modsp) from searchtimers
+
+ selectSearchtimerMaxModSp = new cDbStatement(searchtimerDb);
+
+ selectSearchtimerMaxModSp->build("select ");
+ selectSearchtimerMaxModSp->bind("MODSP", cDBS::bndOut, "max(");
+ selectSearchtimerMaxModSp->build(") from %s", searchtimerDb->TableName());
+
+ status += selectSearchtimerMaxModSp->prepare();
+
+ // ----------
+ // select id
+ // from timersdone
+ // ...
+
+ selectDoneTimer = new cDbStatement(timersDoneDb);
+
+ // selectDoneTimer - will build and prepared later at runtime ...
+
+ // ----------
+ // select channelname
+ // from channelmap
+
+ selectChannelFromMap = new cDbStatement(mapDb);
+
+ selectChannelFromMap->build("select ");
+ selectChannelFromMap->bind("CHANNELNAME", cDBS::bndOut);
+ selectChannelFromMap->build(" from %s where ", mapDb->TableName());
+ selectChannelFromMap->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet);
+
+ status += selectChannelFromMap->prepare();
+
+ // select *,
+ // from timers
+ // where state in ('P','R')
+
+ selectAllTimer = new cDbStatement(timerDb);
+
+ selectAllTimer->build("select ");
+ selectAllTimer->setBindPrefix("t.");
+ selectAllTimer->bindAllOut();
+ selectAllTimer->setBindPrefix("v.");
+ selectAllTimer->bind(vdrDb, "NAME", cDBS::bndOut, ", ");
+ selectAllTimer->bind(vdrDb, "TUNERCOUNT", cDBS::bndOut, ", ");
+ selectAllTimer->build(" from %s t, %s v ", timerDb->TableName(), vdrDb->TableName());
+ selectAllTimer->build(" where t.%s in ('P','R')", timerDb->getField("STATE")->getDbName());
+ selectAllTimer->build(" and t.VDRUUID = v.UUID");
+ selectAllTimer->build(" order by t.%s, t.%s",
+ timerDb->getField("DAY")->getDbName(),
+ timerDb->getField("STARTTIME")->getDbName());
+
+ status += selectAllTimer->prepare();
+
+ // select *
+ // from timers
+ // where (state in ('P','R') or state is null)
+ // and eventid = ?
+
+ selectTimerByEvent = new cDbStatement(timerDb);
+
+ selectTimerByEvent->build("select ");
+ selectTimerByEvent->bindAllOut();
+ selectTimerByEvent->build(" from %s where ", timerDb->TableName());
+ selectTimerByEvent->build("(%s in ('P','R') or %s is null)",
+ timerDb->getField("STATE")->getDbName(),
+ timerDb->getField("STATE")->getDbName());
+ selectTimerByEvent->bind("EVENTID", cDBS::bndIn | cDBS::bndSet, " and ");
+ status += selectTimerByEvent->prepare();
+
+ // select * from timers
+ // where state in ('P','R')
+ // and active = 1
+ // and day + starttime div 100 * 60 * 60 + starttime % 100 * 60 >= ?
+ // and day + starttime div 100 * 60 * 60 + starttime % 100 * 60 <= ?
+ // and vdruuid = ?
+ // group by SUBSTRING_INDEX(channelid, '-', 3);
+
+ // selectConflictingTimers = new cDbStatement(timerDb);
+
+ // selectConflictingTimers->build("select ");
+ // selectConflictingTimers->bindAllOut();
+ // selectConflictingTimers->build(" from %s where %s in ('P','R')",
+ // timerDb->TableName(), timerDb->getField("STATE")->getDbName());
+ // selectConflictingTimers->build(" and active = 1");
+ // selectConflictingTimers->bindText("day + starttime div 100 * 60 * 60 + starttime % 100 * 60", &startValue, ">=", " and ");
+ // selectConflictingTimers->bindText("day + starttime div 100 * 60 * 60 + starttime % 100 * 60", &endValue, "<=", " and ");
+ // selectConflictingTimers->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and ");
+ // selectConflictingTimers->build(" group by SUBSTRING_INDEX(channelid, '-', 3)");
+
+ // status += selectConflictingTimers->prepare();
+
+ // ----------
+
+ if (status != success)
+ {
+ tell(0, "Error: At least %d statements not prepared successfully", status*-1);
+ return status;
+ }
+
+ return success;
+}
+
+int cSearchTimer::exitDb()
+{
+ if (connection)
+ {
+ delete selectActiveSearchtimers; selectActiveSearchtimers = 0;
+ delete selectSearchtimerMaxModSp; selectSearchtimerMaxModSp = 0;
+ delete selectDoneTimer; selectDoneTimer = 0;
+ delete selectChannelFromMap; selectChannelFromMap = 0;
+ delete selectAllTimer; selectAllTimer = 0;
+ // delete selectConflictingTimers; selectConflictingTimers = 0;
+ delete selectTimerByEvent; selectTimerByEvent = 0;
+
+ delete mapDb; mapDb = 0;
+ delete useeventsDb; useeventsDb = 0;
+ delete searchtimerDb; searchtimerDb = 0;
+ delete timerDb; timerDb = 0;
+ delete timersDoneDb; timersDoneDb = 0;
+ delete vdrDb; vdrDb = 0;
+
+ delete connection; connection = 0;
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Any Search Timer Modified
+//***************************************************************************
+
+int cSearchTimer::modified()
+{
+ int modsp = 0;
+
+ if (selectSearchtimerMaxModSp->find())
+ modsp = searchtimerDb->getIntValue("MODSP");
+
+ selectSearchtimerMaxModSp->freeResult();
+
+ return modsp && modsp > lastSearchTimerUpdate;
+}
+
+//***************************************************************************
+// Prepare Search Statement
+//***************************************************************************
+
+cDbStatement* cSearchTimer::prepareSearchStatement(cDbRow* searchTimer, cDbTable* db)
+{
+ cDbStatement* select = new cDbStatement(db);
+
+ const char* searchOp = "=";
+ const char* expression = searchTimer->getStrValue("EXPRESSION");
+ const char* expression1 = searchTimer->getStrValue("EXPRESSION1");
+ const char* episodename = searchTimer->getStrValue("EPISODENAME");
+ const char* season = searchTimer->getStrValue("SEASON");
+ const char* seasonpart = searchTimer->getStrValue("SEASONPART");
+ const char* category = searchTimer->getStrValue("CATEGORY");
+ const char* genre = searchTimer->getStrValue("GENRE");
+ const char* tipp = searchTimer->getStrValue("TIPP");
+ const char* year = searchTimer->getStrValue("YEAR");
+ const char* chformat = searchTimer->getStrValue("CHFORMAT");
+
+ int noepgmatch = searchTimer->getIntValue("NOEPGMATCH");
+ int searchmode = searchTimer->getIntValue("SEARCHMODE");
+ int searchfields = searchTimer->getIntValue("SEARCHFIELDS");
+ int searchfields1 = searchTimer->getIntValue("SEARCHFIELDS1");
+ int casesensitiv = searchTimer->getIntValue("CASESENSITIV");
+ int weekdays = searchTimer->getValue("WEEKDAYS")->isNull() ? (int)na : searchTimer->getIntValue("WEEKDAYS");
+
+ switch (searchmode)
+ {
+ case smExact: searchOp = casesensitiv ? "= BINARY" : "="; break;
+ case smRegexp: searchOp = casesensitiv ? "regexp BINARY" : "regexp"; break;
+ case smLike: searchOp = casesensitiv ? "like BINARY" : "like"; break;
+ case smContained: searchOp = casesensitiv ? "like BINARY" : "like"; break;
+ }
+
+ select->build("select ");
+ select->bindAllOut(0, cDBS::ftData | cDBS::ftPrimary, cDBS::ftMeta);
+ select->setBindPrefix("c.");
+ select->bind(mapDb, "FORMAT", cDBS::bndOut, ", ");
+ select->clrBindPrefix();
+ select->build(" from eventsviewplain e, (select distinct channelid,channelname,format,ord,visible from %s) c where ",
+ mapDb->TableName());
+ select->build("e.%s = c.%s",
+ db->getField("CHANNELID")->getDbName(),
+ mapDb->getField("CHANNELID")->getDbName());
+ select->build(" and e.updflg in (%s)", cEventState::getVisible());
+ select->build(" and e.cnt_starttime >= unix_timestamp()-120"); // not more than 2 minutes running
+
+ // search fields 1
+
+ if (!isEmpty(expression) && strcmp(expression, "%") != 0 && strcmp(expression, "%%") != 0 && searchfields)
+ {
+ select->build(" and (");
+
+ for (int i = 0, n = 0; searchField[i]; i++)
+ {
+ if (!db->getField(searchFieldName[i]))
+ tell(0, "Fatal: Search field '%s' not known!", searchFieldName[i]);
+
+ else if (searchfields & searchField[i])
+ {
+ select->build("%s(%s %s '%s%s%s')", n++ ? " or " : "",
+ db->getField(searchFieldName[i])->getDbName(),
+ searchOp,
+ searchmode == smContained ? "%" : "",
+ connection->escapeSqlString(expression).c_str(),
+ searchmode == smContained ? "%" : "");
+ }
+ }
+
+ select->build(")");
+ }
+
+ // search fields 2
+
+ if (!isEmpty(expression1) && strcmp(expression1, "%") != 0 && strcmp(expression1, "%%") != 0 && searchfields1)
+ {
+ select->build(" and (");
+
+ for (int i = 0, n = 0; searchField[i]; i++)
+ {
+ if (!db->getField(searchFieldName[i]))
+ tell(0, "Fatal: Search field '%s' not known!", searchFieldName[i]);
+
+ else if (searchfields1 & searchField[i])
+ {
+ select->build("%s(%s %s '%s%s%s')", n++ ? " or " : "",
+ db->getField(searchFieldName[i])->getDbName(),
+ searchOp,
+ searchmode == smContained ? "%" : "",
+ connection->escapeSqlString(expression1).c_str(),
+ searchmode == smContained ? "%" : "");
+ }
+ }
+
+ select->build(")");
+ }
+
+ // Channel Format (CHFORMAT)
+
+ if (!isEmpty(chformat))
+ {
+ std::string format;
+
+ format = "'" + strReplace(",", "','", chformat) + "'";
+
+ select->build(" and (");
+
+ select->build(" c.%s in (%s) ",
+ mapDb->getField("FORMAT")->getDbName(),
+ format.c_str());
+
+ select->build(")");
+ }
+
+ // Kategorie 'Spielfilm','Serie' (CATEGORY)
+
+ if (!isEmpty(category))
+ {
+ select->build(" and (");
+
+ if (noepgmatch)
+ select->build("%s is null or ", db->getField("CATEGORY")->getDbName());
+
+ select->build(" %s in (%s) ",
+ db->getField("CATEGORY")->getDbName(),
+ category);
+
+ select->build(")");
+ }
+
+ // Genre 'Krimi','Action' (GENRE)
+
+ if (!isEmpty(genre))
+ {
+ select->build(" and (");
+
+ if (noepgmatch)
+ select->build("%s is null or ", db->getField("GENRE")->getDbName());
+
+ select->build(" %s in (%s) ",
+ db->getField("GENRE")->getDbName(),
+ genre);
+
+ select->build(")");
+ }
+
+ // Tipp (TIPP)
+
+ if (!isEmpty(tipp))
+ {
+ select->build(" and (");
+
+ if (noepgmatch)
+ select->build("%s is null or ", db->getField("TIPP")->getDbName());
+
+ select->build(" %s in (%s) ",
+ db->getField("TIPP")->getDbName(),
+ tipp);
+
+ select->build(")");
+ }
+
+ // Serien Titel (EPISODENAME)
+
+ if (!isEmpty(episodename))
+ {
+ select->build(" and (");
+
+ select->build(" %s = '%s' or (%s is null and %s = '%s')",
+ db->getField("EPISODENAME")->getDbName(),
+ connection->escapeSqlString(episodename).c_str(),
+ db->getField("EPISODENAME")->getDbName(),
+ db->getField("TITLE")->getDbName(),
+ connection->escapeSqlString(episodename).c_str());
+
+ select->build(")");
+ }
+
+ // Staffel like 3-5 (SEASON)
+
+ if (!isEmpty(season))
+ {
+ select->build(" and (");
+
+ if (noepgmatch)
+ select->build("%s is null or ", db->getField("EPISODESEASON")->getDbName());
+
+ select->build(" %s between %d and %d ",
+ db->getField("EPISODESEASON")->getDbName(),
+ rangeFrom(season), rangeTo(season));
+
+ select->build(")");
+ }
+
+ // Staffelfolge (SEASONPART)
+
+ if (!isEmpty(seasonpart))
+ {
+ select->build(" and (");
+
+ if (noepgmatch)
+ select->build("%s is null or ", db->getField("EPISODEPART")->getDbName());
+
+ select->build(" %s between %d and %d ",
+ db->getField("EPISODEPART")->getDbName(),
+ rangeFrom(seasonpart), rangeTo(seasonpart));
+
+ select->build(")");
+ }
+
+ // Jahr (YEAR)
+
+ if (!isEmpty(year))
+ {
+ select->build(" and (");
+
+ if (noepgmatch)
+ select->build("%s is null or ", db->getField("YEAR")->getDbName());
+
+ select->build(" %s between %d and %d ",
+ db->getField("YEAR")->getDbName(),
+ rangeFrom(year), rangeTo(year));
+
+ select->build(")");
+ }
+
+ if (weekdays > 0)
+ {
+ select->build(" and ");
+ select->build("(%d & (1 << weekday(from_unixtime(%s)))) <> 0",
+ weekdays, db->getField("STARTTIME")->getDbName());
+ }
+
+ select->build(" order by e.cnt_starttime, c.ord");
+
+ if (select->prepare() != success)
+ {
+ delete select;
+ select = 0;
+ tell(0, "AUTOTIMER: Prepare of statement for searchtimer failed, skipping");
+ return 0;
+ }
+
+ const char* p = strstr(select->asText(), " from ");
+
+ tell(1, "AUTOTIMER: Search statement [%s;]", p ? p : select->asText());
+
+ return select;
+}
+
+//***************************************************************************
+// Match Criterias
+//***************************************************************************
+
+int cSearchTimer::matchCriterias(cDbRow* searchTimer, cDbRow* event)
+{
+ const char* channelids = searchTimer->getStrValue("CHANNELIDS");
+ int chexclude = searchTimer->getIntValue("CHEXCLUDE");
+ int rangeStart = searchTimer->getValue("STARTTIME")->isNull() ? (int)na : searchTimer->getIntValue("STARTTIME");
+ int rangeEnd = searchTimer->getValue("ENDTIME")->isNull() ? (int)na : searchTimer->getIntValue("ENDTIME");
+ int nextDays = searchTimer->getValue("NEXTDAYS")->isNull() ? (int)na : searchTimer->getIntValue("NEXTDAYS");
+
+ const char* channelid = event->getStrValue("CHANNELID");
+ time_t starttime = event->getIntValue("STARTTIME");
+ int hhmm = l2hhmm(starttime);
+
+ // check if channel known to VDRs
+
+ mapDb->clear();
+ mapDb->setValue("CHANNELID", channelid);
+
+ if (!selectChannelFromMap->find() || mapDb->getIntValue("UNKNOWNATVDR") > 0)
+ {
+ mapDb->reset();
+ tell(2, "AUTOTIMER: Skipping hit, channelid '%s' is unknown at least on one VDR!",
+ event->getStrValue("CHANNELID"));
+ return no;
+ }
+
+ mapDb->reset();
+
+ // check channel matches
+
+ if (!isEmpty(channelids))
+ {
+ if (!chexclude && !strstr(channelids, channelid))
+ {
+ tell(2, "AUTOTIMER: Skipping hit due to channelid - '%s' not in '%s'",
+ event->getStrValue("CHANNELID"), channelids);
+ return no;
+ }
+ else if (chexclude && strstr(channelids, channelid))
+ {
+ tell(2, "AUTOTIMER: Skipping hit due to channelid - '%s' it in '%s'",
+ event->getStrValue("CHANNELID"), channelids);
+ return no;
+ }
+ }
+
+ // check start
+
+ if (rangeStart > 0 && hhmm < rangeStart)
+ {
+ tell(2, "AUTOTIMER: Skipping due to range (start before range)");
+ return no;
+ }
+
+ if (rangeEnd > 0 && hhmm > rangeEnd)
+ {
+ tell(2, "AUTOTIMER: Skipping due to range (start behind range)");
+ return no;
+ }
+
+ if (nextDays > 0 && event->getIntValue("STARTTIME") > time(0) + tmeSecondsPerDay * nextDays)
+ {
+ tell(2, "AUTOTIMER: Skipping due to nextdays (start behind range)");
+ return no;
+ }
+
+ // check range ... is start event in nextdays ...
+
+ return yes;
+}
+
+//***************************************************************************
+// Get Search Matches
+//***************************************************************************
+
+int cSearchTimer::getSearchMatches(cDbRow* searchTimer, json_t* obj)
+{
+ cDbStatement* select = 0;
+ long searchtimerid = searchTimer->getIntValue("ID");
+
+ searchtimerDb->clear();
+
+ if (searchtimerid > 0)
+ {
+ searchtimerDb->setValue("ID", searchtimerid);
+
+ if (!searchtimerDb->find())
+ {
+ tell(0, "Warning: Searchtimer %ld not found!", searchtimerid);
+ return fail;
+ }
+
+ searchTimer = searchtimerDb->getRow();
+ }
+
+ if (!(select = prepareSearchStatement(searchTimer, useeventsDb)))
+ return fail;
+
+ json_t* oEvents = json_array();
+
+ useeventsDb->clear();
+
+ for (int res = select->find(); res; res = select->fetch())
+ {
+ cDbStatement* selectDones = 0;
+
+ useeventsDb->find(); // get all fields ..
+
+ if (!matchCriterias(searchTimer, useeventsDb->getRow()))
+ continue;
+
+ // add to json object
+
+ json_t* oData = json_object();
+
+ addFieldToJson(oData, useeventsDb, "USEID", no, "id");
+ addFieldToJson(oData, useeventsDb, "CHANNELID");
+ addFieldToJson(oData, useeventsDb, "STARTTIME");
+ addFieldToJson(oData, useeventsDb, "DURATION");
+ addFieldToJson(oData, useeventsDb, "CATEGORY");
+ addFieldToJson(oData, useeventsDb, "GENRE");
+ addFieldToJson(oData, useeventsDb, "TITLE");
+ addFieldToJson(oData, useeventsDb, "SHORTTEXT");
+ addFieldToJson(oData, useeventsDb, "SHORTDESCRIPTION");
+ addFieldToJson(oData, useeventsDb, "TIPP");
+
+ if (prepareDoneSelect(useeventsDb->getRow(), searchtimerDb->getIntValue("REPEATFIELDS"), selectDones) == success && selectDones)
+ {
+ json_t* oDones = json_array();
+ int cnt = 0;
+
+ for (int f = selectDones->find(); f; f = selectDones->fetch())
+ {
+ json_t* o = json_object();
+
+ long id = timersDoneDb->getIntValue("ID");
+ const char* state = timersDoneDb->getStrValue("STATE");
+
+ // add to json object
+
+ json_object_set_new(o, "id", json_integer(id));
+ json_object_set_new(o, "state", json_string(state));
+ json_array_append_new(oDones, o);
+ cnt++;
+ }
+
+ selectDones->freeResult();
+
+ if (cnt)
+ json_object_set_new(oData, "dones", oDones);
+ }
+
+ json_array_append_new(oEvents, oData);
+ }
+
+ select->freeResult();
+ delete select;
+
+ searchtimerDb->reset();
+
+ json_object_set_new(obj, "events", oEvents);
+
+ return success;
+}
+
+//***************************************************************************
+// Get Done For Event by Searchtimer
+//***************************************************************************
+
+int cSearchTimer::getDoneFor(cDbRow* searchTimer, cDbRow* useevent, json_t* obj)
+{
+ cDbStatement* select = 0;
+
+ searchtimerDb->clear();
+ searchtimerDb->setValue("ID", searchTimer->getIntValue("ID"));
+
+ if (!searchtimerDb->find())
+ {
+ tell(0, "Warning: Searchtimer %ld not found", searchTimer->getIntValue("ID"));
+ return fail;
+ }
+
+ useeventsDb->clear();
+ useeventsDb->setBigintValue("CNTEVENTID", useevent->getBigintValue("CNTEVENTID"));
+ useeventsDb->setValue("CHANNELID", useevent->getStrValue("CHANNELID"));
+ useeventsDb->setValue("CNTSOURCE", useevent->getStrValue("CNTSOURCE"));
+
+ if (!useeventsDb->find())
+ {
+ tell(0, "Warning: Event '%s/%" PRId64 "/%s' not found",
+ useevent->getStrValue("CHANNELID"),
+ useevent->getBigintValue("CNTEVENTID"),
+ useevent->getStrValue("CNTSOURCE"));
+
+ return fail;
+ }
+
+ if (prepareDoneSelect(useeventsDb->getRow(), searchtimerDb->getIntValue("REPEATFIELDS"), select) != success || !select)
+ return success;
+
+ json_t* oDones = json_array();
+ int cnt = 0;
+
+ for (int f = select->find(); f; f = select->fetch())
+ {
+ json_t* oData = json_object();
+
+ long id = timersDoneDb->getIntValue("ID");
+ const char* state = timersDoneDb->getStrValue("STATE");
+
+ // add to json object
+
+ json_object_set_new(oData, "id", json_integer(id));
+ json_object_set_new(oData, "state", json_string(state));
+ json_array_append_new(oDones, oData);
+ cnt++;
+ }
+
+ select->freeResult();
+ useeventsDb->reset();
+ searchtimerDb->reset();
+
+ if (cnt)
+ json_object_set_new(obj, "dones", oDones);
+
+ return success;
+}
+
+//***************************************************************************
+// Update Search Timers
+//***************************************************************************
+
+int cSearchTimer::updateSearchTimers(int force, const char* reason)
+{
+ uint64_t start = cMyTimeMs::Now();
+ long total = 0;
+
+ tell(0, "AUTOTIMER: Updating searchtimers due to '%s' %s", reason, force ? "(force)" : "");
+
+ searchtimerDb->clear();
+
+ for (int res = selectActiveSearchtimers->find(); res; res = selectActiveSearchtimers->fetch())
+ {
+ long hits = 0;
+ cDbStatement* select = 0;
+
+ // searchtimer updated after last run or force?
+
+ if (!force && searchtimerDb->getIntValue("MODSP") <= searchtimerDb->getIntValue("LASTRUN"))
+ continue;
+
+ select = prepareSearchStatement(searchtimerDb->getRow(), useeventsDb);
+
+ if (!select)
+ {
+ lastSearchTimerUpdate = time(0); // protect for infinite call on error
+ return 0;
+ }
+
+ useeventsDb->clear();
+
+ for (int res = select->find(); res; res = select->fetch())
+ {
+ cDbStatement* select = 0;
+ time_t starttime = useeventsDb->getIntValue("STARTTIME");
+ int weekday = weekdayOf(starttime);
+
+ tell(3, "AUTOTIMER: Found event (%s) '%s' / '%s' (%ld/%s) at day %d",
+ l2pTime(starttime).c_str(),
+ useeventsDb->getStrValue("TITLE"),
+ useeventsDb->getStrValue("SHORTTEXT"),
+ useeventsDb->getIntValue("USEID"),
+ useeventsDb->getStrValue("CHANNELID"),
+ weekday);
+
+ useeventsDb->find(); // get all fields ..
+
+ // match
+
+ if (!matchCriterias(searchtimerDb->getRow(), useeventsDb->getRow()))
+ {
+ useeventsDb->reset();
+ continue;
+ }
+
+ // check if event already recorded or schedule for recording
+
+ tell(2, "Check if '%s/%s' already recorded by fields (%ld)",
+ useeventsDb->getStrValue("TITLE"),
+ useeventsDb->getStrValue("SHORTTEXT"),
+ searchtimerDb->getIntValue("REPEATFIELDS"));
+
+ int isDone = no;
+
+ if (prepareDoneSelect(useeventsDb->getRow(), searchtimerDb->getIntValue("REPEATFIELDS"), select) == success && select)
+ {
+ isDone = select->find() ? yes : no;
+ select->freeResult();
+ }
+
+ if (isDone)
+ continue;
+
+ timerDb->clear();
+ timerDb->setValue("EVENTID", useeventsDb->getIntValue("USEID"));
+
+ if (selectTimerByEvent->find())
+ {
+ tell(2, "Timer for event (%ld) '%s/%s' already scheduled, skipping",
+ useeventsDb->getIntValue("USEID"),
+ useeventsDb->getStrValue("TITLE"),
+ useeventsDb->getStrValue("SHORTTEXT"));
+ }
+ else
+ {
+// #TODO time_t lEndTime = useeventsDb->getIntValue("DURATION");
+// #TODO int count = getUsedTransponderAt(useeventsDb->getIntValue("STARTTIME"), lEndTime);
+// #TODO if (count <= availableTransponders)
+ {
+ if (createTimer(searchtimerDb->getIntValue("ID")) == success)
+ hits++;
+ }
+// else
+// tell(1, "AUTOTIMER: Skipping due to");
+ }
+
+ selectTimerByEvent->freeResult();
+ useeventsDb->reset();
+ }
+
+ total += hits;
+
+ select->freeResult();
+ delete select;
+
+ searchtimerDb->find();
+ searchtimerDb->setValue("LASTRUN", time(0));
+
+ if (hits)
+ searchtimerDb->setValue("HITS", searchtimerDb->getIntValue("HITS") + hits);
+
+ searchtimerDb->update();
+ }
+
+ selectActiveSearchtimers->freeResult();
+ lastSearchTimerUpdate = time(0);
+
+ tell(0, "AUTOTIMER: Update done after %s, created %ld timers",
+ ms2Dur(cMyTimeMs::Now()-start).c_str(), total);
+
+ return total;
+}
+
+//***************************************************************************
+// Prepare Done Select
+//***************************************************************************
+
+int cSearchTimer::prepareDoneSelect(cDbRow* useeventsRow, int repeatfields, cDbStatement*& select)
+{
+ std::string chkFields = "";
+ select = 0;
+
+ if (repeatfields <= 0)
+ return success;
+
+ // prepare statement ...
+
+ selectDoneTimer->clear();
+ selectDoneTimer->build("select ");
+ selectDoneTimer->bind("ID", cDBS::bndOut);
+ selectDoneTimer->bind("STATE", cDBS::bndOut, ", ");
+ selectDoneTimer->build(" from %s where ", timersDoneDb->TableName());
+
+ // retry only 'F'ailed and re'J'ected timers, don't retry 'D'eleted timers sice they are deleted by user
+
+ selectDoneTimer->build(" %s not in ('F','J')", // mysql ignoring case by default!
+ timersDoneDb->getField("STATE")->getDbName());
+
+ for (int i = 0; repeadCheckField[i]; i++)
+ {
+ const char* fieldName = repeadCheckFieldName[i];
+
+ if (!timersDoneDb->getField(fieldName))
+ tell(0, "AUTOTIMER: Search (for 'done' check) field '%s' not known!",
+ fieldName);
+
+ else if (repeatfields & repeadCheckField[i])
+ {
+ // specical handling for episode, use EPISODECOMPSHORTNAME instead (if not null)!
+
+ if (repeadCheckField[i] == sfTitle)
+ {
+ if (!useeventsRow->getValue("EPISODECOMPSHORTNAME")->isNull())
+ fieldName = "EPISODECOMPSHORTNAME";
+ if (!useeventsRow->getValue("EPISODECOMPPARTNAME")->isNull())
+ fieldName = "EPISODECOMPPARTNAME";
+ }
+
+ if (repeadCheckField[i] == sfFolge)
+ {
+ if (!useeventsRow->getValue("EPISODECOMPSHORTNAME")->isNull())
+ fieldName = "EPISODECOMPSHORTNAME";
+ }
+
+ selectDoneTimer->bind(timersDoneDb->getField(fieldName),
+ cDBS::bndIn | cDBS::bndSet, " and ");
+
+ chkFields += " " + std::string(fieldName);
+ }
+ }
+
+ if (selectDoneTimer->prepare() != success)
+ {
+ tell(0, "AUTOTIMER: Prepare of statement for 'done' check failed, skipping");
+ return fail;
+ }
+
+ timersDoneDb->clear();
+ timersDoneDb->setValue("COMPTITLE", useeventsRow->getStrValue("COMPTITLE"));
+
+ if (!useeventsRow->getValue("COMPSHORTTEXT")->isEmpty())
+ timersDoneDb->setValue("COMPSHORTTEXT", useeventsRow->getStrValue("COMPSHORTTEXT"));
+
+ if (!useeventsRow->getValue("EPISODECOMPNAME")->isEmpty())
+ timersDoneDb->setValue("EPISODECOMPNAME", useeventsRow->getStrValue("EPISODECOMPNAME"));
+ if (!useeventsRow->getValue("EPISODECOMPSHORTNAME")->isEmpty())
+ timersDoneDb->setValue("EPISODECOMPSHORTNAME", useeventsRow->getStrValue("EPISODECOMPSHORTNAME"));
+
+ if (!useeventsRow->getValue("EPISODECOMPPARTNAME")->isEmpty())
+ timersDoneDb->setValue("EPISODECOMPPARTNAME", useeventsRow->getStrValue("EPISODECOMPPARTNAME"));
+
+ if (!useeventsRow->getValue("COMPLONGDESCRIPTION")->isEmpty())
+ timersDoneDb->setValue("COMPLONGDESCRIPTION", useeventsRow->getStrValue("COMPLONGDESCRIPTION"));
+
+ select = selectDoneTimer;
+
+ return success;
+}
+
+//***************************************************************************
+// Create Timer
+//
+// - searchtimerDb and useeventsDb has to be positioned and loaded
+//***************************************************************************
+
+int cSearchTimer::createTimer(int id)
+{
+ int status;
+ int timerid;
+
+ const char* channelname = "";
+ int namingmode = searchtimerDb->getIntValue("NAMINGMODE");
+ const char* tmplExpression = timerDb->getStrValue("TEMPLATE");
+
+ // ------------------------------------------
+ // lookup channel name to store in timersdone
+ // (just as a additional 'debug' info)
+
+ mapDb->clear();
+ mapDb->setValue("CHANNELID", useeventsDb->getStrValue("CHANNELID"));
+
+ if (selectChannelFromMap->find())
+ channelname = mapDb->getStrValue("CHANNELNAME");
+
+ if (isEmpty(channelname))
+ channelname = useeventsDb->getStrValue("CHANNELID");
+
+ selectChannelFromMap->freeResult();
+
+ // ------------------------------------------
+ // Create timer in timerdistribution ...
+
+ cDbRow timerRow("timers");
+
+ timerRow.clear();
+
+ timerRow.setValue("ID", na); // 'na' is the signal for modifyCreateTimer to create new timer
+ timerRow.setValue("ACTIVE", yes);
+ timerRow.setValue("TYPE", searchtimerDb->getStrValue("TYPE"));
+ timerRow.setValue("SOURCE", "epgd");
+ timerRow.setValue("EVENTID", useeventsDb->getIntValue("USEID"));
+ timerRow.setValue("CHANNELID", useeventsDb->getStrValue("CHANNELID"));
+ timerRow.setValue("NAMINGMODE", namingmode);
+ timerRow.setValue("TEMPLATE", tmplExpression);
+ timerRow.setValue("CHILDLOCK", searchtimerDb->getIntValue("CHILDLOCK"));
+ timerRow.setValue("VDRUUID", searchtimerDb->getStrValue("VDRUUID"));
+ timerRow.setValue("VPS", searchtimerDb->getIntValue("VPS"));
+ timerRow.setValue("PRIORITY", searchtimerDb->getIntValue("PRIORITY"));
+ timerRow.setValue("LIFETIME", searchtimerDb->getIntValue("LIFETIME"));
+ timerRow.setValue("DIRECTORY", searchtimerDb->getStrValue("DIRECTORY"));
+
+ timerRow.setValue("AUTOTIMERID", searchtimerDb->getIntValue("ID"));
+ timerRow.setValue("AUTOTIMERNAME", searchtimerDb->getStrValue("NAME"));
+ timerRow.setValue("AUTOTIMERINSSP", searchtimerDb->getIntValue("INSSP"));
+ timerRow.setValue("EXPRESSION", searchtimerDb->getStrValue("EXPRESSION"));
+
+ if (ptyRecName && namingmode != tnmDefault)
+ {
+ // execupe python - calc recording name
+
+ if (ptyRecName->execute(useeventsDb, namingmode, tmplExpression) == success)
+ {
+ tell(0, "Info: The recording name calculated by 'recording.py' is '%s'",
+ ptyRecName->getResult());
+
+ if (!isEmpty(ptyRecName->getResult()))
+ timerRow.setValue("FILE", ptyRecName->getResult());
+ }
+ }
+
+ status = modifyCreateTimer(&timerRow, timerid);
+
+ // on scuccess add to timersdone ..
+
+ if (status == success)
+ {
+ timersDoneDb->clear();
+ timersDoneDb->setCharValue("STATE", tdsTimerRequested);
+ timersDoneDb->setValue("SOURCE", "epgd");
+ timersDoneDb->setValue("AUTOTIMERID", id);
+ timersDoneDb->setValue("TIMERID", timerid);
+ timersDoneDb->setValue("TITLE", useeventsDb->getStrValue("TITLE"));
+ timersDoneDb->setValue("COMPTITLE", useeventsDb->getStrValue("COMPTITLE"));
+
+ if (!useeventsDb->getValue("SHORTTEXT")->isEmpty())
+ timersDoneDb->setValue("SHORTTEXT", useeventsDb->getStrValue("SHORTTEXT"));
+ if (!useeventsDb->getValue("COMPSHORTTEXT")->isEmpty())
+ timersDoneDb->setValue("COMPSHORTTEXT", useeventsDb->getStrValue("COMPSHORTTEXT"));
+
+ if (!useeventsDb->getValue("LONGDESCRIPTION")->isEmpty())
+ timersDoneDb->setValue("LONGDESCRIPTION", useeventsDb->getStrValue("LONGDESCRIPTION"));
+ if (!useeventsDb->getValue("COMPLONGDESCRIPTION")->isEmpty())
+ timersDoneDb->setValue("COMPLONGDESCRIPTION", useeventsDb->getStrValue("COMPLONGDESCRIPTION"));
+
+ if (!useeventsDb->getValue("EPISODECOMPNAME")->isEmpty())
+ timersDoneDb->setValue("EPISODECOMPNAME", useeventsDb->getStrValue("EPISODECOMPNAME"));
+ if (!useeventsDb->getValue("EPISODECOMPSHORTNAME")->isEmpty())
+ timersDoneDb->setValue("EPISODECOMPSHORTNAME", useeventsDb->getStrValue("EPISODECOMPSHORTNAME"));
+ if (!useeventsDb->getValue("EPISODECOMPPARTNAME")->isEmpty())
+
+ timersDoneDb->setValue("EPISODECOMPPARTNAME", useeventsDb->getStrValue("EPISODECOMPPARTNAME"));
+ if (!useeventsDb->getValue("EPISODELANG")->isEmpty())
+ timersDoneDb->setValue("EPISODELANG", useeventsDb->getStrValue("EPISODELANG"));
+ if (!useeventsDb->getValue("EPISODESEASON")->isEmpty())
+ timersDoneDb->setValue("EPISODESEASON", useeventsDb->getIntValue("EPISODESEASON"));
+ if (!useeventsDb->getValue("EPISODEPART")->isEmpty())
+ timersDoneDb->setValue("EPISODEPART", useeventsDb->getIntValue("EPISODEPART"));
+
+ timersDoneDb->setValue("CHANNELID", useeventsDb->getStrValue("CHANNELID"));
+ timersDoneDb->setValue("STARTTIME", useeventsDb->getIntValue("STARTTIME"));
+ timersDoneDb->setValue("DURATION", useeventsDb->getIntValue("DURATION"));
+ timersDoneDb->setValue("EXPRESSION", searchtimerDb->getStrValue("EXPRESSION"));
+
+ if (!isEmpty(channelname))
+ timersDoneDb->setValue("CHANNELNAME", channelname);
+
+ timersDoneDb->insert();
+
+ int doneid = timersDoneDb->getLastInsertId();
+
+ tell(0, "AUTOTIMER: Created timer request (%d) for (%s) '%s' / '%s' on channel '%s' at %s with doneid (%d)",
+ timerid,
+ l2pTime(useeventsDb->getIntValue("STARTTIME")).c_str(),
+ useeventsDb->getStrValue("TITLE"), useeventsDb->getStrValue("SHORTTEXT"),
+ channelname, toWeekdayName(weekdayOf(useeventsDb->getIntValue("STARTTIME"))),
+ doneid);
+
+ timerDb->clear();
+ timerDb->setValue("ID", timerid);
+ timerDb->setValue("VDRUUID", searchtimerDb->getStrValue("VDRUUID"));
+
+ if (timerDb->find())
+ {
+ timerDb->setValue("DONEID", doneid);
+ timerDb->update();
+ }
+ }
+
+ return status;
+}
+
+//***************************************************************************
+// Modify Timer (copy paste from cMenuDb of epg2vdr/httpd)
+//
+// - timerRow contains the destination vdrUuid
+//***************************************************************************
+
+int cSearchTimer::modifyCreateTimer(cDbRow* timerRow, int& newid)
+{
+ int status = success;
+ int timerid = timerRow->getIntValue("ID");
+ int knownTimer = timerid != na;
+ int move = no;
+
+ newid = na;
+ connection->startTransaction();
+
+ timerDb->clear();
+
+ // lookup known (existing) timer
+
+ if (knownTimer)
+ {
+ timerDb->copyValues(timerRow, cDBS::ftPrimary);
+
+ if (!timerDb->find())
+ {
+ connection->commit();
+
+ tell(0, "Timer (%d) at vdr '%s' not found, aborting modify request!",
+ timerid, timerDb->getStrValue("VDRUUID"));
+
+ return fail;
+ }
+
+ // found and all values are loaded!
+
+ // move to another vdr?
+
+ if (!timerDb->hasValue("VDRUUID", timerRow->getStrValue("VDRUUID")))
+ move = yes;
+ }
+ else
+ {
+ timerDb->setValue("VDRUUID", timerRow->getStrValue("VDRUUID"));
+ }
+
+ if (move)
+ {
+ // request 'D'elete of 'old' timer
+
+ timerDb->setCharValue("ACTION", taDelete);
+ timerDb->setValue("SOURCE", timerRow->getStrValue("SOURCE"));
+ status = timerDb->update();
+
+ // triggerVdrs("TIMERJOB", timerDb->getStrValue("VDRUUID"));
+
+ // create new on other vdr
+
+ timerDb->copyValues(timerRow, cDBS::ftData); // takeover all data (can be modified by user)
+ timerDb->setValue("ID", 0);
+ timerDb->setCharValue("ACTION", taCreate);
+ status += timerDb->insert();
+ newid = timerDb->getLastInsertId();
+
+ if (status == success)
+ tell(0, "Created 'move' request for timer (%d) at vdr '%s'",
+ timerid, timerDb->getStrValue("VDRUUID"));
+ }
+ else
+ {
+ // create 'C'reate oder 'M'odify request ...
+
+ timerDb->copyValues(timerRow, cDBS::ftData);
+
+ timerDb->setCharValue("ACTION", knownTimer ? taModify : taCreate);
+
+ // if (!knownTimer)
+ // timerDb->setValue("NAMINGMODE", tnmDefault);
+
+ if (knownTimer)
+ {
+ status = timerDb->update();
+ newid = timerid;
+ }
+ else
+ {
+ status = timerDb->insert();
+ newid = timerDb->getLastInsertId();
+ }
+
+ if (status == success)
+ {
+ tell(0, "Created '%s' request for timer (%d), event (%ld) at vdr '%s' by autotimer (%ld)",
+ knownTimer ? "modify" : "create",
+ newid, timerDb->getIntValue("EVENTID"), timerDb->getStrValue("VDRUUID"),
+ timerDb->getIntValue("AUTOTIMERID"));
+ }
+ }
+
+ connection->commit();
+
+ // triggerVdrs("TIMERJOB", timerDb->getStrValue("VDRUUID"));
+
+ return status;
+}
+
+//***************************************************************************
+// Check Timer Conficts
+//***************************************************************************
+/*
+int cSearchTimer::checkTimerConflicts(std::string& mailBody)
+{
+ int conflicts = 0;
+
+ tell(0, "TCC: Starting timer conflict check");
+
+ timerDb->clear();
+ vdrDb->clear();
+
+ for (int f = selectAllTimer->find(); f; f = selectAllTimer->fetch())
+ {
+ if (!timerDb->getIntValue("ACTIVE"))
+ continue;
+
+ if (timerDb->getIntValue("TCCMAILCNT") > 0)
+ continue;
+
+ tell(2, "TCC: Check conflicts for timer (%ld) '%s' on '%s'",
+ timerDb->getIntValue("ID"), timerDb->getStrValue("FILE"),
+ vdrDb->getStrValue("NAME"));
+
+ // calc 'start' and 'end' time of this timer ..
+
+ int sDay = timerDb->getIntValue("DAY");
+ int sTime = timerDb->getIntValue("STARTTIME");
+ int eDay = timerDb->getIntValue("DAY");
+ int eTime = timerDb->getIntValue("ENDTIME");
+
+ if (eTime < sTime)
+ eDay += tmeSecondsPerDay;
+
+ time_t lStartTime = sDay + sTime / 100 * tmeSecondsPerHour + sTime % 100 * tmeSecondsPerMinute;
+ time_t lEndTime = eDay + eTime / 100 * tmeSecondsPerHour + eTime % 100 * tmeSecondsPerMinute;
+
+ // check for conflicts
+
+ std::string mailPart;
+ int tunerCount = getUsedTransponderAt(lStartTime, lEndTime, mailPart);
+
+ if (tunerCount > vdrDb->getIntValue("TUNERCOUNT"))
+ {
+ conflicts++;
+
+ tell(0, "TCC: Timer (%ld) '%s' conflict at '%s - %s' needed %d transponders on '%s'",
+ timerDb->getIntValue("ID"),
+ timerDb->getStrValue("FILE"),
+ l2pTime(lStartTime).c_str(),
+ l2pTime(lEndTime).c_str(),
+ tunerCount,
+ vdrDb->getStrValue("NAME"));
+
+ mailBody += " <tr>\n"
+ " <th><font face=\"Arial\"> conflict #" + num2Str(conflicts)
+ + "on" + vdrDb->getStrValue("NAME")
+ + " </font></th>\n"
+ " </tr>\n";
+
+ mailBody += mailPart;
+
+ timerDb->setValue("TCCMAILCNT", timerDb->getIntValue("TCCMAILCNT") + 1);
+ timerDb->update();
+
+ // #TODO - reject autotimer ... ?
+ // rejectTimer(timerDb->getRow());
+ }
+
+ else if (tunerCount <= 0) // DEBUG-tell - to be removed?
+ tell(0, "TCC: Fatal got 0 used transponders for timer (%ld) between %s (%ld) and %s (%ld)",
+ timerDb->getIntValue("ID"),
+ l2pTime(lStartTime).c_str(), lStartTime,
+ l2pTime(lEndTime).c_str(), lEndTime);
+ }
+
+ selectAllTimer->freeResult();
+
+ tell(0, "TCC: Finished timer conflict check");
+
+ return conflicts;
+}
+*/
+//***************************************************************************
+// Get Used Transponder At
+//***************************************************************************
+/*
+int cSearchTimer::getUsedTransponderAt(time_t lStartTime, time_t lEndTime, std::string& mailPart)
+{
+ char buf[1024+TB];
+ int count = 0;
+
+ startValue.setValue(lStartTime);
+ endValue.setValue(lEndTime);
+
+ // #TODO
+ // this loop don't get all timers at this time and VDR but the conflicting
+ // to add all timer to the mail a inner select loop for each found transponder is needed!
+
+ for (int f = selectConflictingTimers->find(); f; f = selectConflictingTimers->fetch())
+ {
+ count++;
+
+ tell(3, "TCC: found '%s'", timerDb->getStrValue("FILE"));
+
+ sprintf(buf,
+ " <tr>"
+ "<td><font face=\"Arial\">%ld</font></td>"
+ "<td><font face=\"Arial\">%s</font></td>"
+ "<td><font face=\"Arial\">%s</font></td>"
+ "<td><font face=\"Arial\">%s</font></td>"
+ "<td><font face=\"Arial\">%s</font></td>"
+ "</tr>\n",
+ timerDb->getIntValue("ID"),
+ timerDb->getStrValue("FILE"),
+ l2pTime(lStartTime, "%d. %b %H:%M").c_str(),
+ l2pTime(lEndTime, "%H:%M").c_str(),
+ vdrDb->getStrValue("NAME"));
+
+ mailPart += buf;
+ }
+
+ selectConflictingTimers->freeResult();
+
+ return count;
+}
+*/
+// //***************************************************************************
+// // Reject Timer
+// //***************************************************************************
+
+// int cSearchTimer::rejectTimer(cDbRow* timerRow)
+// {
+// tell(0, "Rejecting timer (%ld)", timerRow->getIntValue("ID"));
+
+// timerJobsDb->clear();
+// timerJobsDb->setValue("TIMERID", timerRow->getIntValue("ID"));
+// timerJobsDb->setValue("DONEID", timerRow->getIntValue("DONEID"));
+// timerJobsDb->setValue("STATE", "J");
+// timerJobsDb->setValue("ASSUMED", "N");
+// timerJobsDb->insert();
+
+// tell(0, "Created delete job for timer (%ld)", timerRow->getIntValue("ID"));
+
+// return success;
+// }
diff --git a/lib/searchtimer.h b/lib/searchtimer.h
new file mode 100644
index 0000000..ecd3624
--- /dev/null
+++ b/lib/searchtimer.h
@@ -0,0 +1,86 @@
+/*
+ * searchtimer.h
+ *
+ * See the README file for copyright information
+ *
+ */
+
+#ifndef __SEARCHTIMER_H
+#define __SEARCHTIMER_H
+
+#include "common.h"
+#include "db.h"
+#include "epgservice.h"
+#include "json.h"
+
+class Python;
+
+//***************************************************************************
+// Search Timer
+//***************************************************************************
+
+class cSearchTimer
+{
+ public:
+
+ cSearchTimer();
+ ~cSearchTimer();
+
+ int init(const char* confDir);
+ int initDb();
+ int exitDb();
+
+ int modified(); // check if a search timer is modified by user
+ int updateSearchTimers(int force = yes, const char* reason = "");
+
+ int getSearchMatches(cDbRow* searchTimer, json_t* obj);
+ int getDoneFor(cDbRow* searchTimer, cDbRow* useevent, json_t* obj);
+ // int checkTimerConflicts(std::string& mailBody);
+ // int getUsedTransponderAt(time_t lStartTime, time_t lEndTime, std::string& mailBody);
+
+ int prepareDoneSelect(cDbRow* useeventsRow, int repeatfields, cDbStatement*& select);
+ cDbStatement* prepareSearchStatement(cDbRow* searchTimer, cDbTable* db);
+ int matchCriterias(cDbRow* searchTimer, cDbRow* event);
+
+ private:
+
+ int createTimer(int id);
+ int modifyCreateTimer(cDbRow* timerRow, int& newid);
+ // int rejectTimer(cDbRow* timerRow);
+
+ // data
+
+ Python* ptyRecName;
+
+ cDbConnection* connection;
+
+ cDbTable* searchtimerDb;
+ cDbTable* useeventsDb;
+ cDbTable* timersDoneDb;
+ cDbTable* timerDb;
+ cDbTable* mapDb;
+ cDbTable* vdrDb;
+
+ cDbStatement* selectChannelFromMap;
+ cDbStatement* selectDoneTimer;
+ cDbStatement* selectActiveSearchtimers;
+ cDbStatement* selectSearchtimerMaxModSp;
+ cDbStatement* selectActiveVdrs;
+ cDbStatement* selectAllTimer;
+ cDbStatement* selectTimerByEvent;
+ // cDbStatement* selectConflictingTimers;
+
+ cDbValue startValue;
+ cDbValue endValue;
+
+ time_t lastSearchTimerUpdate;
+
+ static int searchField[];
+ static const char* searchFieldName[];
+ static int repeadCheckField[];
+ static const char* repeadCheckFieldName[];
+
+};
+
+//***************************************************************************
+#endif // __SEARCHTIMER_H
diff --git a/lib/semtst.c b/lib/semtst.c
new file mode 100644
index 0000000..68a784c
--- /dev/null
+++ b/lib/semtst.c
@@ -0,0 +1,42 @@
+
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "common.h"
+
+const char* logPrefix = "";
+
+int main()
+{
+
+ Sem s(0x3db00001);
+
+ if (s.check() == success)
+ printf("free \n");
+ else
+ printf("locked \n");
+
+ printf("inc\n");
+
+ s.inc();
+
+ printf("inc done\n");
+ sleep(5);
+
+ s.inc();
+
+ printf("inc done\n");
+ sleep(5);
+
+ s.v();
+ s.v();
+
+ if (s.check() == success)
+ printf("free \n");
+ else
+ printf("locked \n");
+
+ return 0;
+}
diff --git a/lib/test.c b/lib/test.c
new file mode 100644
index 0000000..e8a0001
--- /dev/null
+++ b/lib/test.c
@@ -0,0 +1,756 @@
+/*
+ * test.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <stdint.h> // uint_64_t
+#include <sys/time.h>
+#include <time.h>
+
+#include <stdio.h>
+#include <string>
+
+#include "config.h"
+#include "common.h"
+#include "db.h"
+#include "epgservice.h"
+#include "dbdict.h"
+//#include "wol.h"
+
+cDbConnection* connection = 0;
+const char* logPrefix = "";
+
+//***************************************************************************
+// Init Connection
+//***************************************************************************
+
+void initConnection()
+{
+ cDbConnection::init();
+
+ cDbConnection::setHost("10.241.0.3");
+ //cDbConnection::setHost("localhost");
+ cDbConnection::setPort(3306);
+
+ cDbConnection::setName("epg2vdr");
+ cDbConnection::setUser("epg2vdr");
+ cDbConnection::setPass("epg");
+
+ cDbConnection::setConfPath("/etc/epgd/");
+ cDbConnection::setEncoding("utf8");
+
+ connection = new cDbConnection();
+}
+
+void exitConnection()
+{
+ cDbConnection::exit();
+
+ if (connection)
+ delete connection;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+void chkCompress()
+{
+ std::string s = "_+*!#?=&%$< Hallo TEIL Hallo Folge ";
+
+ printf("'%s'\n", s.c_str());
+ prepareCompressed(s);
+ printf("'%s'\n", s.c_str());
+
+ s = "Place Vendôme - Heiße Diamanten";
+ printf("'%s'\n", s.c_str());
+ prepareCompressed(s);
+ printf("'%s'\n", s.c_str());
+
+ s = "Halöö älter";
+ printf("'%s'\n", s.c_str());
+ prepareCompressed(s);
+ printf("'%s'\n", s.c_str());
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+void chkStatement1()
+{
+ cDbTable* epgDb = new cDbTable(connection, "events");
+
+ if (epgDb->open() != success)
+ {
+ tell(0, "Could not access database '%s:%d' (%s)",
+ cDbConnection::getHost(), cDbConnection::getPort(), epgDb->TableName());
+
+ return ;
+ }
+
+ tell(0, "---------------------------------------------------");
+
+ // prepare statement to mark wasted DVB events
+
+ cDbValue* endTime = new cDbValue("starttime+duration", cDBS::ffInt, 10);
+ cDbStatement* updateDelFlg = new cDbStatement(epgDb);
+
+ // update events set delflg = ?, updsp = ?
+ // where channelid = ? and source = ?
+ // and starttime+duration > ?
+ // and starttime < ?
+ // and (tableid > ? or (tableid = ? and version <> ?))
+
+ updateDelFlg->build("update %s set ", epgDb->TableName());
+ updateDelFlg->bind(epgDb->getField("DelFlg"), cDBS::bndIn | cDBS::bndSet);
+ updateDelFlg->bind(epgDb->getField("UpdSp"), cDBS::bndIn | cDBS::bndSet, ", ");
+ updateDelFlg->build(" where ");
+ updateDelFlg->bind(epgDb->getField("ChannelId"), cDBS::bndIn | cDBS::bndSet);
+ updateDelFlg->bind(epgDb->getField("Source"), cDBS::bndIn | cDBS::bndSet, " and ");
+
+ updateDelFlg->bindCmp(0, endTime, ">", " and ");
+
+ updateDelFlg->bindCmp(0, epgDb->getField("StartTime"), 0, "<" , " and ");
+ updateDelFlg->bindCmp(0, epgDb->getField("TableId"), 0, ">" , " and (");
+ updateDelFlg->bindCmp(0, epgDb->getField("TableId"), 0, "=" , " or (");
+ updateDelFlg->bindCmp(0, epgDb->getField("Version"), 0, "<>" , " and ");
+ updateDelFlg->build("));");
+
+ updateDelFlg->prepare();
+
+ tell(0, "---------------------------------------------------");
+}
+
+// //***************************************************************************
+// //
+// //***************************************************************************
+
+// void chkStatement2()
+// {
+// cDbTable* imageRefDb = new cTableImageRefs(connection);
+// cDbTable* imageDb = new cTableImages(connection);
+
+// if (imageRefDb->open() != success)
+// return ;
+
+// if (imageDb->open() != success)
+// return ;
+
+// tell(0, "---------------------------------------------------");
+
+// cDbStatement* selectAllImages = new cDbStatement(imageRefDb);
+
+// cDbValue imageData;
+// imageData.setField(imageDb->getField(cTableImages::fiImage));
+
+// // select r.imagename, r.eventid, r.lfn, i.image from imagerefs r, images i
+// // where r.imagename = i.imagename and i.image is not null;
+
+// selectAllImages->build("select ");
+// selectAllImages->setBindPrefix("r.");
+// selectAllImages->bind(cTableImageRefs::fiImgName, cDBS::bndOut);
+// selectAllImages->bind(cTableImageRefs::fiEventId, cDBS::bndOut, ", ");
+// selectAllImages->bind(cTableImageRefs::fiLfn, cDBS::bndOut, ", ");
+// selectAllImages->setBindPrefix("i.");
+// selectAllImages->bind(&imageData, cDBS::bndOut, ",");
+// selectAllImages->clrBindPrefix();
+// selectAllImages->build(" from %s r, %s i where ", imageRefDb->TableName(), imageDb->TableName());
+// selectAllImages->build("r.%s = i.%s and i.%s is not null;",
+// imageRefDb->getField(cTableImageRefs::fiImgName)->name,
+// imageDb->getField(cTableImages::fiImgName)->name,
+// imageDb->getField(cTableImages::fiImage)->name);
+
+// selectAllImages->prepare();
+
+
+// tell(0, "---------------------------------------------------");
+
+// //delete s;
+// delete imageRefDb;
+// delete imageDb;
+// }
+
+// //***************************************************************************
+// //
+// //***************************************************************************
+
+// void chkStatement3()
+// {
+// int count = 0;
+// int lcount = 0;
+
+// cDbTable* epgDb = new cTableEvents(connection);
+// cDbTable* mapDb = new cTableChannelMap(connection);
+
+// if (epgDb->open() != success)
+// return ;
+
+// if (mapDb->open() != success)
+// return ;
+
+// tell(0, "---------------------------------------------------");
+
+// cDbStatement* s = new cDbStatement(epgDb);
+
+// s->build("select ");
+// s->setBindPrefix("e.");
+// s->bind(cTableEvents::fiEventId, cDBS::bndOut);
+// s->bind(cTableEvents::fiChannelId, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiSource, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiDelFlg, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiFileRef, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiTableId, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiVersion, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiTitle, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiShortText, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiStartTime, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiDuration, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiParentalRating, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiVps, cDBS::bndOut, ", ");
+// s->bind(cTableEvents::fiDescription, cDBS::bndOut, ", ");
+// s->clrBindPrefix();
+// s->build(" from eventsview e, %s m where ", mapDb->TableName());
+// s->build("e.%s = m.%s and e.%s = m.%s and ",
+// epgDb->getField(cTableEvents::fiChannelId)->name,
+// mapDb->getField(cTableChannelMap::fiChannelName)->name,
+// epgDb->getField(cTableEvents::fiSource)->name,
+// mapDb->getField(cTableChannelMap::fiSource)->name);
+// s->bindCmp("e", cTableEvents::fiUpdSp, 0, ">");
+// s->build(" order by m.%s;", mapDb->getField(cTableChannelMap::fiChannelName)->name);
+
+// s->prepare();
+
+// epgDb->clear();
+// epgDb->setValue(cTableEvents::fiUpdSp, (double)0);
+// epgDb->setValue(cTableEvents::fiSource, "vdr"); // used by selectUpdEventsByChannel
+// epgDb->setValue(cTableEvents::fiChannelId, "xxxxxxxxxxxxx"); // used by selectUpdEventsByChannel
+
+// int channels = 0;
+// char chan[100]; *chan = 0;
+
+// tell(0, "---------------------------------------------------");
+
+// for (int found = s->find(); found; found = s->fetch())
+// {
+// if (!*chan || strcmp(chan, epgDb->getStrValue(cTableEvents::fiChannelId)) != 0)
+// {
+// if (*chan)
+// tell(0, "processed %-20s with %d events", chan, count - lcount);
+
+// lcount = count;
+// channels++;
+// strcpy(chan, epgDb->getStrValue(cTableEvents::fiChannelId));
+
+// tell(0, "processing %-20s now", chan);
+// }
+
+// tell(0, "-> '%s' - (%ld)", epgDb->getStrValue(cTableEvents::fiChannelId),
+// epgDb->getIntValue(cTableEvents::fiEventId));
+
+
+// count++;
+// }
+
+// s->freeResult();
+
+// tell(0, "---------------------------------------------------");
+// tell(0, "updated %d channels and %d events", channels, count);
+// tell(0, "---------------------------------------------------");
+
+// delete s;
+// delete epgDb;
+// delete mapDb;
+// }
+
+// //***************************************************************************
+// //
+// //***************************************************************************
+
+// void chkStatement4()
+// {
+// cDbTable* eventDb = new cTableEvents(connection);
+// if (eventDb->open() != success) return;
+
+// cDbTable* imageRefDb = new cTableImageRefs(connection);
+// if (imageRefDb->open() != success) return;
+
+// cDbTable* imageDb = new cTableImages(connection);
+// if (imageDb->open() != success) return;
+
+// // select e.masterid, r.imagename, r.eventid, r.lfn, i.image
+// // from imagerefs r, images i, events e
+// // where r.imagename = i.imagename
+// // and e.eventid = r.eventid,
+// // and i.image is not null
+// // and (i.updsp > ? or r.updsp > ?);
+
+// cDBS::FieldDef masterFld = { "masterid", cDBS::ffUInt, 0, 999, cDBS::ftData };
+// cDbValue masterId;
+// cDbValue imageData;
+// cDbValue imageUpdSp;
+
+// masterId.setField(&masterFld);
+// imageData.setField(imageDb->getField(cTableImages::fiImage));
+// imageUpdSp.setField(imageDb->getField(cTableImages::fiUpdSp));
+
+// cDbStatement* selectAllImages = new cDbStatement(imageRefDb);
+
+// selectAllImages->build("select ");
+// selectAllImages->setBindPrefix("e.");
+// selectAllImages->bind(&masterId, cDBS::bndOut);
+// selectAllImages->setBindPrefix("r.");
+// selectAllImages->bind(cTableImageRefs::fiImgName, cDBS::bndOut, ", ");
+// selectAllImages->bind(cTableImageRefs::fiEventId, cDBS::bndOut, ", ");
+// selectAllImages->bind(cTableImageRefs::fiLfn, cDBS::bndOut, ", ");
+// selectAllImages->setBindPrefix("i.");
+// selectAllImages->bind(&imageData, cDBS::bndOut, ", ");
+// selectAllImages->clrBindPrefix();
+// selectAllImages->build(" from %s r, %s i, %s e where ",
+// imageRefDb->TableName(), imageDb->TableName(), eventDb->TableName());
+// selectAllImages->build("e.%s = r.%s and i.%s = r.%s and i.%s is not null and (",
+// eventDb->getField(cTableEvents::fiEventId)->name,
+// imageRefDb->getField(cTableImageRefs::fiEventId)->name,
+// imageDb->getField(cTableImageRefs::fiImgName)->name,
+// imageRefDb->getField(cTableImageRefs::fiImgName)->name,
+// imageDb->getField(cTableImages::fiImage)->name);
+// selectAllImages->bindCmp("i", &imageUpdSp, ">");
+// selectAllImages->build(" or ");
+// selectAllImages->bindCmp("r", cTableImageRefs::fiUpdSp, 0, ">");
+// selectAllImages->build(");");
+
+// selectAllImages->prepare();
+
+// imageRefDb->clear();
+// imageRefDb->setValue(cTableImageRefs::fiUpdSp, 1377733333L);
+// imageUpdSp.setValue(1377733333L);
+
+// int count = 0;
+// for (int res = selectAllImages->find(); res; res = selectAllImages->fetch())
+// {
+// count ++;
+// }
+// tell(0,"%d", count);
+// }
+
+// //***************************************************************************
+// //
+// //***************************************************************************
+
+// int structure()
+// {
+// cDbTable* table = new cTableEvents(connection);
+// //cDbTable* table = new cTableVdrs(connection);
+
+// if (table->open(yes) != success)
+// return fail;
+
+// // table->validateStructure();
+
+// delete table;
+
+// return done;
+// }
+
+//***************************************************************************
+// Content Of
+//***************************************************************************
+
+int contentOf(char* buf, const char* tag, const char* xml)
+{
+ std::string sTag = "<" + std::string(tag) + ">";
+ std::string eTag = "</" + std::string(tag) + ">";
+
+ const char* s;
+ const char* e;
+
+ *buf = 0;
+
+ if ((s = strstr(xml, sTag.c_str())) && (e = strstr(xml, eTag.c_str())))
+ {
+ s += strlen(sTag.c_str());
+
+ sprintf(buf, "%.*s", (int)(e-s), s);
+
+ return success;
+ }
+
+ return fail;
+}
+
+//***************************************************************************
+// Get Timer Id Of
+//***************************************************************************
+
+long getTimerIdOf(const char* aux)
+{
+ char epgaux[1000+TB];
+ char tid[100+TB];
+
+ if (isEmpty(aux))
+ return na;
+
+ if (contentOf(epgaux, "epgd", aux) != success)
+ return na;
+
+ if (contentOf(tid, "timerid", epgaux) != success)
+ return na;
+
+ return atol(tid);
+}
+
+//***************************************************************************
+// Remove Tag
+//***************************************************************************
+
+void removeTag(char* xml, const char* tag)
+{
+ std::string sTag = "<" + std::string(tag) + ">";
+ std::string eTag = "</" + std::string(tag) + ">";
+
+ const char* s;
+ const char* e;
+
+ if ((s = strstr(xml, sTag.c_str())) && (e = strstr(xml, eTag.c_str())))
+ {
+ char tmp[1000+TB];
+
+ e += strlen(eTag.c_str());
+
+ // sicher ist sicher ;)
+
+ if (e <= s)
+ return;
+
+ sprintf(tmp, "%.*s%s", int(s-xml), xml, e);
+
+ strcpy(xml, tmp);
+ }
+}
+
+//***************************************************************************
+// Insert Tag
+//***************************************************************************
+
+int insertTag(char* xml, const char* parent, const char* tag, int value)
+{
+ char tmp[1000+TB];
+ std::string sTag = "<" + std::string(parent) + ">";
+ const char* s;
+
+ if ((s = strstr(xml, sTag.c_str())))
+ {
+ s += strlen(sTag.c_str());
+ sprintf(tmp, "%.*s<%s>%d</%s>%s", int(s-xml), xml, tag, value, tag, s);
+ }
+ else
+ {
+ sprintf(tmp, "%s<%s><%s>%d</%s></%s>", xml, parent, tag, value, tag, parent);
+ }
+
+ strcpy(xml, tmp);
+
+ return success;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+void statementrecording()
+{
+ int insert;
+
+ tell(0, "---------------------------------");
+
+ cDbTable* recordingListDb = new cDbTable(connection, "recordinglist");
+ if (recordingListDb->open() != success) return ;
+
+ recordingListDb->clear();
+
+#ifdef USEMD5
+ md5Buf md5path;
+ createMd5("rec->FileName() dummy", md5path);
+ recordingListDb->setValue("MD5PATH", md5path);
+#else
+ recordingListDb->setValue("MD5PATH", "dummy");
+#endif
+
+ recordingListDb->setValue("OWNER", "me");
+ recordingListDb->setValue("STARTTIME", 12121212);
+
+ insert = !recordingListDb->find();
+ recordingListDb->clearChanged();
+
+ tell(0, "#1 %d changes", recordingListDb->getChanges());
+
+ // recordingListDb->setValue("STATE", "E");
+ recordingListDb->getValue("STATE")->setNull();
+ recordingListDb->setValue("PATH", "rec->FileName()");
+ recordingListDb->setValue("TITLE", "title");
+ recordingListDb->setValue("SHORTTEXT", "subTitle");
+ // recordingListDb->setValue("DESCRIPTION", "description");
+
+ recordingListDb->setValue("DURATION", 120*60);
+ recordingListDb->setValue("EVENTID", 1212);
+ recordingListDb->setValue("CHANNELID", "xxxxxx");
+
+ tell(0, "#2 %d changes", recordingListDb->getChanges());
+ recordingListDb->setValue("FSK", yes);
+ tell(0, "#3 %d changes", recordingListDb->getChanges());
+
+ // don't toggle uuid if already set!
+
+ if (recordingListDb->getValue("VDRUUID")->isNull())
+ recordingListDb->setValue("VDRUUID", "11111");
+
+ if (insert || recordingListDb->getChanges())
+ {
+ tell(0, "storing '%s' due to %d changes ", insert ? "insert" : "update", recordingListDb->getChanges());
+ recordingListDb->store();
+ }
+
+ recordingListDb->reset();
+
+ tell(0, "---------------------------------");
+
+ delete recordingListDb;
+}
+
+//***************************************************************************
+//
+//***************************************************************************
+
+void statementTimer()
+{
+ cDbValue timerState;
+ cDbValue timerAction;
+ cDbFieldDef timerStateDef("STATE", "state", cDBS::ffAscii, 100, cDBS::ftData);
+ cDbFieldDef timerActionDef("ACTION", "action", cDBS::ffAscii, 100, cDBS::ftData);
+
+ cEpgConfig::loglevel = 0;
+
+ cDbTable* timerDb = new cDbTable(connection, "timers");
+ if (timerDb->open() != success) return ;
+
+ cDbTable* useeventsDb = new cDbTable(connection, "useevents");
+ if (useeventsDb->open() != success) return ;
+
+ // select t.*,
+ // e.eventid, e.channelid, e.title, e.shorttext, e.shortdescription, e.category, e.genre, e.tipp
+ // from timers t left outer join events e
+ // on (t.eventid = e.masterid and e.updflg in (...))
+ // where
+ // t.state in (?)
+
+ timerState.setField(&timerStateDef);
+ timerAction.setField(&timerActionDef);
+
+ cDbStatement* selectAllTimer = new cDbStatement(timerDb);
+
+ selectAllTimer->build("select ");
+ selectAllTimer->setBindPrefix("t.");
+ selectAllTimer->bindAllOut();
+ selectAllTimer->setBindPrefix("e.");
+ selectAllTimer->bind(useeventsDb, "USEID", cDBS::bndOut, ", ");
+ selectAllTimer->bind(useeventsDb, "CHANNELID", cDBS::bndOut, ", ");
+ selectAllTimer->bind(useeventsDb, "TITLE", cDBS::bndOut, ", ");
+ selectAllTimer->bind(useeventsDb, "SHORTTEXT", cDBS::bndOut, ", ");
+ selectAllTimer->bind(useeventsDb, "SHORTDESCRIPTION", cDBS::bndOut, ", ");
+ selectAllTimer->bind(useeventsDb, "CATEGORY", cDBS::bndOut, ", ");
+ selectAllTimer->bind(useeventsDb, "GENRE", cDBS::bndOut, ", ");
+ selectAllTimer->bind(useeventsDb, "TIPP", cDBS::bndOut, ", ");
+ selectAllTimer->clrBindPrefix();
+ selectAllTimer->build(" from %s t left outer join %s e",
+ timerDb->TableName(), "eventsviewplain");
+ selectAllTimer->build(" on (t.eventid = e.cnt_useid) and e.updflg in (%s)", cEventState::getVisible());
+
+ selectAllTimer->setBindPrefix("t.");
+ selectAllTimer->build(" where ");
+
+ selectAllTimer->bindInChar("t", timerDb->getField("STATE")->getDbName(), &timerState, " and (");
+ selectAllTimer->build(" or t.%s is null)", timerDb->getField("STATE")->getDbName());
+
+ selectAllTimer->bindInChar("t", timerDb->getField("ACTION")->getDbName(), &timerAction, " and (");
+ selectAllTimer->build(" or t.%s is null)", timerDb->getField("ACTION")->getDbName());
+
+ // selectAllTimer->bindInChar(0, "STATE", &timerState);
+
+ cEpgConfig::loglevel = 2;
+ selectAllTimer->prepare();
+
+ // ---------------------------------
+
+ timerDb->clear();
+ timerState.setValue("P,R,u");
+ timerState.setValue("C,M,A,F,a");
+ // timerState.setValue("A,D,P");
+
+ tell(0, "---------------------------------");
+
+ for (int found = selectAllTimer->find(); found; found = selectAllTimer->fetch())
+ {
+ tell(0, "%ld) %s/%s- %s",
+ timerDb->getIntValue("ID"),
+ timerDb->getStrValue("STATE"),
+ timerDb->getStrValue("ACTION"),
+ timerDb->getStrValue("FILE"));
+ }
+
+ tell(0, "---------------------------------");
+
+ delete selectAllTimer;
+ delete timerDb;
+}
+
+void statementVdrs()
+{
+ cDbTable* vdrDb = new cDbTable(connection, "vdrs");
+ if (vdrDb->open() != success) return ;
+
+ vdrDb->clear();
+ vdrDb->setValue("UUID", "10");
+ vdrDb->find();
+ vdrDb->setValue("VIDEOTOTAL", 1782579);
+ vdrDb->store();
+
+ delete vdrDb;
+}
+
+std::map<std::string, int> transponders;
+
+int tsp(std::string transponder)
+{
+ size_t endpos = transponder.find_last_of("-");
+
+ if (endpos == std::string::npos)
+ return 0;
+
+ transponder = transponder.substr(0, endpos);
+ transponders[transponder]++;
+
+ return 0;
+}
+
+//***************************************************************************
+// Main
+//***************************************************************************
+
+int main(int argc, char** argv)
+{
+ cEpgConfig::logstdout = yes;
+ cEpgConfig::loglevel = 2;
+
+/* const char* interface = getFirstInterface();
+
+ tell(0, "%s: %s - %s [%s]", getFirstInterface(), getIpOf(""), getMaskOf(""), getMacOf(""));
+ tell(0, "%s: %s - %s [%s]", getFirstInterface(), getIpOf(interface), getMaskOf(interface), getMacOf(interface));
+
+ // if (sendWol(argv[1], bcastAddressOf(getIpOf(interface), getMaskOf(interface))) != success)
+ // tell(0, "Error occured during sending the WOL magic packet for mac address '%s' via bcat '%s'",
+ // argv[1], bcastAddressOf(getIpOf(interface), getMaskOf(interface)));
+
+ return 0;
+
+ tsp("S19.2E-1-1109-5402");
+ tsp("S19.2E-1-1109-5404");
+ tsp("S19.2E-1-1112-5404");
+
+ std::map<std::string, int>::iterator it;
+
+ for (it = transponders.begin(); it != transponders.end(); it++)
+ printf("'%s' (%d)\n", it->first.c_str(), it->second);
+
+ return 0;
+
+ printf("%s\n", getInterfaces());
+
+
+ if (argc > 1)
+ {
+ int timerId = getTimerIdOf(argv[1]);
+ char aux[10000+TB];
+ strcpy(aux, argv[1]);
+
+ tell(0, "TimerId = %d", timerId);
+
+ removeTag(aux, "timerid");
+ tell(0, "aux: '%s'", aux);
+ insertTag(aux, "epgd", "timerid", 677776);
+ tell(0, "aux: '%s'", aux);
+
+ return 0;
+ }
+*/
+ setlocale(LC_CTYPE, "");
+ char* lang = setlocale(LC_CTYPE, 0);
+
+ if (lang)
+ {
+ tell(0, "Set locale to '%s'", lang);
+
+ if ((strcasestr(lang, "UTF-8") != 0) || (strcasestr(lang, "UTF8") != 0))
+ tell(0, "detected UTF-8");
+ else
+ tell(0, "no UTF-8");
+ }
+ else
+ {
+ tell(0, "Reseting locale for LC_CTYPE failed.");
+ }
+
+ // read dictionary
+
+ if (dbDict.in("/etc/epgd/epg.dat") != success)
+// if (dbDict.in("../configs/epg.dat") != success)
+ {
+ tell(0, "Invalid dictionary configuration, aborting!");
+ return 1;
+ }
+
+ // dbDict.show();
+
+ initConnection();
+
+ /*
+ cDbTable* table = new cDbTable(connection, "_test");
+
+ if (table->open(yes) != success)
+ {
+ tell(0, "Could not access database '%s:%d' (%s)",
+ cDbConnection::getHost(), cDbConnection::getPort(), table->TableName());
+
+ return 0;
+ }
+
+ table->setValue("ID", 5);
+ table->setValue("VDRUUID", "0000");
+
+ if (!table->find())
+ tell(0, "not found");
+ table->setValue("INFO", "abcd");
+
+ table->store();
+
+ delete table;
+
+ tell(0, "---------------------------------------------------");
+ */
+ // structure();
+
+// chkCompress();
+
+// tell(0, "duration was: '%s'", ms2Dur(2340).c_str());
+
+// statementVdrs();
+
+ statementTimer();
+ // statementrecording();
+ // chkStatement2();
+ // chkStatement3();
+ // chkStatement4(); exitConnection();
+
+ return 0;
+}
diff --git a/lib/thread.c b/lib/thread.c
new file mode 100644
index 0000000..5938d75
--- /dev/null
+++ b/lib/thread.c
@@ -0,0 +1,342 @@
+/*
+ * thread.c:
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <linux/unistd.h>
+#include <sys/resource.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <sys/time.h>
+
+#include "thread.h"
+
+//***************************************************************************
+// get abs time plus 'millisecondsFromNow'
+//***************************************************************************
+
+static bool absTime(struct timespec* abstime, int millisecondsFromNow)
+{
+ struct timeval now;
+
+ if (gettimeofday(&now, 0) == 0)
+ {
+ // get current time
+
+ now.tv_sec += millisecondsFromNow / 1000; // add full seconds
+ now.tv_usec += (millisecondsFromNow % 1000) * 1000; // add microseconds
+
+ // take care of an overflow
+
+ if (now.tv_usec >= 1000000)
+ {
+ now.tv_sec++;
+ now.tv_usec -= 1000000;
+ }
+
+ abstime->tv_sec = now.tv_sec; // seconds
+ abstime->tv_nsec = now.tv_usec * 1000; // nano seconds
+
+ return success;
+ }
+
+ return fail;
+}
+
+//***************************************************************************
+// Class cThread
+//***************************************************************************
+
+cThread::cThread(const char* Description, bool LowPriority)
+{
+ active = running = no;
+ childTid = 0;
+ childThreadId = 0;
+ description = 0;
+ silent = no;
+
+ if (Description)
+ SetDescription("%s", Description);
+
+ lowPriority = LowPriority;
+}
+
+cThread::~cThread()
+{
+ Cancel(); // just in case the derived class didn't call it
+ free(description);
+}
+
+void cThread::SetDescription(const char *Description, ...)
+{
+ free(description);
+ description = NULL;
+
+ if (Description)
+ {
+ va_list ap;
+ va_start(ap, Description);
+ vasprintf(&description, Description, ap);
+ va_end(ap);
+ }
+}
+
+void *cThread::StartThread(cThread *Thread)
+{
+ Thread->childThreadId = ThreadId();
+ if (Thread->description)
+ {
+ tell(Thread->silent ? 2 : 0, "'%s' thread started (pid=%d, tid=%d, prio=%s)", Thread->description, getpid(), Thread->childThreadId, Thread->lowPriority ? "low" : "high");
+#ifdef PR_SET_NAME
+ if (prctl(PR_SET_NAME, Thread->description, 0, 0, 0) < 0)
+ tell(0, "%s thread naming failed (pid=%d, tid=%d)", Thread->description, getpid(), Thread->childThreadId);
+#endif
+ }
+
+ if (Thread->lowPriority)
+ {
+ Thread->SetPriority(19);
+ Thread->SetIOPriority(7);
+ }
+
+ Thread->action();
+
+ if (Thread->description)
+ tell(Thread->silent ? 2 : 0, "'%s' thread ended (pid=%d, tid=%d)", Thread->description, getpid(), Thread->childThreadId);
+
+ Thread->running = false;
+ Thread->active = false;
+
+ return NULL;
+}
+
+//***************************************************************************
+// Priority
+//***************************************************************************
+
+void cThread::SetPriority(int priority)
+{
+ if (setpriority(PRIO_PROCESS, 0, priority) < 0)
+ tell(0, "Error: Setting priority failed");
+}
+
+void cThread::SetIOPriority(int priority)
+{
+ if (syscall(SYS_ioprio_set, 1, 0, (priority & 0xff) | (3 << 13)) < 0) // idle class
+ tell(0, "Error: Setting io priority failed");
+}
+
+//***************************************************************************
+// Start
+//***************************************************************************
+
+#define THREAD_STOP_TIMEOUT 3000 // ms to wait for a thread to stop before newly starting it
+#define THREAD_STOP_SLEEP 30 // ms to sleep while waiting for a thread to stop
+
+bool cThread::Start(int s)
+{
+ silent = s;
+
+ if (!running)
+ {
+ if (active)
+ {
+ // Wait until the previous incarnation of this thread has completely ended
+ // before starting it newly:
+
+ cMyTimeMs RestartTimeout;
+
+ while (!running && active && RestartTimeout.Elapsed() < THREAD_STOP_TIMEOUT)
+ cCondWait::SleepMs(THREAD_STOP_SLEEP);
+ }
+ if (!active)
+ {
+ active = running = true;
+
+ if (pthread_create(&childTid, NULL, (void *(*) (void *))&StartThread, (void *)this) == 0)
+ {
+ pthread_detach(childTid); // auto-reap
+ }
+ else
+ {
+ tell(0, "Error: Thread won't start");
+ active = running = false;
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool cThread::Active(void)
+{
+ if (active)
+ {
+ int err;
+
+ if ((err = pthread_kill(childTid, 0)) != 0)
+ {
+ if (err != ESRCH)
+ tell(0, "Error: Thread ...");
+ childTid = 0;
+ active = running = false;
+ }
+ else
+ return true;
+ }
+
+ return false;
+}
+
+void cThread::Cancel(int WaitSeconds)
+{
+ running = false;
+
+ if (active && WaitSeconds > -1)
+ {
+ if (WaitSeconds > 0)
+ {
+ for (time_t t0 = time(NULL) + WaitSeconds; time(NULL) < t0; )
+ {
+ if (!Active())
+ return;
+ cCondWait::SleepMs(10);
+ }
+
+ tell(0, "ERROR: %s thread %d won't end (waited %d seconds) - canceling it...", description ? description : "", childThreadId, WaitSeconds);
+ }
+
+ pthread_cancel(childTid);
+ childTid = 0;
+ active = false;
+ }
+}
+
+pid_t cThread::ThreadId()
+{
+ return syscall(__NR_gettid);
+}
+
+//***************************************************************************
+// cCondWait
+//***************************************************************************
+
+cCondWait::cCondWait()
+{
+ signaled = false;
+ pthread_mutex_init(&mutex, NULL);
+ pthread_cond_init(&cond, NULL);
+}
+
+cCondWait::~cCondWait()
+{
+ pthread_cond_broadcast(&cond); // wake up any sleepers
+ pthread_cond_destroy(&cond);
+ pthread_mutex_destroy(&mutex);
+}
+
+void cCondWait::SleepMs(int TimeoutMs)
+{
+ cCondWait w;
+ w.Wait(max(TimeoutMs, 3)); // making sure the time is >2ms to avoid a possible busy wait
+}
+
+bool cCondWait::Wait(int TimeoutMs)
+{
+ pthread_mutex_lock(&mutex);
+
+ if (!signaled)
+ {
+ if (TimeoutMs)
+ {
+ struct timespec abstime;
+
+ if (absTime(&abstime, TimeoutMs) == success)
+ {
+ while (!signaled)
+ {
+ if (pthread_cond_timedwait(&cond, &mutex, &abstime) == ETIMEDOUT)
+ break;
+ }
+ }
+ }
+ else
+ pthread_cond_wait(&cond, &mutex);
+ }
+
+ bool r = signaled;
+ signaled = false;
+ pthread_mutex_unlock(&mutex);
+
+ return r;
+}
+
+void cCondWait::Signal()
+{
+ pthread_mutex_lock(&mutex);
+ signaled = true;
+ pthread_cond_broadcast(&cond);
+ pthread_mutex_unlock(&mutex);
+}
+
+//***************************************************************************
+// cCondVar
+//***************************************************************************
+
+cCondVar::cCondVar(void)
+{
+ pthread_cond_init(&cond, 0);
+}
+
+cCondVar::~cCondVar()
+{
+ pthread_cond_broadcast(&cond); // wake up any sleepers
+ pthread_cond_destroy(&cond);
+}
+
+void cCondVar::Wait(cMyMutex &Mutex)
+{
+ if (Mutex.locked)
+ {
+ int locked = Mutex.locked;
+ Mutex.locked = 0; // have to clear the locked count here, as pthread_cond_wait
+ // does an implicit unlock of the mutex
+ pthread_cond_wait(&cond, &Mutex.mutex);
+ Mutex.locked = locked;
+ }
+}
+
+bool cCondVar::TimedWait(cMyMutex &Mutex, int TimeoutMs)
+{
+ bool r = true; // true = condition signaled, false = timeout
+
+ if (Mutex.locked)
+ {
+ struct timespec abstime;
+
+ if (absTime(&abstime, TimeoutMs) == success)
+ {
+ int locked = Mutex.locked;
+
+ // have to clear the locked count here, as pthread_cond_timedwait
+ // does an implicit unlock of the mutex.
+
+ Mutex.locked = 0;
+
+ if (pthread_cond_timedwait(&cond, &Mutex.mutex, &abstime) == ETIMEDOUT)
+ r = false;
+
+ Mutex.locked = locked;
+ }
+ }
+
+ return r;
+}
+
+void cCondVar::Broadcast(void)
+{
+ pthread_cond_broadcast(&cond);
+}
diff --git a/lib/thread.h b/lib/thread.h
new file mode 100644
index 0000000..1598ff2
--- /dev/null
+++ b/lib/thread.h
@@ -0,0 +1,92 @@
+/*
+ * thread.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __LIB_THREAD_H
+#define __LIB_THREAD_H
+
+#include "common.h"
+
+//***************************************************************************
+// Class cThread
+//***************************************************************************
+
+class cThread
+{
+ public:
+
+ cThread(const char *Description = NULL, bool LowPriority = false);
+ virtual ~cThread();
+
+ void SetDescription(const char* Description, ...) __attribute__ ((format (printf, 2, 3)));
+ bool Start(int s = no);
+ bool Active();
+ void SetPriority(int priority);
+ void SetIOPriority(int priority);
+
+ static pid_t ThreadId();
+
+ private:
+
+ static void* StartThread(cThread *Thread);
+
+ bool active;
+ bool running;
+ pthread_t childTid;
+ pid_t childThreadId;
+ cMyMutex mutex;
+ char* description;
+ bool lowPriority;
+ int silent;
+
+ protected:
+
+ virtual void action() = 0;
+
+ void Lock() { mutex.Lock(); }
+ void Unlock() { mutex.Unlock(); }
+ bool Running() { return running; }
+ void Cancel(int WaitSeconds = 0);
+};
+
+class cCondWait
+{
+ public:
+
+ cCondWait();
+ ~cCondWait();
+
+ bool Wait(int TimeoutMs = 0);
+ void Signal();
+
+ static void SleepMs(int TimeoutMs);
+
+ private:
+
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ bool signaled;
+};
+
+class cCondVar
+{
+ public:
+
+ cCondVar();
+ ~cCondVar();
+
+ void Wait(cMyMutex &Mutex);
+ bool TimedWait(cMyMutex &Mutex, int TimeoutMs);
+ void Broadcast();
+
+ private:
+
+ pthread_cond_t cond;
+};
+
+//***************************************************************************
+
+#endif // __LIB_THREAD_H
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<cLine> result;
+
+ asprintf(&command, "PLUG %s %s %s", plgs[i], trg, !isEmpty(options) ? options : "");
+
+ tell(0, "Send '%s' to '%s' at '%s:%d'", command, plgs[i], ip, port);
+
+ if (!cl.send(command))
+ tell(0, "Error: Send '%s' to '%s' at '%s:%d' failed!",
+ command, plgs[i], ip, port);
+ else
+ cl.receive(&result, 2);
+
+ free(command);
+ }
+
+ cl.close(no);
+ }
+
+ return success;
+}
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 <vdr/osdbase.h>
+#include <vdr/menuitems.h>
+
+#include "lib/common.h"
+#include "lib/db.h"
+#include "lib/epgservice.h"
+#include "lib/searchtimer.h"
+
+#include "menu.h"
+#include "ttools.h"
+#include "parameters.h"
+
+class cMenuEpgEditTimer;
+class cTimerData;
+
+//***************************************************************************
+//
+//***************************************************************************
+
+#define NEWTIMERLIMIT 60 // seconds until the start time of a new timer created from the Schedule menu,
+ // within which it will go directly into the "Edit timer" menu to allow
+ // further parameter settings
+
+#define MAXCHNAMWIDTH 16 // maximum number of characters of channels' short names shown in schedules menus
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+# define CHNUMWIDTH (numdigits(cChannels::MaxNumber()) + 1)
+# define CHNAMWIDTH (std::min(MAXCHNAMWIDTH, cChannels::MaxShortChannelNameLength() + 1))
+#else
+# define CHNUMWIDTH (numdigits(Channels.MaxNumber()) + 1)
+# define CHNAMWIDTH (std::min(MAXCHNAMWIDTH, Channels.MaxShortChannelNameLength() + 1))
+#endif
+
+//***************************************************************************
+// Class cMenuDb
+//***************************************************************************
+
+class cMenuDb : public cParameters
+{
+ friend class cMenuEpgEvent;
+ friend class cMenuEpgEditTimer;
+ friend class cMenuEpgWhatsOn;
+ friend class cMenuEpgSchedule;
+ friend class cMenuEpgTimers;
+ friend class cMenuEpgSearchTimers;
+ friend class cEpgMenuDones;
+ friend class cEpgMenuSearchTimerEdit;
+ friend class cMenuEpgMatchRecordings;
+ friend class cEpgMenuSearchResult;
+ friend class cMenuSetupEPG2VDR;
+
+ public:
+
+ cMenuDb();
+ virtual ~cMenuDb();
+
+ int initDb();
+ int exitDb();
+
+ int initUserTimes();
+ int initUserList(char**& userList, int& count, long int& loginEnabled);
+ int lookupTimer(const cEvent* event, int& timerMatch, int& remote, int force = no);
+
+ // timer db
+
+ int createTimer(cDbRow* timerRow, const char* destUuid);
+ int modifyTimer(cDbRow* timerRow, const char* destUuid = 0);
+ int deleteTimer(long timerid);
+
+ //
+
+ int triggerVdrs(const char* trg, const char* uuid = "", const char* options = "");
+ int triggerVdr(const char* uuid, const char* trg, const char* options = "");
+
+ protected:
+
+ virtual int dbConnected(int force = no)
+ {
+ return dbInitialized
+ && connection
+ && (!force || connection->check() == success);
+ }
+
+ // data
+
+ long webLoginEnabled;
+ std::string user;
+ long startWithSched;
+
+ cUserTimes* userTimes;
+ char** vdrList;
+ char** vdrUuidList;
+ int vdrCount;
+
+ // database
+
+ int dbInitialized;
+ cDbConnection* connection;
+
+ cDbTable* timerDb;
+ cDbTable* vdrDb;
+ cDbTable* timerDoneDb;
+ cDbTable* userDb;
+ cDbTable* searchtimerDb;
+ cDbTable* recordingListDb;
+ cDbTable* useeventsDb;
+
+ cDbStatement* selectTimers;
+ cDbStatement* selectMaxUpdSp;
+ cDbStatement* selectTimerById;
+ cDbStatement* selectActiveVdrs;
+ cDbStatement* selectAllVdrs;
+ cDbStatement* selectDoneTimerByState;
+ cDbStatement* selectAllUser;
+ cDbStatement* selectSearchTimers;
+ cDbStatement* selectSearchTimerByName;
+ cDbStatement* selectDoneTimerByStateTitleOrder;
+ cDbStatement* selectDoneTimerByStateTimeOrder;
+ cDbStatement* selectRecordingForEvent;
+ cDbStatement* selectRecordingForEventByLv;
+
+ cSearchTimer* search;
+
+ cDbValue valueStartTime;
+ cDbValue timerState;
+ cDbValue timerAction;
+
+ class cTimerInfo : public cListObject
+ {
+ public:
+
+ uint timerid;
+ char state;
+ time_t starttime;
+ uint eventid;
+ char channelid[100];
+ char uuid[sizeUuid+TB];
+ };
+
+ cList<cTimerInfo> timers;
+ int timersCacheMaxUpdsp;
+};
+
+//***************************************************************************
+// Class cMenuEditTimer
+//***************************************************************************
+
+class cMenuEpgEditTimer : public cOsdMenu
+{
+ public:
+
+ cMenuEpgEditTimer(cMenuDb* db, cEpgTimer* Timer, bool New = no);
+ virtual ~cMenuEpgEditTimer();
+
+ virtual eOSState ProcessKey(eKeys Key);
+
+ protected:
+
+ eOSState SetFolder();
+ void SetFirstDayItem();
+ void SetHelpKeys();
+
+ public:
+
+ class cTimerData
+ {
+ public:
+
+ cTimerData(cMenuDb* p) { memset(this, 0, sizeof(cTimerData)); db = p; }
+ ~cTimerData() { free(aux); }
+
+ const char* getVdrName() { return db->vdrList[vdrIndex]; }
+ const char* getVdrUuid() { return db->vdrUuidList[vdrIndex]; }
+ const char* getLastVdrUuid() { return lastVdrUuid; }
+ long TimerId() { return timerid; }
+
+ //**************************************************
+ // From Timer
+ //**************************************************
+
+ void fromTimer(cEpgTimer* Timer, bool New = no)
+ {
+ weekdays = Timer->WeekDays();
+ start = Timer->Start();
+ stop = Timer->Stop();
+ priority = Timer->Priority();
+ lifetime = Timer->Lifetime();
+ flags = Timer->Flags();
+ channel = Timer->Channel();
+ day = Timer->Day();
+ strcpy(file, Timer->File());
+ vdrIndex = 0;
+ free(aux); aux = 0;
+
+ if (New)
+ flags |= tfActive;
+
+ if (!isEmpty(Timer->Aux()))
+ aux = strdup(Timer->Aux());
+
+ timerid = Timer->TimerId();
+ eventid = Timer->EventId();
+ state = Timer->State();
+ sprintf(stateInfo, "%.300s", Timer->StateInfo());
+ action = Timer->Action();
+ strcpy(lastVdrUuid, Timer->VdrUuid());
+
+ for (int i = 0; i < db->vdrCount; i++)
+ if (strncmp(db->vdrList[i], Timer->VdrName(), strlen(db->vdrList[i])-1) == 0)
+ vdrIndex = i;
+
+#ifdef WITH_PIN
+ fskProtection = Timer->FskProtection();
+#endif
+ }
+
+ int toRow(cDbRow* timerRow)
+ {
+ timerRow->clear();
+
+ timerRow->setValue("ID", timerid);
+ timerRow->setValue("VDRUUID", lastVdrUuid);
+ timerRow->setValue("ACTIVE", (int)flags & tfActive);
+ timerRow->setValue("CHILDLOCK", fskProtection);
+ timerRow->setValue("CHANNELID", channel->GetChannelID().ToString());
+ timerRow->setValue("FILE", file);
+ timerRow->setValue("VPS", (int)flags & tfVps);
+ timerRow->setValue("DAY", day);
+ timerRow->setValue("WEEKDAYS", weekdays);
+ timerRow->setValue("STARTTIME", start);
+ timerRow->setValue("ENDTIME", stop);
+ timerRow->setValue("PRIORITY", priority);
+ timerRow->setValue("LIFETIME", lifetime);
+ timerRow->setValue("EVENTID", eventid);
+
+ return done;
+ }
+
+ long timerid;
+ long eventid;
+ int weekdays; // bitmask, lowest bits: SSFTWTM (the 'M' is the LSB)
+ int start;
+ int stop;
+ int priority;
+ int fskProtection;
+ int lifetime;
+ uint flags;
+ char state;
+ char stateInfo[300+TB];
+ char action;
+ int vdrIndex;
+ const cChannel* channel;
+ mutable time_t day;
+ mutable char file[NAME_MAX*2 + TB]; // *2 to be able to hold 'title' and 'episode', which can each be up to 255 characters long
+ char* aux;
+
+ cMenuDb* db;
+ char lastVdrUuid[sizeUuid+TB];
+ };
+
+ protected:
+
+ cMenuDb* menuDb;
+ cTimerData data;
+ int channelNr;
+ cMenuEditStrItem* file;
+ cMenuEditDateItem* day;
+ cMenuEditDateItem* firstday;
+};
+
+//***************************************************************************
+// class cEpgMenuTextItem
+//***************************************************************************
+
+class cEpgMenuTextItem : public cOsdItem
+{
+ public:
+
+ cEpgMenuTextItem(long aId, const char* text)
+ {
+ cid = 0;
+ id = aId;
+ SetText(text);
+ }
+
+ cEpgMenuTextItem(const char* aId, const char* text)
+ {
+ cid = strdup(aId);
+ SetText(text);
+ }
+
+ virtual ~cEpgMenuTextItem() { free(cid); }
+
+ long getId() { return id; }
+ const char* getCharId() { return cid; }
+
+ protected:
+
+ long id;
+ char* cid;
+};
+
+//***************************************************************************
+// Class cMenuEpgTimers
+//***************************************************************************
+
+class cMenuEpgTimers : public cOsdMenu
+{
+ public:
+
+ cMenuEpgTimers();
+ virtual ~cMenuEpgTimers();
+
+ virtual eOSState ProcessKey(eKeys Key);
+ int refresh();
+
+ private:
+
+ eOSState edit();
+ eOSState create();
+ eOSState remove();
+ eOSState toggleState();
+ eOSState info();
+ cEpgTimer* currentTimer();
+ void SetHelpKeys();
+
+ cMenuDb* menuDb;
+ int helpKeys;
+ int timersMaxUpdsp;
+};
+
+//***************************************************************************
+// Class cMenuEpgSearchTimers
+//***************************************************************************
+
+class cMenuEpgSearchTimers : public cOsdMenu
+{
+ public:
+
+ cMenuEpgSearchTimers();
+ virtual ~cMenuEpgSearchTimers();
+
+ virtual eOSState ProcessKey(eKeys Key);
+ int refresh();
+ void setHelpKeys();
+
+ private:
+
+ cMenuDb* menuDb;
+ int helpKeys;
+};
+
+//***************************************************************************
+// Class cMenuEpgEvent
+//***************************************************************************
+
+class cMenuEpgEvent : public cOsdMenu
+{
+ public:
+
+ cMenuEpgEvent(cMenuDb* db, const cEvent* Event, const cSchedules* schedules,
+ int timerMatch, bool DispSchedule, bool CanSwitch = false);
+ virtual void Display(void);
+ virtual eOSState ProcessKey(eKeys Key);
+ virtual const char* MenuKind() { return "MenuEvent"; }
+
+ private:
+
+ const cEvent* getNextPrevEvent(const cEvent* event, int step);
+ int setEvent(const cEvent* Event, int timerMatch);
+
+ cMenuDb* menuDb;
+ const cSchedules* schedules;
+ const cEvent* event;
+ bool dispSchedule;
+ bool canSwitch;
+
+ std::string prevTime;
+ std::string nextTime;
+};
+
+//***************************************************************************
+// Class cMenuEpgScheduleItem
+//***************************************************************************
+
+class cMenuEpgScheduleItem : public cOsdItem
+{
+ public:
+
+ enum eScheduleSortMode { ssmAllThis, ssmThisThis, ssmThisAll, ssmAllAll }; // "which event(s) on which channel(s)"
+
+ cMenuEpgScheduleItem(cMenuDb* db, const cEvent* Event, const cChannel* Channel = 0, bool WithDate = no);
+
+ static void SetSortMode(eScheduleSortMode SortMode) { sortMode = SortMode; }
+ static void IncSortMode() { sortMode = eScheduleSortMode((sortMode == ssmAllAll) ? ssmAllThis : sortMode + 1); }
+ static eScheduleSortMode SortMode() { return sortMode; }
+
+ virtual int Compare(const cListObject &ListObject) const;
+ virtual bool Update(bool Force = false);
+ virtual void SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable);
+
+ const cEvent* event;
+ const cChannel* channel;
+ bool withDate;
+ int timerMatch;
+
+ private:
+
+ cMenuDb* menuDb;
+ static eScheduleSortMode sortMode;
+};
+
+//***************************************************************************
+// Class cMenuEpgWhatsOn
+//***************************************************************************
+
+class cMenuEpgWhatsOn : public cOsdMenu
+{
+ public:
+
+ cMenuEpgWhatsOn(const cEvent* aSearchEvent = 0);
+ ~cMenuEpgWhatsOn();
+
+ static int CurrentChannel() { return currentChannel; }
+ static void SetCurrentChannel(int ChannelNr) { currentChannel = ChannelNr; }
+ virtual eOSState ProcessKey(eKeys Key);
+ virtual void Display();
+
+ private:
+
+ int LoadAt();
+ int LoadSearch(const cUserTimes::UserTime* userTime);
+ int LoadSchedule();
+ int LoadQuery(const cEvent* searchEvent);
+
+ bool canSwitch;
+ int helpKeys;
+ time_t helpKeyTime;
+ int helpKeyTimeMode;
+ int timerState;
+ eOSState Record();
+ eOSState Switch();
+ bool Update();
+ void SetHelpKeys();
+
+ cMenuDb* menuDb;
+ const cSchedules* schedules;
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cStateKey schedulesKey;
+#else
+ cSchedulesLock* schedulesLock;
+#endif
+ const cEvent* searchEvent;
+ int dispSchedule;
+
+ static int currentChannel;
+ static const cEvent* scheduleEvent;
+};
+
+//***************************************************************************
+// class cEpgMenuDones
+//***************************************************************************
+
+class cEpgMenuDones : public cOsdMenu
+{
+ public:
+
+ enum JournalFilter
+ {
+ jfAll = 0,
+
+ jfFirst = 1,
+ jfRecorded = jfFirst,
+ jfCreated,
+ jfFailed,
+ jfDeleted,
+
+ jfCount
+ };
+
+ cEpgMenuDones();
+ virtual ~cEpgMenuDones();
+ virtual eOSState ProcessKey(eKeys Key);
+
+ protected:
+
+ int refresh();
+ void setHelp();
+
+ // data
+
+ cMenuDb* menuDb;
+ int journalFilter;
+ int order;
+};
+
+//***************************************************************************
+// class cMenuEpgMatchRecordings
+//***************************************************************************
+
+class cMenuEpgMatchRecordings : public cOsdMenu
+{
+ public:
+
+ cMenuEpgMatchRecordings(cMenuDb* db, const cEvent* event);
+ virtual ~cMenuEpgMatchRecordings() {};
+ virtual eOSState ProcessKey(eKeys Key);
+};
+
+//***************************************************************************
+
+#endif // __EPG2VDR_MENU_H
diff --git a/menudone.c b/menudone.c
new file mode 100644
index 0000000..be04ca8
--- /dev/null
+++ b/menudone.c
@@ -0,0 +1,158 @@
+/*
+ * menudone.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <vdr/interface.h>
+
+#include "epg2vdr.h"
+#include "update.h"
+#include "menu.h"
+
+#include "svdrpclient.h"
+
+//***************************************************************************
+// Object
+//***************************************************************************
+
+cEpgMenuDones::cEpgMenuDones()
+ : cOsdMenu("", 2, 20, 35)
+{
+ journalFilter = jfAll;
+ order = 0;
+
+ menuDb = new cMenuDb;
+
+ refresh();
+}
+
+cEpgMenuDones::~cEpgMenuDones()
+{
+ delete menuDb;
+}
+
+//***************************************************************************
+// Set Help
+//***************************************************************************
+
+void cEpgMenuDones::setHelp()
+{
+ const char* btGreen = "";
+
+ switch (journalFilter)
+ {
+ case jfAll: btGreen = tr("Recorded"); break;
+ case jfRecorded: btGreen = tr("Created"); break;
+ case jfCreated: btGreen = tr("Failed"); break;
+ case jfFailed: btGreen = tr("Deleted"); break;
+ case jfDeleted: btGreen = tr("Recorded"); break;
+
+ }
+
+ SetHelp(tr("All"), btGreen, tr("Delete"), 0);
+}
+
+//***************************************************************************
+// Refresh
+//***************************************************************************
+
+int cEpgMenuDones::refresh()
+{
+ int current = Current();
+ const char* state = "%";
+
+ cDbStatement* select = order ? menuDb->selectDoneTimerByStateTimeOrder : menuDb->selectDoneTimerByStateTitleOrder;
+
+ Clear();
+
+ switch (journalFilter)
+ {
+ case jfAll: state = "%"; SetTitle(tr("Timer journal")); break;
+ case jfRecorded: state = "R"; SetTitle(tr("Timer journal - Recorded")); break;
+ case jfCreated: state = "C"; SetTitle(tr("Timer journal - Created")); break;
+ case jfFailed: state = "F"; SetTitle(tr("Timer journal - Failed")); break;
+ case jfDeleted: state = "D"; SetTitle(tr("Timer journal - Delete")); break;
+ }
+
+ // fill menu ..
+
+ menuDb->timerDoneDb->clear();
+ menuDb->timerDoneDb->setValue("STATE", state);
+
+ for (int f = select->find(); f && menuDb->dbConnected(); f = select->fetch())
+ {
+ char* buf;
+ asprintf(&buf, "%s\t%s\t%s\t%s",
+ menuDb->timerDoneDb->getStrValue("STATE"),
+ l2pTime(menuDb->timerDoneDb->getIntValue("STARTTIME"), "%d.%m.%y %H:%M").c_str(),
+ menuDb->timerDoneDb->getStrValue("TITLE"),
+ menuDb->timerDoneDb->getStrValue("SHORTTEXT"));
+ Add(new cEpgMenuTextItem(menuDb->timerDoneDb->getIntValue("ID"), buf));
+ free(buf);
+ }
+
+ select->freeResult();
+
+ // init ..
+
+ SetCurrent(current >= 0 ? Get(current) : First());
+ setHelp();
+ Display();
+
+ return success;
+}
+
+//***************************************************************************
+// ProcessKey
+//***************************************************************************
+
+eOSState cEpgMenuDones::ProcessKey(eKeys Key)
+{
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (state == osUnknown)
+ {
+ switch (Key)
+ {
+ case k0:
+ {
+ order = !order;
+ refresh();
+ break;
+ }
+ case kRed:
+ {
+ journalFilter = jfAll;
+ refresh();
+ return osContinue;
+
+ }
+ case kGreen:
+ {
+ if (++journalFilter >= jfCount)
+ journalFilter = jfFirst;
+
+ refresh();
+ return osContinue;
+ }
+ case kYellow:
+ {
+ cEpgMenuTextItem* item = (cEpgMenuTextItem*)Get(Current());
+
+ if (item && Interface->Confirm(tr("Delete timer from journal?")))
+ {
+ menuDb->timerDoneDb->deleteWhere("id = %ld", item->getId());
+ refresh();
+ }
+
+ return osContinue;
+ }
+
+ default: break;
+ }
+ }
+
+ return state;
+}
diff --git a/menusched.c b/menusched.c
new file mode 100644
index 0000000..452c7b4
--- /dev/null
+++ b/menusched.c
@@ -0,0 +1,1204 @@
+/*
+ * menusched.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#define __STL_CONFIG_H
+
+#include <string>
+
+#include <vdr/menuitems.h>
+#include <vdr/status.h>
+#include <vdr/menu.h>
+
+#include "plgconfig.h"
+#include "menu.h"
+#include "ttools.h"
+
+class cMenuEpgScheduleItem;
+
+//***************************************************************************
+// Class cEpgCommandMenu
+//***************************************************************************
+
+class cEpgCommandMenu : public cOsdMenu
+{
+ public:
+
+ cEpgCommandMenu(const char* title, cMenuDb* db, const cMenuEpgScheduleItem* aItem);
+ virtual ~cEpgCommandMenu() { };
+
+ virtual eOSState ProcessKey(eKeys key);
+
+ protected:
+
+ const cMenuEpgScheduleItem* item;
+ cMenuDb* menuDb;
+};
+
+cEpgCommandMenu::cEpgCommandMenu(const char* title, cMenuDb* db, const cMenuEpgScheduleItem* aItem)
+ : cOsdMenu(title)
+{
+ item = aItem;
+ menuDb = db;
+
+ SetMenuCategory(mcMain);
+ SetHasHotkeys(yes);
+ Clear();
+
+ cOsdMenu::Add(new cOsdItem(hk(tr("Search matching Events")), osUser1));
+ cOsdMenu::Add(new cOsdItem(hk(tr("Search matching Recordings")), osUser2));
+ cOsdMenu::Add(new cOsdItem(hk(tr("Searchtimers")), osUser3));
+
+ SetHelp(0, 0, 0, 0);
+
+ Display();
+}
+
+eOSState cEpgCommandMenu::ProcessKey(eKeys key)
+{
+ eOSState state = cOsdMenu::ProcessKey(key);
+
+ switch (state)
+ {
+ case osUser1:
+ {
+ if (item)
+ return AddSubMenu(new cMenuEpgWhatsOn(item->event));
+
+ break;
+ }
+
+ case osUser2:
+ {
+ if (item)
+ return AddSubMenu(new cMenuEpgMatchRecordings(menuDb, item->event));
+
+ break;
+ }
+
+ case osUser3: return AddSubMenu(new cMenuEpgSearchTimers());
+
+ default: ;
+ }
+
+ return state;
+}
+
+//***************************************************************************
+// Class cMenuEpgMatchRecordings
+//***************************************************************************
+
+cMenuEpgMatchRecordings::cMenuEpgMatchRecordings(cMenuDb* db, const cEvent* event)
+ : cOsdMenu(tr("Matching recordings"), 30, 30, 30, 40)
+{
+ cMenuDb* menuDb = db;
+
+ Add(new cOsdItem(tr("Direct Matches:")));
+ cList<cOsdItem>::Last()->SetSelectable(no);
+
+ tell(0, "Search recordings for '%s' '%s'", event->Title(), event->ShortText());
+
+ menuDb->recordingListDb->clear();
+ menuDb->recordingListDb->setValue("TITLE", event->Title());
+ menuDb->recordingListDb->setValue("SHORTTEXT", event->ShortText());
+
+ for (int f = menuDb->selectRecordingForEvent->find(); f; f = menuDb->selectRecordingForEvent->fetch())
+ {
+ const char* vdr = 0;
+
+#ifdef WITH_PIN
+ if (!cOsd::pinValid && menuDb->recordingListDb->getIntValue("FSK"))
+ continue;
+#endif
+
+ if (!menuDb->recordingListDb->getValue("OWNER")->isEmpty())
+ {
+ menuDb->vdrDb->clear();
+ menuDb->vdrDb->setValue("UUID", menuDb->recordingListDb->getStrValue("OWNER"));
+
+ if (menuDb->vdrDb->find())
+ vdr = menuDb->vdrDb->getStrValue("NAME");
+ }
+
+ Add(new cEpgMenuTextItem(menuDb->recordingListDb->getStrValue("MD5PATH"),
+ cString::sprintf("%s%s%s\t%s\t%s/%s", vdr ? vdr : "", vdr ? "\t" : "",
+ menuDb->recordingListDb->getStrValue("TITLE"),
+ menuDb->recordingListDb->getStrValue("SHORTTEXT"),
+ menuDb->recordingListDb->getStrValue("FOLDER"),
+ menuDb->recordingListDb->getStrValue("NAME"))));
+ }
+
+ menuDb->selectRecordingForEvent->freeResult();
+
+ Add(new cOsdItem(tr("All Matches:")));
+ cList<cOsdItem>::Last()->SetSelectable(no);
+
+ menuDb->recordingListDb->clear();
+ menuDb->recordingListDb->setValue("TITLE", event->Title());
+
+ for (int f = menuDb->selectRecordingForEventByLv->find(); f; f = menuDb->selectRecordingForEventByLv->fetch())
+ {
+ const char* vdr = 0;
+
+#ifdef WITH_PIN
+ if (!cOsd::pinValid && menuDb->recordingListDb->getIntValue("FSK"))
+ continue;
+#endif
+
+ if (!menuDb->recordingListDb->getValue("OWNER")->isEmpty())
+ {
+ menuDb->vdrDb->clear();
+ menuDb->vdrDb->setValue("UUID", menuDb->recordingListDb->getStrValue("OWNER"));
+
+ if (menuDb->vdrDb->find())
+ vdr = menuDb->vdrDb->getStrValue("NAME");
+ }
+
+ Add(new cEpgMenuTextItem(menuDb->recordingListDb->getStrValue("MD5PATH"),
+ cString::sprintf("%s%s%s\t%s\t%s/%s", vdr ? vdr : "", vdr ? "\t" : "",
+ menuDb->recordingListDb->getStrValue("TITLE"),
+ menuDb->recordingListDb->getStrValue("SHORTTEXT"),
+ menuDb->recordingListDb->getStrValue("FOLDER"),
+ menuDb->recordingListDb->getStrValue("NAME"))));
+ }
+
+ menuDb->selectRecordingForEventByLv->freeResult();
+
+ Display();
+}
+
+eOSState cMenuEpgMatchRecordings::ProcessKey(eKeys Key)
+{
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (state == osUnknown)
+ {
+ return osContinue;
+ }
+
+ return state;
+}
+
+//***************************************************************************
+//***************************************************************************
+// Class cMenuEpgScheduleItem
+//***************************************************************************
+
+cMenuEpgScheduleItem::eScheduleSortMode cMenuEpgScheduleItem::sortMode = ssmAllThis;
+
+//***************************************************************************
+// Object
+//***************************************************************************
+
+cMenuEpgScheduleItem::cMenuEpgScheduleItem(cMenuDb* db, const cEvent* Event, const cChannel* Channel, bool WithDate)
+{
+ menuDb = db;
+
+ event = Event;
+ channel = Channel;
+ withDate = WithDate;
+ timerMatch = tmNone;
+ Update(yes);
+}
+
+//***************************************************************************
+// Compare
+//***************************************************************************
+
+int cMenuEpgScheduleItem::Compare(const cListObject &ListObject) const
+{
+ cMenuEpgScheduleItem *p = (cMenuEpgScheduleItem *)&ListObject;
+ int r = -1;
+
+ if (sortMode != ssmAllThis)
+ r = strcoll(event->Title(), p->event->Title());
+
+ if (sortMode == ssmAllThis || r == 0)
+ r = event->StartTime() - p->event->StartTime();
+
+ return r;
+}
+
+//***************************************************************************
+// Update
+//***************************************************************************
+
+bool cMenuEpgScheduleItem::Update(bool Force)
+{
+ static const char* TimerMatchChars = " tT";
+ static const char* TimerMatchCharsRemote = " sS";
+
+ bool result = false;
+ int oldTimerMatch = timerMatch;
+ int remote = no;
+
+ if (event)
+ menuDb->lookupTimer(event, timerMatch, remote); // -> loads timerDb, vdrDb and set ther timerMatch
+
+ if (Force || timerMatch != oldTimerMatch)
+ {
+ if (timerMatch && event)
+ tell(0, "Timer match for '%s'", event->Title());
+
+ cString buffer;
+ char t = !remote ? TimerMatchChars[timerMatch] : TimerMatchCharsRemote[timerMatch];
+ char v = !event ? ' ' : event->Vps() && (event->Vps() - event->StartTime()) ? 'V' : ' ';
+ char r = !event ? ' ' : event->SeenWithin(30) && event->IsRunning() ? '*' : ' ';
+ const char* csn = channel ? channel->ShortName(true) : 0;
+ cString eds = !event ? "" : event->GetDateString();
+
+ if (channel && withDate)
+ buffer = cString::sprintf("%d\t%.*s\t%.*s\t%s\t%c%c%c\t%s", channel->Number(),
+ Utf8SymChars(csn, 999), csn, Utf8SymChars(eds, 6),
+ *eds,
+ !event ? "" : *event->GetTimeString(),
+ t, v, r,
+ !event ? "" : event->Title());
+ else if (channel)
+ buffer = cString::sprintf("%d\t%.*s\t%s\t%c%c%c\t%s", channel->Number(),
+ Utf8SymChars(csn, 999), csn,
+ !event ? "" : *event->GetTimeString(),
+ t, v, r,
+ !event ? "" : event->Title());
+ else
+ buffer = cString::sprintf("%.*s\t%s\t%c%c%c\t%s", Utf8SymChars(eds, 6), *eds,
+ !event ? "" : *event->GetTimeString(),
+ t, v, r, !event ? "" :
+ !event ? "" : event->Title());
+
+ SetText(buffer);
+ result = true;
+ }
+
+ return result;
+}
+
+//***************************************************************************
+// SetMenuItem
+//***************************************************************************
+
+void cMenuEpgScheduleItem::SetMenuItem(cSkinDisplayMenu* DisplayMenu,
+ int Index, bool Current, bool Selectable)
+{
+ if (!DisplayMenu->SetItemEvent(event, Index, Current, Selectable, channel,
+ withDate, (eTimerMatch)timerMatch))
+ {
+ DisplayMenu->SetItem(Text(), Index, Current, Selectable);
+ }
+}
+
+//***************************************************************************
+//***************************************************************************
+// cMenuEpgScheduleSepItem
+//***************************************************************************
+
+class cMenuEpgScheduleSepItem : public cOsdItem
+{
+ public:
+
+ cMenuEpgScheduleSepItem(const cChannel* Channel, const cEvent* Event);
+ ~cMenuEpgScheduleSepItem();
+
+ virtual bool Update(bool Force = false);
+ virtual void SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable);
+
+ private:
+
+ const cChannel* channel;
+ const cEvent* event;
+ cEvent* tmpEvent;
+};
+
+cMenuEpgScheduleSepItem::cMenuEpgScheduleSepItem(const cChannel* Channel, const cEvent* Event)
+{
+ channel = Channel;
+ event = Event;
+ tmpEvent = 0;
+
+ SetSelectable(false);
+ Update(true);
+}
+
+cMenuEpgScheduleSepItem::~cMenuEpgScheduleSepItem()
+{
+ delete tmpEvent;
+}
+
+bool cMenuEpgScheduleSepItem::Update(bool Force)
+{
+ if (channel)
+ {
+ SetText(cString::sprintf("-----\t %s -----", channel ? channel->Name() : *event->GetDateString()));
+ }
+ else if (event)
+ {
+ tmpEvent = new cEvent(0);
+ tmpEvent->SetTitle(cString::sprintf("-----\t %s -----", *event->GetDateString()));
+ SetText(tmpEvent->Title());
+ }
+
+ return true;
+}
+
+void cMenuEpgScheduleSepItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable)
+{
+ if (!DisplayMenu->SetItemEvent(tmpEvent, Index, Current, Selectable, channel, no, tmNone))
+ DisplayMenu->SetItem(Text(), Index, Current, Selectable);
+}
+
+//***************************************************************************
+// cMenuEpgEvent
+//***************************************************************************
+
+cMenuEpgEvent::cMenuEpgEvent(cMenuDb* db, const cEvent* Event, const cSchedules* Schedules,
+ int timerMatch, bool DispSchedule, bool CanSwitch)
+ : cOsdMenu(tr("Event"))
+{
+ menuDb = db;
+ dispSchedule = DispSchedule;
+ schedules = Schedules;
+ canSwitch = CanSwitch;
+ SetMenuCategory(mcEvent);
+ setEvent(Event, timerMatch);
+}
+
+//***************************************************************************
+// Set Event
+//***************************************************************************
+
+int cMenuEpgEvent::setEvent(const cEvent* Event, int timerMatch)
+{
+ if (Event)
+ {
+ event = Event;
+
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+ const cChannel* channel = channels->GetByChannelID(event->ChannelID(), true);
+#else
+ cChannels* channels = &Channels;
+ cChannel* channel = channels->GetByChannelID(event->ChannelID(), true);
+#endif
+
+ if (channel)
+ {
+ const cChannel* prevChannel = 0;
+ const cChannel* nextChannel = 0;
+
+ prevTime = "";
+ nextTime = "";
+
+ SetTitle(channel->Name());
+
+ if (menuDb->userTimes->current()->getMode() != cUserTimes::mSearch)
+ {
+ const cEvent* e;
+
+ if (e = getNextPrevEvent(event, -1))
+ {
+ if (dispSchedule)
+ prevTime = l2pTime(e->StartTime(), "%H:%M");
+ else
+ prevChannel = channels->GetByChannelID(e->ChannelID(), true);
+ }
+
+ if (e = getNextPrevEvent(event, +1))
+ {
+ if (dispSchedule)
+ nextTime = l2pTime(e->StartTime(), "%H:%M");
+ else
+ nextChannel = channels->GetByChannelID(e->ChannelID(), true);
+ }
+ }
+
+ SetHelp(timerMatch == tmFull ? tr("Button$Timer") : tr("Button$Record"),
+ dispSchedule ? prevTime.c_str() : (prevChannel ? prevChannel->Name() : 0),
+ dispSchedule ? nextTime.c_str() : (nextChannel ? nextChannel->Name() : 0),
+ canSwitch ? tr("Button$Switch") : 0);
+ }
+
+ Display();
+ }
+
+ return success;
+}
+
+void cMenuEpgEvent::Display()
+{
+ cOsdMenu::Display();
+ DisplayMenu()->SetEvent(event);
+
+#ifdef WITH_GTFT
+ cStatus::MsgOsdSetEvent(event);
+#endif
+
+ if (event->Description())
+ cStatus::MsgOsdTextItem(event->Description());
+}
+
+//***************************************************************************
+// Get Next Prev Event (related to current)
+//***************************************************************************
+
+const cEvent* cMenuEpgEvent::getNextPrevEvent(const cEvent* event, int step)
+{
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+ const cChannel* channel = channels->GetByChannelID(event->ChannelID(), true);
+#else
+ cChannels* channels = &Channels;
+ cChannel* channel = channels->GetByChannelID(event->ChannelID(), true);
+#endif
+
+ const cEvent* e = 0;
+
+ if (dispSchedule)
+ {
+ const cSchedule* schedule = schedules->GetSchedule(channel);
+
+ if (schedule && step == -1)
+ e = schedule->Events()->Prev(event);
+ else if (schedule && step == +1)
+ e = schedule->Events()->Next(event);
+ }
+ else
+ {
+ while (!e)
+ {
+ int cn = channel->Number() + step;
+
+ if (cn < 1)
+ cn = channels->MaxNumber();
+ else if (cn > channels->MaxNumber())
+ cn = 1;
+
+ if (!(channel = channels->GetByNumber(cn)))
+ return 0;
+
+ const cSchedule* schedule = schedules->GetSchedule(channel);
+
+ if (schedule)
+ {
+ cUserTimes::UserTime* userTime = menuDb->userTimes->current();
+
+ switch (userTime->getMode())
+ {
+ case cUserTimes::mNow: e = schedule->GetPresentEvent(); break;
+ case cUserTimes::mNext: e = schedule->GetFollowingEvent(); break;
+ case cUserTimes::mTime: e = schedule->GetEventAround(userTime->getTime()); break;
+ }
+ }
+ }
+ }
+
+ return e;
+}
+
+//***************************************************************************
+// Process Key
+//***************************************************************************
+
+eOSState cMenuEpgEvent::ProcessKey(eKeys Key)
+{
+ switch (int(Key))
+ {
+ case kUp|k_Repeat:
+ case kUp:
+ case kDown|k_Repeat:
+ case kDown:
+ case kLeft|k_Repeat:
+ case kLeft:
+ case kRight|k_Repeat:
+ case kRight:
+ {
+ DisplayMenu()->Scroll(NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft, NORMALKEY(Key) == kLeft || NORMALKEY(Key) == kRight);
+ cStatus::MsgOsdTextItem(NULL, NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft);
+ return osContinue;
+ }
+
+ case kInfo: return osBack;
+ default: break;
+ }
+
+ if (Key == kGreen || Key == kYellow)
+ {
+ if (menuDb->userTimes->current()->getMode() == cUserTimes::mSearch)
+ return osContinue;
+
+ const cEvent* e = getNextPrevEvent(event, Key == kGreen ? -1 : +1);
+ int remote = no;
+ int timerMatch = tmNone;
+
+ // get remote and timerMatch info
+
+ menuDb->lookupTimer(e, timerMatch, remote);
+ setEvent(e, timerMatch);
+
+ return osContinue;
+ }
+
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (state == osUnknown)
+ {
+ switch (Key)
+ {
+ case kOk: return osBack;
+ default: break;
+ }
+ }
+
+ return state;
+}
+
+//***************************************************************************
+//***************************************************************************
+// cMenuEpgWhatsOn
+//***************************************************************************
+
+int cMenuEpgWhatsOn::currentChannel = 0;
+const cEvent* cMenuEpgWhatsOn::scheduleEvent = 0;
+
+//***************************************************************************
+// Object
+//***************************************************************************
+
+cMenuEpgWhatsOn::cMenuEpgWhatsOn(const cEvent* aSearchEvent)
+ : cOsdMenu("", CHNUMWIDTH, CHNAMWIDTH, 6, 4)
+{
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+#else
+ schedulesLock = 0;
+#endif
+ schedules = 0;
+ dispSchedule = no;
+ canSwitch = no;
+ helpKeys = 0;
+ helpKeyTime = 0;
+ helpKeyTimeMode = na;
+// timerState = 0;
+ searchEvent = aSearchEvent;
+ menuDb = new cMenuDb;
+
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+ const cChannel* channel = channels->GetByNumber(cDevice::CurrentChannel());
+#else
+ cChannels* channels = &Channels;
+ const cChannel* channel = channels->GetByNumber(cDevice::CurrentChannel());
+#endif
+
+ if (channel)
+ {
+ currentChannel = channel->Number();
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ schedules = cSchedules::GetSchedulesRead(schedulesKey);
+#else
+ schedulesLock = new cSchedulesLock(false);
+ schedules = (cSchedules*)cSchedules::Schedules(*schedulesLock);
+#endif
+ }
+
+// Timers.Modified(timerState);
+
+ if (menuDb->dbConnected())
+ {
+ menuDb->userTimes->first(); // 'whats on now' should be the first
+
+ if (searchEvent)
+ LoadQuery(searchEvent);
+ else if (menuDb->startWithSched)
+ LoadSchedule();
+ else
+ LoadAt();
+ }
+}
+
+cMenuEpgWhatsOn::~cMenuEpgWhatsOn()
+{
+ delete menuDb;
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ if (schedules)
+ schedulesKey.Remove();
+#else
+ tell(3, "LOCK free (cMenuEpgWhatsOn)");
+ delete schedulesLock;
+#endif
+}
+
+//***************************************************************************
+// Load At
+//***************************************************************************
+
+int cMenuEpgWhatsOn::LoadAt()
+{
+ dispSchedule = no;
+ cUserTimes::UserTime* userTime = menuDb->userTimes->current();
+ cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current());
+
+ if (item)
+ {
+ scheduleEvent = item->event;
+
+ if (item->channel)
+ currentChannel = item->channel->Number();
+ }
+
+ if (userTime->getMode() == cUserTimes::mSearch)
+ return LoadSearch(userTime);
+
+ Clear();
+// Timers.Modified(timerState);
+
+ SetMenuCategory(userTime->getMode() == cUserTimes::mNow ? mcScheduleNow : mcScheduleNext);
+
+ if (isEmpty(userTime->getHHMMStr()))
+ SetTitle(cString::sprintf(tr("Overview - %s"), userTime->getTitle()));
+ else
+ SetTitle(cString::sprintf(tr("Overview - %s (%s)"), userTime->getTitle(), userTime->getHHMMStr()));
+
+ tell(2, "DEBUG: Loading events for '%s' %s", userTime->getTitle(), !isEmpty(userTime->getHHMMStr()) ? userTime->getHHMMStr() : "");
+
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+#else
+ const cChannels* channels = &Channels;
+#endif
+
+ // events
+
+ for (const cChannel* Channel = channels->First(); Channel; Channel = channels->Next(Channel))
+ {
+ if (!Channel->GroupSep())
+ {
+ const cSchedule* Schedule = schedules->GetSchedule(Channel);
+ const cEvent* Event = 0;
+
+ if (Schedule)
+ {
+ switch (userTime->getMode())
+ {
+ case cUserTimes::mNow: Event = Schedule->GetPresentEvent(); break;
+ case cUserTimes::mNext: Event = Schedule->GetFollowingEvent(); break;
+ case cUserTimes::mTime: Event = Schedule->GetEventAround(userTime->getTime()); break;
+ }
+ }
+
+ // #TODO
+ // hier ein cEpgEvent für das Event aus der Row erzeugen und dieses anstelle von cEvent für das Menü verwenden
+
+ if (!Event && !Epg2VdrConfig.showEmptyChannels)
+ continue;
+
+ Add(new cMenuEpgScheduleItem(menuDb, Event, Channel), Channel->Number() == currentChannel);
+ }
+ else
+ {
+ if (strlen(Channel->Name()))
+ Add(new cMenuEpgScheduleSepItem(Channel, 0));
+ }
+ }
+
+ Display();
+ SetHelpKeys();
+
+ return done;
+}
+
+//***************************************************************************
+// Load Search
+//***************************************************************************
+
+int cMenuEpgWhatsOn::LoadSearch(const cUserTimes::UserTime* userTime)
+{
+ cDbStatement* select = 0;
+
+ Clear();
+ SetMenuCategory(mcSchedule);
+ SetTitle(cString::sprintf(tr("Overview - %s"), userTime->getTitle()));
+ tell(2, "DEBUG: Loading events for search '%s'", userTime->getSearch());
+
+ menuDb->searchtimerDb->clear();
+ menuDb->searchtimerDb->setValue("NAME", userTime->getSearch());
+
+ if (!menuDb->selectSearchTimerByName->find())
+ return done;
+
+ if (!(select = menuDb->search->prepareSearchStatement(menuDb->searchtimerDb->getRow(), menuDb->useeventsDb)))
+ return fail;
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cChannelsLock channelsLock(false);
+ const cChannels* channels = channelsLock.Channels();
+#else
+ cChannels* channels = &Channels;
+#endif
+
+ menuDb->useeventsDb->clear();
+
+ for (int res = select->find(); res; res = select->fetch())
+ {
+ menuDb->useeventsDb->find(); // get all fields ..
+
+ if (!menuDb->search->matchCriterias(menuDb->searchtimerDb->getRow(), menuDb->useeventsDb->getRow()))
+ continue;
+
+ //
+
+ const char* strChannelId = menuDb->useeventsDb->getStrValue("CHANNELID");
+ const cChannel* channel = channels->GetByChannelID(tChannelID::FromString(strChannelId));
+ const cSchedule* schedule = schedules->GetSchedule(channel);
+ const cEvent* event = !schedule ? 0 : schedule->GetEvent(menuDb->useeventsDb->getIntValue("USEID"));
+
+ if (event)
+ Add(new cMenuEpgScheduleItem(menuDb, event, channel, yes));
+ }
+
+ select->freeResult();
+ menuDb->selectSearchTimerByName->freeResult();
+
+ Display();
+ SetHelpKeys();
+
+ return Count();
+}
+
+//***************************************************************************
+// Load Schedule
+//***************************************************************************
+
+int cMenuEpgWhatsOn::LoadSchedule()
+{
+ Clear();
+ SetCols(7, 6, 4);
+ dispSchedule = yes;
+
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+ const cChannel* channel = channels->GetByNumber(currentChannel);
+#else
+ cChannels* channels = &Channels;
+ const cChannel* channel = channels->GetByNumber(currentChannel);
+#endif
+
+ cMenuEpgScheduleItem::SetSortMode(cMenuEpgScheduleItem::ssmAllThis);
+ SetMenuCategory(mcSchedule);
+ SetTitle(cString::sprintf(tr("Schedule - %s"), channel->Name()));
+
+ if (schedules && channel)
+ {
+ const cSchedule* Schedule = schedules->GetSchedule(channel);
+
+ if (Schedule)
+ {
+ int lastDay = 0;
+ const cEvent* PresentEvent = scheduleEvent ? scheduleEvent : Schedule->GetPresentEvent();
+ time_t now = time(0) - Setup.EPGLinger * 60;
+
+ for (const cEvent* ev = Schedule->Events()->First(); ev; ev = Schedule->Events()->Next(ev))
+ {
+ time_t stime = ev->StartTime();
+ struct tm tmSTime;
+ localtime_r(&stime, &tmSTime);
+
+ if (lastDay != 0 && tmSTime.tm_mday != lastDay)
+ Add(new cMenuEpgScheduleSepItem(0, ev));
+
+ if (ev->EndTime() > now || ev == PresentEvent)
+ Add(new cMenuEpgScheduleItem(menuDb, ev, channel), ev == PresentEvent);
+
+ lastDay = tmSTime.tm_mday;
+ }
+ }
+ }
+
+ Display();
+ SetHelpKeys();
+
+ return done;
+}
+
+//***************************************************************************
+// Load Query
+//***************************************************************************
+
+int cMenuEpgWhatsOn::LoadQuery(const cEvent* searchEvent)
+{
+ Clear();
+ SetMenuCategory(mcSchedule);
+ cMenuEpgScheduleItem::SetSortMode(cMenuEpgScheduleItem::ssmAllThis);
+
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+#else
+ const cChannels* channels = &Channels;
+#endif
+
+ tell(1, "Lookup events with title '%s'", searchEvent->Title());
+
+ for (const cChannel* channel = channels->First(); channel; channel = channels->Next(channel))
+ {
+ const cSchedule* schedule = schedules->GetSchedule(channel);
+
+ if (schedule)
+ {
+ for (const cEvent* event = schedule->Events()->First(); event; event = schedule->Events()->Next(event))
+ {
+ if (strcasecmp(event->Title(), searchEvent->Title()) == 0 &&
+ event->StartTime() > time(0))
+ Add(new cMenuEpgScheduleItem(menuDb, event, channel, yes));
+ }
+ }
+ }
+
+ Sort();
+ SetTitle(cString::sprintf("%d %s - %s", Count(), tr("Search results"), searchEvent->Title()));
+
+ Display();
+ SetHelpKeys();
+
+ return Count();
+}
+
+//***************************************************************************
+// Display
+//***************************************************************************
+
+void cMenuEpgWhatsOn::Display()
+{
+ cOsdMenu::Display();
+
+#ifdef WITH_GTFT
+ if (Count() > 0)
+ {
+ int ni = 0;
+
+ for (cOsdItem *item = First(); item; item = Next(item))
+ cStatus::MsgOsdEventItem(((cMenuEpgScheduleItem*)item)->event, item->Text(), ni++, Count());
+ }
+#endif
+}
+
+//***************************************************************************
+// Update
+//***************************************************************************
+
+bool cMenuEpgWhatsOn::Update()
+{
+ bool result = false;
+
+ for (cOsdItem* item = First(); item; item = Next(item))
+ {
+ if (((cMenuEpgScheduleItem*)item)->Update())
+ result = true;
+ }
+
+ return result;
+}
+
+//***************************************************************************
+// SetHelpKeys
+//***************************************************************************
+
+void cMenuEpgWhatsOn::SetHelpKeys()
+{
+ cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem *)Get(Current());
+ canSwitch = no;
+ int NewHelpKeys = 0;
+
+ if (item)
+ {
+ if (item->timerMatch == tmFull)
+ NewHelpKeys |= 0x02; // "Timer"
+ else
+ NewHelpKeys |= 0x01; // "Record"
+
+ if (!dispSchedule)
+ NewHelpKeys |= 0x04; // "Schedule"
+
+ if (item->channel)
+ {
+ if (item->channel->Number() != cDevice::CurrentChannel())
+ {
+ NewHelpKeys |= 0x10; // "Switch"
+ canSwitch = yes;
+ }
+ }
+ }
+
+ cUserTimes::UserTime* userTimeGreen = !dispSchedule ? menuDb->userTimes->getNext() : menuDb->userTimes->current();
+
+ if (NewHelpKeys != helpKeys || helpKeyTime != userTimeGreen->getTime() || helpKeyTimeMode != userTimeGreen->getMode())
+ {
+ const char* Red[] = { 0, tr("Button$Record"), tr("Button$Timer") };
+
+ if (searchEvent)
+ {
+ helpKeys = 0;
+ helpKeyTime = 0;
+ helpKeyTimeMode = na;
+
+ SetHelp(Red[NewHelpKeys & 0x03], 0, 0, 0);
+ }
+ else
+ {
+ SetHelp(Red[NewHelpKeys & 0x03], // red
+ userTimeGreen->getHelpKey(), // green
+ !dispSchedule
+ ? tr("Button$Schedule") // yellow
+ : menuDb->userTimes->current() == menuDb->userTimes->getFirst()
+ ? tr(menuDb->userTimes->getNext()->getHelpKey()) // yellow
+ : tr(menuDb->userTimes->getFirst()->getHelpKey()), // yellow
+ Epg2VdrConfig.xchgOkBlue ? tr("Button$Info") : canSwitch ? tr("Button$Switch") : 0); // blue
+
+ helpKeyTime = userTimeGreen->getTime();
+ helpKeyTimeMode = userTimeGreen->getMode();
+ helpKeys = NewHelpKeys;
+ }
+ }
+}
+
+//***************************************************************************
+// Switch
+//***************************************************************************
+
+eOSState cMenuEpgWhatsOn::Switch()
+{
+ cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current());
+
+ if (cDevice::PrimaryDevice()->SwitchChannel(item->channel, true))
+ return osEnd;
+
+ Skins.Message(mtError, tr("Can't switch channel!"));
+
+ return osContinue;
+}
+
+//***************************************************************************
+// Record
+//***************************************************************************
+
+eOSState cMenuEpgWhatsOn::Record()
+{
+ int timerMatch = tmNone;
+ cEpgTimer* timer = 0;
+ long timerid = 0;
+ int remote = no;
+ cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current());
+
+ if (!item)
+ return osContinue;
+
+ if (timerid = menuDb->lookupTimer(item->event, timerMatch, remote)) // -> loads timerDb and vdrDb
+ {
+ menuDb->timerDb->clear();
+ menuDb->timerDb->setValue("ID", timerid);
+
+ if (!menuDb->selectTimerById->find())
+ {
+ tell(0, "Fatal, Can't find timer (%ld)", timerid);
+ return osContinue;
+ }
+
+ timer = newTimerObjectFromRow(menuDb->timerDb->getRow(), menuDb->vdrDb->getRow());
+ }
+
+ if (timer)
+ return AddSubMenu(new cMenuEpgEditTimer(menuDb, timer));
+
+ // neuen Timer anlegen
+
+ cDbRow* timerRow = newTimerRowFromEvent(item->event);
+ char timerDefaultVDRuuid[150+TB] = "";
+
+ menuDb->getParameter(menuDb->user.c_str(), "timerDefaultVDRuuid", timerDefaultVDRuuid);
+
+ // Menü bei 'aktuellem' Event Timer Dialog öffen -> #TODO
+
+ if (timer && timer->Event() && timer->Event()->StartTime() < time(0) + NEWTIMERLIMIT)
+ {
+ // timer = newTimerObjectFromRow(timerRow, xxxxx);
+ // return AddSubMenu(new cMenuEpgEditTimer(menuDb, timer));
+ }
+
+ // ansonsten direkt anlegen
+
+ menuDb->createTimer(timerRow, isEmpty(timerDefaultVDRuuid) || Epg2VdrConfig.createTimerLocal ? Epg2VdrConfig.uuid : timerDefaultVDRuuid);
+ delete timerRow;
+
+ if (HasSubMenu())
+ CloseSubMenu();
+
+ sleep(1);
+
+ if (Update())
+ Display();
+
+ SetHelpKeys();
+
+ return osContinue;
+}
+
+//***************************************************************************
+// ProcessKey
+//***************************************************************************
+
+eOSState cMenuEpgWhatsOn::ProcessKey(eKeys Key)
+{
+ bool HadSubMenu = HasSubMenu();
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (!menuDb->dbConnected())
+ return state;
+
+ if (state == osUnknown)
+ {
+ if (searchEvent && Key != kOk && Key != kRed && Key != kRecord)
+ return osContinue;
+
+ if (Epg2VdrConfig.xchgOkBlue && !HasSubMenu())
+ {
+ if (Key == kBlue) Key = kOk;
+ else if (Key == kOk) Key = kBlue;
+ }
+
+ switch (Key)
+ {
+ case k0: // command menu
+ {
+ cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current());
+
+ return AddSubMenu(new cEpgCommandMenu("Commands", menuDb, item));
+ }
+
+ case k1: // search repeat of event
+ {
+ if (Get(Current()))
+ {
+ cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current());
+
+ if (item)
+ return AddSubMenu(new cMenuEpgWhatsOn(item->event));
+ }
+
+ break;
+ }
+
+ case k2: // search matching recordings
+ {
+ if (Get(Current()))
+ {
+ cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current());
+
+ if (item)
+ return AddSubMenu(new cMenuEpgMatchRecordings(menuDb, item->event));
+ }
+
+ break;
+ }
+
+ case k3: // search timer dialog
+ {
+ return AddSubMenu(new cMenuEpgSearchTimers());
+ }
+
+ case k4: // Umschalt Timer erstellen
+ {
+ break;
+ }
+
+ case kRecord:
+ case kRed:
+ return Record();
+
+ case kYellow:
+ {
+ if (!dispSchedule && Get(Current()))
+ {
+ cMenuEpgScheduleItem* mi = (cMenuEpgScheduleItem*)Get(Current());
+
+ if (mi)
+ {
+ scheduleEvent = mi->event;
+ currentChannel = mi->channel->Number();
+ LoadSchedule();
+ }
+ }
+ else
+ {
+ if (menuDb->userTimes->current() == menuDb->userTimes->getFirst())
+ menuDb->userTimes->next();
+ else
+ menuDb->userTimes->first();
+
+ LoadAt();
+ }
+
+ return osContinue;
+ }
+ case kGreen:
+ {
+ if (!dispSchedule)
+ menuDb->userTimes->next();
+
+ LoadAt();
+
+ return osContinue;
+ }
+
+ case kBlue:
+ {
+ if (canSwitch)
+ return Switch();
+
+ break;
+ }
+
+ case kInfo:
+ case kOk:
+ {
+ if (Get(Current()))
+ {
+ cMenuEpgScheduleItem* item = (cMenuEpgScheduleItem*)Get(Current());
+
+ if (item)
+ {
+ const cEvent* event = item->event;
+
+ if (Count() && event)
+ return AddSubMenu(new cMenuEpgEvent(menuDb, event, schedules,
+ item->timerMatch, dispSchedule, canSwitch));
+ }
+ }
+
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ if (!HasSubMenu())
+ {
+ if (HadSubMenu && Update())
+ Display();
+
+ if (Key != kNone)
+ SetHelpKeys();
+ }
+
+ return state;
+}
+
+//***************************************************************************
diff --git a/menusearchtimer.c b/menusearchtimer.c
new file mode 100644
index 0000000..dd59c1b
--- /dev/null
+++ b/menusearchtimer.c
@@ -0,0 +1,381 @@
+/*
+ * menusearchtimer.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <vdr/interface.h>
+
+#include "lib/searchtimer.h"
+#include "menu.h"
+
+//***************************************************************************
+// Class cEpgMenuSearchTimerEdit
+//***************************************************************************
+
+class cEpgMenuSearchTimerEdit : public cOsdMenu
+{
+ public:
+
+ cEpgMenuSearchTimerEdit(cMenuDb* db, long id, bool New = no);
+ virtual ~cEpgMenuSearchTimerEdit();
+
+ virtual eOSState ProcessKey(eKeys Key);
+
+ protected:
+
+ cMenuDb* menuDb;
+};
+
+cEpgMenuSearchTimerEdit::cEpgMenuSearchTimerEdit(cMenuDb* db, long id, bool New)
+ : cOsdMenu(tr("Edit Search Timer"), 12)
+{
+ menuDb = db;
+ menuDb->searchtimerDb->clear();
+ menuDb->searchtimerDb->setValue("ID", id);
+
+ if (!menuDb->searchtimerDb->find())
+ return;
+
+ cDbTableDef* def = menuDb->searchtimerDb->getTableDef();
+
+ for (int i = 0; i < def->fieldCount(); i++)
+ {
+ cDbValue* value = menuDb->searchtimerDb->getValue(def->getField(i)->getName());
+ const char* name = def->getField(i)->getDescription();
+
+ if (!value || def->getField(i)->hasType(cDBS::ftMeta))
+ continue;
+
+ if (isEmpty(name))
+ name = def->getField(i)->getName();
+
+ if (def->getField(i)->hasFormat(cDBS::ffAscii))
+ Add(new cMenuEditStrItem(tr(name), (char*)value->getStrValueRef(), def->getField(i)->getSize()));
+
+ else if (def->getField(i)->hasFormat(cDBS::ffInt))
+ Add(new cMenuEditIntItem(tr(name), (int*)value->getIntValueRef()));
+ }
+
+ Display();
+}
+
+cEpgMenuSearchTimerEdit::~cEpgMenuSearchTimerEdit()
+{
+}
+
+eOSState cEpgMenuSearchTimerEdit::ProcessKey(eKeys Key)
+{
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ return state;
+}
+
+//***************************************************************************
+// Class cEpgMenuSearchTimerItem
+//***************************************************************************
+
+class cEpgMenuSearchTimerItem : public cOsdItem
+{
+ public:
+
+ cEpgMenuSearchTimerItem(long aId, int aActive, const char* text)
+ {
+ id = aId;
+ active = aActive;
+ SetText(text);
+ }
+
+ ~cEpgMenuSearchTimerItem() { }
+
+ long getId() { return id; }
+ int isActive() { return active; }
+
+ protected:
+
+ long id;
+ int active;
+};
+
+//***************************************************************************
+//***************************************************************************
+// Class cEpgMenuSearchResult
+//***************************************************************************
+
+class cEpgMenuSearchResult : public cOsdMenu
+{
+ public:
+
+ cEpgMenuSearchResult(cMenuDb* db, long id);
+ virtual ~cEpgMenuSearchResult();
+ virtual eOSState ProcessKey(eKeys Key);
+
+ protected:
+
+ int refresh(long id);
+
+ cMenuDb* menuDb;
+};
+
+cEpgMenuSearchResult::cEpgMenuSearchResult(cMenuDb* db, long id)
+ : cOsdMenu(tr("Edit Search Timer"), 17, CHNAMWIDTH, 3, 30)
+{
+ menuDb = db;
+ refresh(id);
+}
+
+cEpgMenuSearchResult::~cEpgMenuSearchResult()
+{
+}
+
+//***************************************************************************
+// Refresh
+//***************************************************************************
+
+int cEpgMenuSearchResult::refresh(long id)
+{
+ cDbStatement* select = 0;
+
+ menuDb->searchtimerDb->setValue("ID", id);
+
+ if (!menuDb->searchtimerDb->find())
+ return done;
+
+ if (!(select = menuDb->search->prepareSearchStatement(menuDb->searchtimerDb->getRow(), menuDb->useeventsDb)))
+ return fail;
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cChannelsLock channelsLock(false);
+ const cChannels* channels = channelsLock.Channels();
+#else
+ cChannels* channels = &Channels;
+#endif
+
+ menuDb->useeventsDb->clear();
+
+ for (int res = select->find(); res; res = select->fetch())
+ {
+ int cnt = 0;
+ cDbStatement* selectDones = 0;
+
+ menuDb->useeventsDb->find(); // get all fields ..
+
+ if (!menuDb->search->matchCriterias(menuDb->searchtimerDb->getRow(), menuDb->useeventsDb->getRow()))
+ continue;
+
+ // dones ...
+
+ if (menuDb->search->prepareDoneSelect(menuDb->useeventsDb->getRow(),
+ menuDb->searchtimerDb->getIntValue("REPEATFIELDS"),
+ selectDones) == success
+ && selectDones)
+ {
+ // first only count - #TODO show them in a sub-menu oder let delete by kYellow?
+
+ for (int f = selectDones->find(); f; f = selectDones->fetch())
+ cnt++;
+
+ selectDones->freeResult();
+ }
+
+ //
+
+ const char* strChannelId = menuDb->useeventsDb->getStrValue("CHANNELID");
+ const cChannel* channel = channels->GetByChannelID(tChannelID::FromString(strChannelId));
+
+ Add(new cEpgMenuTextItem(menuDb->useeventsDb->getIntValue("USEID"),
+ cString::sprintf("%s\t%s\t%s\t%s\t%s",
+ l2pTime(menuDb->useeventsDb->getIntValue("STARTTIME")).c_str(),
+ channel->Name(),
+ cnt ? num2Str(cnt).c_str() : "",
+ menuDb->useeventsDb->getStrValue("TITLE"),
+ menuDb->useeventsDb->getStrValue("SHORTTEXT"))));
+ }
+
+ select->freeResult();
+ menuDb->searchtimerDb->reset();
+
+ SetHelp(0, 0, "Delete dones", 0);
+ Display();
+
+ return success;
+}
+
+//***************************************************************************
+// Process Key
+//***************************************************************************
+
+eOSState cEpgMenuSearchResult::ProcessKey(eKeys Key)
+{
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (state == osUnknown)
+ {
+ switch (Key)
+ {
+ case kYellow:
+ {
+ cDbStatement* selectDones = 0;
+ cEpgMenuTextItem* item = (cEpgMenuTextItem*)Get(Current());
+
+ if (item && Interface->Confirm(tr("Remove all done entries of this event?")))
+ {
+ menuDb->useeventsDb->clear();
+ menuDb->useeventsDb->setValue("USEID", item->getId());
+
+ if (menuDb->useeventsDb->find())
+ {
+ if (menuDb->search->prepareDoneSelect(menuDb->useeventsDb->getRow(),
+ menuDb->searchtimerDb->getIntValue("REPEATFIELDS"),
+ selectDones) == success
+ && selectDones)
+ {
+ for (int f = selectDones->find(); f; f = selectDones->fetch())
+ selectDones->getTable()->deleteWhere("id = %ld", selectDones->getTable()->getIntValue("ID"));
+
+ selectDones->freeResult();
+ }
+ }
+ }
+ }
+
+ default: break;
+ }
+ }
+
+ return state;
+}
+
+//***************************************************************************
+// Class cMenuEpgSearchTimers
+//***************************************************************************
+//***************************************************************************
+// Object
+//***************************************************************************
+
+cMenuEpgSearchTimers::cMenuEpgSearchTimers()
+ : cOsdMenu(tr("Search Timers"), 2, 6)
+{
+ menuDb = new cMenuDb;
+ refresh();
+}
+
+cMenuEpgSearchTimers::~cMenuEpgSearchTimers()
+{
+ delete menuDb;
+}
+
+int cMenuEpgSearchTimers::refresh()
+{
+ int current = Current();
+
+ menuDb->searchtimerDb->clear();
+
+ Clear();
+
+ for (int f = menuDb->selectSearchTimers->find(); f && menuDb->dbConnected(); f = menuDb->selectSearchTimers->fetch())
+ {
+ char* buf;
+ asprintf(&buf, "%c\t%ld\t%s",
+ menuDb->searchtimerDb->getIntValue("ACTIVE") ? '>' : ' ',
+ menuDb->searchtimerDb->getIntValue("HITS"),
+ menuDb->searchtimerDb->getStrValue("EXPRESSION"));
+ Add(new cEpgMenuSearchTimerItem(menuDb->searchtimerDb->getIntValue("ID"),
+ menuDb->searchtimerDb->getIntValue("ACTIVE"), buf));
+ free(buf);
+ }
+
+ menuDb->selectSearchTimers->freeResult();
+
+ Sort();
+ SetCurrent(current >= 0 ? Get(current) : First());
+
+ setHelpKeys();
+ Display();
+
+ return done;
+}
+
+void cMenuEpgSearchTimers::setHelpKeys()
+{
+ cEpgMenuSearchTimerItem* item = (cEpgMenuSearchTimerItem*)Get(Current());
+
+ if (item)
+ SetHelp(item->isActive() ? tr("Deactivate") : tr("Activate"),
+ tr("Test"),
+ tr("Delete"),
+ 0);
+ else
+ SetHelp(0, 0, 0, 0);
+
+ return ;
+}
+
+//***************************************************************************
+// Process Key
+//***************************************************************************
+
+eOSState cMenuEpgSearchTimers::ProcessKey(eKeys Key)
+{
+ eOSState state = cOsdMenu::ProcessKey(Key);
+ cEpgMenuSearchTimerItem* item = (cEpgMenuSearchTimerItem*)Get(Current());
+
+ if (state == osUnknown)
+ {
+ switch (Key)
+ {
+ case kOk:
+ {
+ if (HasSubMenu() || Count() == 0)
+ return osContinue;
+
+ return osContinue; // #TODO
+ return AddSubMenu(new cEpgMenuSearchTimerEdit(menuDb, item->getId()));
+ }
+
+ case kRed:
+ {
+ menuDb->searchtimerDb->setValue("ID", item->getId());
+
+ if (menuDb->searchtimerDb->find())
+ {
+ menuDb->searchtimerDb->setValue("ACTIVE", !menuDb->searchtimerDb->getIntValue("ACTIVE"));
+ menuDb->searchtimerDb->update();
+ refresh();
+ }
+
+ return osContinue;
+ }
+
+ case kGreen:
+ {
+ return AddSubMenu(new cEpgMenuSearchResult(menuDb, item->getId()));
+ }
+
+ case kYellow:
+ {
+ if (item && Interface->Confirm(tr("Delete search timer?")))
+ {
+ menuDb->searchtimerDb->setValue("ID", item->getId());
+
+ if (menuDb->searchtimerDb->find())
+ {
+ menuDb->searchtimerDb->setCharValue("STATE", 'D');
+ menuDb->searchtimerDb->update();
+ refresh();
+ }
+ }
+
+ return osContinue;
+ }
+
+ default: break;
+ }
+ }
+
+ if (!HasSubMenu() && Key != kNone)
+ setHelpKeys();
+
+ return state;
+}
diff --git a/menutimers.c b/menutimers.c
new file mode 100644
index 0000000..011cc39
--- /dev/null
+++ b/menutimers.c
@@ -0,0 +1,572 @@
+/*
+ * menutimers.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <vdr/menu.h>
+#include <vdr/interface.h>
+
+#include "plgconfig.h"
+#include "menu.h"
+
+//***************************************************************************
+// cMenuEpgEditTimer
+//***************************************************************************
+
+cMenuEpgEditTimer::cMenuEpgEditTimer(cMenuDb* db, cEpgTimer* Timer, bool New)
+ : cOsdMenu(tr("Edit Timer"), 12),
+ data(db)
+{
+ SetMenuCategory(mcTimerEdit);
+ file = 0;
+ day = firstday = 0;
+ menuDb = db;
+
+ if (Timer)
+ {
+ data.fromTimer(Timer);
+
+ channelNr = data.channel->Number();
+ Add(new cMenuEditBitItem(tr("Active"), &data.flags, tfActive));
+
+ if (menuDb->vdrCount)
+ Add(new cMenuEditStraItem(tr("VDR"), &data.vdrIndex, menuDb->vdrCount, menuDb->vdrList));
+
+ Add(new cMenuEditChanItem(tr("Channel"), &channelNr));
+ Add(day = new cMenuEditDateItem(tr("Day"), &data.day, &data.weekdays));
+ Add(new cMenuEditTimeItem(tr("Start"), &data.start));
+ Add(new cMenuEditTimeItem(tr("Stop"), &data.stop));
+ Add(new cMenuEditBitItem(tr("VPS"), &data.flags, tfVps));
+ Add(new cMenuEditIntItem(tr("Priority"), &data.priority, 0, MAXPRIORITY));
+ Add(new cMenuEditIntItem(tr("Lifetime"), &data.lifetime, 0, MAXLIFETIME));
+
+#ifdef WITH_PIN
+ if (cOsd::pinValid || !data.fskProtection)
+ Add(new cMenuEditBoolItem(tr("Childlock"), &data.fskProtection));
+ else
+ Add(new cOsdItem(cString::sprintf("%s\t%s", tr("Childlock"), data.fskProtection ? trVDR("yes") : trVDR("no"))));
+#endif
+
+ Add(file = new cMenuEditStrItem(tr("File"), data.file, sizeof(data.file)));
+ SetFirstDayItem();
+
+ Add(new cOsdItem(cString::sprintf("------- %s ----------", tr("Information"))));
+ cList<cOsdItem>::Last()->SetSelectable(false);
+ Add(new cOsdItem(cString::sprintf("Status:\t%s", toName((TimerState)data.state))));
+
+ if (!isEmpty(data.stateInfo))
+ Add(new cOsdItem(cString::sprintf("\t%s", data.stateInfo)));
+
+ Add(new cOsdItem(cString::sprintf("Pending action:\t%s", toName((TimerAction)data.action, yes))));
+ }
+
+ SetHelpKeys();
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+#else
+ Timers.IncBeingEdited();
+#endif
+}
+
+cMenuEpgEditTimer::~cMenuEpgEditTimer()
+{
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+#else
+ Timers.DecBeingEdited();
+#endif
+}
+
+void cMenuEpgEditTimer::SetHelpKeys(void)
+{
+ SetHelp(tr("Button$Folder"), data.weekdays ? tr("Button$Single") : tr("Button$Repeating"));
+}
+
+void cMenuEpgEditTimer::SetFirstDayItem(void)
+{
+ if (!firstday && data.weekdays)
+ {
+ Add(firstday = new cMenuEditDateItem(tr("First day"), &data.day));
+ Display();
+ }
+ else if (firstday && !data.weekdays)
+ {
+ Del(firstday->Index());
+ firstday = 0;
+ Display();
+ }
+}
+
+eOSState cMenuEpgEditTimer::SetFolder(void)
+{
+ if (cMenuFolder* mf = dynamic_cast<cMenuFolder*>(SubMenu()))
+ {
+ cString Folder = mf->GetFolder();
+ const char *p = strrchr(data.file, FOLDERDELIMCHAR);
+
+ if (p)
+ p++;
+ else
+ p = data.file;
+
+ if (!isempty(*Folder))
+ strn0cpy(data.file, cString::sprintf("%s%c%s", *Folder, FOLDERDELIMCHAR, p), sizeof(data.file));
+ else if (p != data.file)
+ memmove(data.file, p, strlen(p) + 1);
+
+ SetCurrent(file);
+ Display();
+ }
+
+ return CloseSubMenu();
+}
+
+eOSState cMenuEpgEditTimer::ProcessKey(eKeys Key)
+{
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (state == osUnknown)
+ {
+ switch (Key)
+ {
+ case kOk:
+ {
+ int recording = no;
+
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannel* ch = Channels->GetByNumber(channelNr);
+#else
+ cChannel* ch = Channels.GetByNumber(channelNr);
+#endif
+
+ if (!ch)
+ {
+ Skins.Message(mtError, tr("*** Invalid Channel ***"));
+ break;
+ }
+
+ data.channel = ch;
+
+ if (!*data.file)
+ strcpy(data.file, data.channel->ShortName(true));
+
+ cDbRow timerRow("timers");
+
+ data.toRow(&timerRow);
+
+ // get actual timer state from database
+
+ menuDb->timerDb->clear();
+ menuDb->timerDb->setValue("ID", data.TimerId());
+ menuDb->timerDb->setValue("VDRUUID", data.getLastVdrUuid());
+
+ if (menuDb->timerDb->find() && menuDb->timerDb->hasCharValue("STATE", tsRunning))
+ recording = yes;
+
+ menuDb->timerDb->reset();
+
+ // ask for confirmation if timer running!
+
+ if (recording && strcmp(data.getVdrUuid(), data.getLastVdrUuid()) != 0)
+ {
+ if (!Interface->Confirm(tr("Timer still recording - really move to other VDR?")))
+ return osContinue;
+ }
+
+ menuDb->modifyTimer(&timerRow, data.getVdrUuid());
+
+ return osBack;
+ }
+
+ case kRed:
+ return AddSubMenu(new cMenuFolder(tr("Select folder"), &Folders, data.file));
+
+ case kGreen:
+ {
+ if (day)
+ {
+ day->ToggleRepeating();
+ SetCurrent(day);
+ SetFirstDayItem();
+ SetHelpKeys();
+ Display();
+ }
+
+ return osContinue;
+ }
+
+ case kYellow:
+ case kBlue: return osContinue;
+
+ default: break;
+ }
+ }
+ else if (state == osEnd && HasSubMenu())
+ state = SetFolder();
+
+ if (Key != kNone)
+ SetFirstDayItem();
+
+ return state;
+}
+
+//***************************************************************************
+// cMenuEpgTimerItem
+//***************************************************************************
+
+class cMenuEpgTimerItem : public cOsdItem
+{
+ public:
+
+ cMenuEpgTimerItem(cEpgTimer* Timer);
+ ~cMenuEpgTimerItem();
+
+ virtual int Compare(const cListObject &ListObject) const;
+ virtual void Set();
+ cEpgTimer* Timer() { return timer; }
+ virtual void SetMenuItem(cSkinDisplayMenu* DisplayMenu, int Index,
+ bool Current, bool Selectable);
+
+ private:
+
+ cEpgTimer* timer;
+};
+
+cMenuEpgTimerItem::cMenuEpgTimerItem(cEpgTimer* Timer)
+{
+ timer = Timer;
+ Set();
+}
+
+cMenuEpgTimerItem::~cMenuEpgTimerItem()
+{
+ delete timer;
+}
+
+int cMenuEpgTimerItem::Compare(const cListObject &ListObject) const
+{
+ return timer->Compare(*((cMenuEpgTimerItem*)&ListObject)->timer);
+}
+
+void cMenuEpgTimerItem::Set()
+{
+ cString day, dayName("");
+
+ if (timer->WeekDays())
+ {
+ day = timer->PrintDay(0, timer->WeekDays(), false);
+ }
+ else if (timer->Day() - time(0) < 28 * SECSINDAY)
+ {
+ day = itoa(timer->GetMDay(timer->Day()));
+ dayName = WeekDayName(timer->Day());
+ }
+ else
+ {
+ struct tm tm_r;
+ time_t Day = timer->Day();
+ localtime_r(&Day, &tm_r);
+ char buffer[16];
+ strftime(buffer, sizeof(buffer), "%Y%m%d", &tm_r);
+ day = buffer;
+ }
+
+ const char* vdr = timer->VdrName();
+ const char* File = Setup.FoldersInTimerMenu ? 0 : strrchr(timer->File(), FOLDERDELIMCHAR);
+
+ if (File && strcmp(File + 1, TIMERMACRO_TITLE) && strcmp(File + 1, TIMERMACRO_EPISODE))
+ File++;
+ else
+ File = timer->File();
+
+ SetText(cString::sprintf("%c\t%s%c\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s",
+ !(timer->HasFlags(tfActive)) ? ' ' : timer->FirstDay() ? '!' : timer->Recording() ? '#' : '>',
+ vdr, timer->isVdrRunning() ? '*' : ' ',
+ timer->Channel() ? timer->Channel()->Number() : na,
+ *dayName, *dayName && **dayName ? " " : "", *day,
+ timer->Start() / 100, timer->Start() % 100,
+ timer->Stop() / 100, timer->Stop() % 100,
+ File));
+}
+
+void cMenuEpgTimerItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable)
+{
+ if (!DisplayMenu->SetItemTimer(timer, Index, Current, Selectable))
+ DisplayMenu->SetItem(Text(), Index, Current, Selectable);
+}
+
+//***************************************************************************
+// Class cMenuEpgTimers
+//***************************************************************************
+//***************************************************************************
+// Object
+//***************************************************************************
+
+cMenuEpgTimers::cMenuEpgTimers()
+ : cOsdMenu(tr("Timers"), 2, CHNUMWIDTH, 10, 6, 6)
+{
+ helpKeys = -1;
+ timersMaxUpdsp = 0;
+
+ menuDb = new cMenuDb;
+
+ SetMenuCategory(mcTimer);
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+#else
+ Timers.IncBeingEdited();
+#endif
+
+ refresh();
+}
+
+cMenuEpgTimers::~cMenuEpgTimers()
+{
+ delete menuDb;
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+#else
+ Timers.DecBeingEdited();
+#endif
+}
+
+//***************************************************************************
+// Refresh
+//***************************************************************************
+
+int cMenuEpgTimers::refresh()
+{
+ int current = Current();
+
+ menuDb->timerDb->clear();
+ menuDb->vdrDb->clear();
+
+ menuDb->timerDb->clear();
+ menuDb->selectMaxUpdSp->execute();
+ timersMaxUpdsp = menuDb->timerDb->getIntValue("UPDSP");
+ menuDb->selectMaxUpdSp->freeResult();
+
+ Clear();
+
+ menuDb->timerState.setValue("P,R,u");
+ menuDb->timerAction.setValue("C,M,A,F,a");
+
+ for (int f = menuDb->selectTimers->find(); f && menuDb->dbConnected(); f = menuDb->selectTimers->fetch())
+ {
+ cEpgTimer* t = newTimerObjectFromRow(menuDb->timerDb->getRow(), menuDb->vdrDb->getRow());
+
+ if (t)
+ Add(new cMenuEpgTimerItem(t));
+ }
+
+ menuDb->selectTimers->freeResult();
+
+ Sort();
+ SetCurrent(current >= 0 ? Get(current) : First());
+ SetHelpKeys();
+ Display();
+
+ return done;
+}
+
+cEpgTimer* cMenuEpgTimers::currentTimer()
+{
+ cMenuEpgTimerItem* item = (cMenuEpgTimerItem*)Get(Current());
+ return item ? item->Timer() : 0;
+}
+
+void cMenuEpgTimers::SetHelpKeys()
+{
+ int newHelpKeys = 0;
+ cEpgTimer* timer = currentTimer();
+
+ if (timer)
+ {
+ if (timer->Event())
+ newHelpKeys = 2;
+ else
+ newHelpKeys = 1;
+ }
+
+ if (newHelpKeys != helpKeys)
+ {
+ helpKeys = newHelpKeys;
+ SetHelp(helpKeys > 0 ? tr("Button$On/Off") : 0,
+ tr("Button$New"),
+ helpKeys > 0 ? tr("Button$Delete") : 0,
+ helpKeys == 2 ? tr("Button$Info") : 0);
+ }
+}
+
+eOSState cMenuEpgTimers::toggleState()
+{
+ if (HasSubMenu())
+ return osContinue;
+
+ cEpgTimer* timer = currentTimer();
+
+ if (timer)
+ {
+ menuDb->timerDb->clear();
+ menuDb->timerDb->setValue("ID", timer->TimerId());
+ menuDb->timerDb->setValue("VDRUUID", timer->VdrUuid());
+
+ if (menuDb->timerDb->find())
+ {
+ menuDb->timerDb->setCharValue("ACTION", taModify);
+ menuDb->timerDb->getValue("STATE")->setNull();
+ menuDb->timerDb->setValue("SOURCE", Epg2VdrConfig.uuid);
+ menuDb->timerDb->setValue("ACTIVE", !menuDb->timerDb->getIntValue("ACTIVE"));
+ menuDb->timerDb->update();
+ menuDb->triggerVdrs("TIMERJOB"); // trigger all!
+
+ tell(0, "Timer %s %sactivated", *timer->ToDescr(), menuDb->timerDb->getIntValue("ACTIVE") ? "" : "de");
+ }
+ }
+
+ return osUser1;
+}
+
+eOSState cMenuEpgTimers::edit()
+{
+ if (HasSubMenu() || Count() == 0)
+ return osContinue;
+
+ tell(0, "Editing timer %s", *currentTimer()->ToDescr());
+
+ return AddSubMenu(new cMenuEpgEditTimer(menuDb, currentTimer()));
+}
+
+eOSState cMenuEpgTimers::create()
+{
+ if (HasSubMenu())
+ return osContinue;
+
+ return AddSubMenu(new cMenuEpgEditTimer(menuDb, new cEpgTimer, yes));
+}
+
+eOSState cMenuEpgTimers::remove()
+{
+ int recording = no;
+ cEpgTimer* ti = currentTimer();
+
+ if (!ti)
+ return osContinue;
+
+ if (!Interface->Confirm(tr("Delete timer?")))
+ return osContinue;
+
+ // get actual state from database
+
+ menuDb->timerDb->clear();
+ menuDb->timerDb->setValue("ID", ti->TimerId());
+ menuDb->timerDb->setValue("VDRUUID", ti->VdrUuid());
+
+ if (menuDb->timerDb->find() && menuDb->timerDb->hasCharValue("STATE", tsRunning))
+ recording = yes;
+
+ menuDb->timerDb->reset();
+
+ // check
+
+ if (recording && !Interface->Confirm(tr("Timer still recording - really delete?")))
+ return osContinue;
+
+ // request timer deletion
+
+ tell(0, "Deleting timer %s", *ti->ToDescr());
+
+ menuDb->deleteTimer(currentTimer()->TimerId());
+
+ return osUser1;
+}
+
+eOSState cMenuEpgTimers::info()
+{
+ if (HasSubMenu() || !Count())
+ return osContinue;
+
+ cEpgTimer* ti = currentTimer();
+
+ if (ti && ti->Event())
+ {
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ LOCK_TIMERS_READ;
+ LOCK_CHANNELS_READ;
+ return AddSubMenu(new cMenuEvent(Timers, Channels, ti->Event()));
+#else
+ return AddSubMenu(new cMenuEvent(ti->Event()));
+#endif
+ }
+
+ return osContinue;
+}
+
+eOSState cMenuEpgTimers::ProcessKey(eKeys Key)
+{
+ eOSState state = cOsdMenu::ProcessKey(Key);
+
+ if (state == osUnknown)
+ {
+ switch (Key)
+ {
+ case kInfo:
+ case kBlue: return info();
+
+ case kOk: state = edit(); break;
+ case kGreen: state = create(); break;
+ case kYellow: state = remove(); break;
+ case kRed: state = toggleState(); break;
+
+ case k1: // search repetition of event
+ {
+ cEpgTimer* timer = currentTimer();
+
+ if (timer->Event())
+ return AddSubMenu(new cMenuEpgWhatsOn(timer->Event()));
+
+ break;
+ }
+
+ default: break;
+ }
+ }
+
+ if (!HasSubMenu() && Key != kNone && Key != kUp && Key != kDown)
+ {
+ int updSp = na;
+
+ SetHelpKeys();
+
+ if (state != osUser1)
+ {
+ menuDb->timerDb->clear();
+ menuDb->selectMaxUpdSp->execute();
+ updSp = menuDb->timerDb->getIntValue("UPDSP");
+ menuDb->selectMaxUpdSp->freeResult();
+ }
+ else
+ {
+ usleep(300000); // give VDRs a chance to process the last request
+ }
+
+ // new timer created or deleted ... ?
+
+ if (state == osUser1 || (updSp != na && timersMaxUpdsp != updSp))
+ {
+ if (updSp != na)
+ timersMaxUpdsp = updSp;
+
+ refresh();
+
+ tell(2, "DEBUG: maxUpdSp = %d; osUser1 = '%s'", timersMaxUpdsp,
+ state == osUser1 ? "Y" : "N");
+ }
+
+ if (state == osUser1)
+ state = osContinue;
+ }
+
+ return state;
+}
diff --git a/parameters.c b/parameters.c
new file mode 100644
index 0000000..05b0eba
--- /dev/null
+++ b/parameters.c
@@ -0,0 +1,253 @@
+/*
+ * parameters.c
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "plgconfig.h"
+#include "parameters.h"
+
+//***************************************************************************
+// Parameters
+//***************************************************************************
+
+cParameters::Parameter cParameters::parameters[] =
+{
+ // owner, name, type, default, regexp, readonly, visible
+
+#ifndef VDR_PLUGIN
+ // --------------------------------------------
+ // epgd / epghttpd
+
+ { "epgd", "manualTimer2Done", ptBool, "1", "^[01]$", no, yes },
+ { "epgd", "timerJobFailedHistory", ptNum, "10", "^[0-9]{1,3}$", no, yes },
+
+ { "epgd", "logoUpperCase", ptBool, "0", "^[01]$", no, yes },
+ { "epgd", "logoById", ptBool, "0", "^[01]$", no, yes },
+ { "epgd", "logoSuffix", ptAscii, "png", "^.{1,5}$", no, yes },
+
+ { "epgd", "mailScript", ptAscii, BINDEST"/epgd-sendmail", "^.{0,150}$", no, yes },
+
+ { "epgd", "mergeStart", ptTime, "0", "^[0-9]{1,20}$", yes, no },
+ { "epgd", "lastMergeAt", ptTime, "0", "^[0-9]{1,20}$", yes, no },
+ { "epgd", "lastScrRefUpdate", ptTime, "0", "^[0-9]{1,20}$", yes, no },
+ { "epgd", "lastEpisodeRun", ptTime, "0", "^[0-9]{1,20}$", yes, no },
+ { "epgd", "lastEpisodeFullRun", ptTime, "0", "^[0-9]{1,20}$", yes, no },
+
+ { "epgd", "maxEventTime", ptTime, "0", "^[0-9]{1,20}$", yes, yes },
+ { "epgd", "minEventTime", ptTime, "0", "^[0-9]{1,20}$", yes, yes },
+
+ { "epgd", "md5", ptAscii, "", "^.{0,150}$", yes, no },
+
+#endif
+
+ // --------------------------------------------
+ // epg2vdr / scraper2vdr
+
+ { "uuid", "lastEventsUpdateAt", ptNum, NULL, "^[0-9]{1,20}$", yes, yes },
+
+ // --------------------------------------------
+ // webif
+
+ { "webif", "needLogin", ptBool, "0", "^[01]$", no, yes },
+
+ // --------------------------------------------
+ // user
+
+ { "@", "chFormat", ptAscii, "", "^.{0,150}$", no, yes },
+ { "@", "defaultVDRuuid", ptAscii, "", "^.{0,150}$", no, yes },
+ { "@", "startPage", ptAscii, "menu_magazine", "^.{0,150}$", no, yes },
+ { "@", "timerDefaultVDRuuid", ptAscii, "", "^.{0,150}$", no, yes },
+ { "@", "quickTimes", ptAscii, "Now=@Now~Next=@Next~PrimeTime=20:15~LateNight=00:00", "^(~?[^=]+=(([0-1]?[0-9]|2[0-4]):[0-5]?[0-9]|@Now|@Next))*$", no, yes },
+ { "@", "startWithSched", ptBool, "0", "^[01]$", no, yes },
+ { "@", "searchAdv", ptBool, "1", "^[01]$", no, yes },
+ { "@", "namingModeSerie", ptNum, "1", "^[0-5]$", no, yes },
+ { "@", "namingModeSearchSerie", ptNum, "1", "^[0-5]$", no, yes },
+ { "@", "namingModeMovie", ptNum, "1", "^[0-5]$", no, yes },
+ { "@", "namingModeSearchMovie", ptNum, "1", "^[0-5]$", no, yes },
+ { "@", "pickerFirstDay", ptNum, "1", "^[0-6]$", no, yes },
+ { "@", "sendTCC", ptBool, "1", "^[01]$", no, yes },
+ { "@", "constabelLoginPath", ptAscii, "", "^.{0,150}$", no, yes },
+ { "@", "mailReceiver", ptAscii, "", "^.{0,150}$", no, yes },
+
+ { 0, 0, 0, 0, 0, 0, 0 }
+};
+
+//***************************************************************************
+// Get Definition
+//***************************************************************************
+
+cParameters::Parameter* cParameters::getDefinition(const char* owner, const char* name)
+{
+ if (isEmpty(owner) || isEmpty(name))
+ return 0;
+
+ // some specials for dynamic owner or name
+
+ if (owner[0] == '@')
+ owner = "@";
+
+ if (strcmp(owner, "epgd") == 0 && strstr(name, ".md5"))
+ name = "md5";
+
+ // lookup
+
+ for (int i = 0; parameters[i].name != 0; i++)
+ {
+ if (strcmp(parameters[i].owner, owner) == 0 && strcasecmp(parameters[i].name, name) == 0)
+ return &parameters[i];
+ }
+
+ tell(0, "Warning: Requested parameter '%s/%s' not known, ignoring", owner, name);
+
+ return 0;
+}
+
+//***************************************************************************
+// Object / Init / Exit
+//***************************************************************************
+
+cParameters::cParameters()
+{
+ parametersDb = 0;
+ selectParameters = 0;
+}
+
+int cParameters::initDb(cDbConnection* connection)
+{
+ int status = success;
+
+ parametersDb = new cDbTable(connection, "parameters");
+ if (parametersDb->open() != success) return fail;
+
+ // ----------
+ // select * from parameters
+ // where owner = ?
+
+ selectParameters = new cDbStatement(parametersDb);
+
+ selectParameters->build("select ");
+ selectParameters->bindAllOut();
+ selectParameters->build(" from %s", parametersDb->TableName());
+
+ status += selectParameters->prepare();
+
+ return status;
+}
+
+int cParameters::exitDb()
+{
+ delete parametersDb; parametersDb = 0;
+ delete selectParameters; selectParameters = 0;
+
+ return done;
+}
+
+//***************************************************************************
+// Get String Parameter
+//***************************************************************************
+
+int cParameters::getParameter(const char* owner, const char* name, char* value)
+{
+ int found;
+ Parameter* definition = getDefinition(owner, name);
+
+ if (value)
+ *value = 0;
+
+ if (!definition)
+ return no;
+
+ if (strcasecmp(owner, "uuid") == 0)
+ owner = Epg2VdrConfig.uuid;
+
+ parametersDb->clear();
+ parametersDb->setValue("OWNER", owner);
+ parametersDb->setValue("NAME", name);
+
+ if (found = parametersDb->find())
+ {
+ if (value)
+ sprintf(value, "%s", parametersDb->getStrValue("Value"));
+ }
+ else if (value && definition->def)
+ {
+ sprintf(value, "%s", definition->def);
+ found = yes;
+ setParameter(owner, name, value);
+ }
+
+ parametersDb->reset();
+
+ return found;
+}
+
+//***************************************************************************
+// Get Integer Parameter
+//***************************************************************************
+
+int cParameters::getParameter(const char* owner, const char* name, long int& value)
+{
+ char txt[100]; *txt = 0;
+ int found;
+
+ found = getParameter(owner, name, txt);
+
+ if (!isEmpty(txt))
+ value = atol(txt);
+ else
+ value = 0;
+
+ return found;
+}
+
+//***************************************************************************
+// Set String Parameter
+//***************************************************************************
+
+int cParameters::setParameter(const char* owner, const char* name, const char* value)
+{
+ Parameter* definition = getDefinition(owner, name);
+
+ if (!definition)
+ return fail;
+
+ if (!value)
+ return fail;
+
+ if (strcasecmp(owner, "uuid") == 0)
+ owner = Epg2VdrConfig.uuid;
+
+ tell(2, "Storing '%s' for '%s' with value '%s'", name, owner, value);
+
+ // validate parameter
+
+ if (definition->regexp)
+ {
+ if (rep(value, definition->regexp) != success)
+ {
+ tell(0, "Ignoring '%s' for parameter '%s/%s' don't match expression '%s'",
+ value, owner, name, definition->regexp);
+
+ return fail;
+ }
+ }
+
+ parametersDb->clear();
+ parametersDb->setValue("OWNER", owner);
+ parametersDb->setValue("NAME", name);
+ parametersDb->setValue("VALUE", value);
+
+ return parametersDb->store();
+}
+
+int cParameters::setParameter(const char* owner, const char* name, long int value)
+{
+ char txt[16];
+
+ snprintf(txt, sizeof(txt), "%ld", value);
+
+ return setParameter(owner, name, txt);
+}
+
diff --git a/parameters.h b/parameters.h
new file mode 100644
index 0000000..7d45b0b
--- /dev/null
+++ b/parameters.h
@@ -0,0 +1,59 @@
+/*
+ * parameters.h
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __PARAMETERS_H
+#define __PARAMETERS_H
+
+#include "lib/db.h"
+
+//***************************************************************************
+// Parameters
+//***************************************************************************
+
+class cParameters
+{
+ public:
+
+ enum Type
+ {
+ ptNum,
+ ptTime, // unix time
+ ptBool,
+ ptAscii
+ };
+
+ struct Parameter
+ {
+ const char* owner;
+ const char* name;
+ int type;
+ const char* def;
+ const char* regexp;
+ int readonly;
+ int visible;
+ };
+
+ cParameters();
+
+ int initDb(cDbConnection* connection);
+ int exitDb();
+
+ int getParameter(const char* owner, const char* name, char* value = 0);
+ int getParameter(const char* owner, const char* name, long int& value);
+ int setParameter(const char* owner, const char* name, const char* value);
+ int setParameter(const char* owner, const char* name, long int value);
+
+ protected:
+
+ cDbTable* parametersDb;
+ cDbStatement* selectParameters;
+
+ static Parameter parameters[];
+ static Parameter* getDefinition(const char* owner, const char* name);
+};
+
+#endif // __PARAMETERS_H
diff --git a/patches/pre-vdr-2.1.x--epghandler-segment-transfer.patch b/patches/pre-vdr-2.1.x--epghandler-segment-transfer.patch
new file mode 100644
index 0000000..8374a66
--- /dev/null
+++ b/patches/pre-vdr-2.1.x--epghandler-segment-transfer.patch
@@ -0,0 +1,65 @@
+--- ../vdr-2.0.2.plain//eit.c 2012-12-04 12:10:10.000000000 +0100
++++ eit.c 2013-05-22 16:49:37.635027462 +0200
+@@ -46,6 +46,8 @@
+ return;
+ }
+
++ EpgHandlers.BeginSegmentTransfer(channel, OnlyRunningStatus);
++
+ bool handledExternally = EpgHandlers.HandledExternally(channel);
+ cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(channel, true);
+
+@@ -310,6 +312,7 @@
+ Schedules->SetModified(pSchedule);
+ }
+ Channels.Unlock();
++ EpgHandlers.EndSegmentTransfer(Modified, OnlyRunningStatus);
+ }
+
+ // --- cTDT ------------------------------------------------------------------
+--- ../vdr-2.0.2.plain//epg.c 2013-02-17 15:12:07.000000000 +0100
++++ epg.c 2013-05-22 16:50:29.043029281 +0200
+@@ -1537,3 +1537,19 @@
+ }
+ Schedule->DropOutdated(SegmentStart, SegmentEnd, TableID, Version);
+ }
++
++void cEpgHandlers::BeginSegmentTransfer(const cChannel *Channel, bool OnlyRunningStatus)
++{
++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
++ if (eh->BeginSegmentTransfer(Channel, OnlyRunningStatus))
++ return;
++ }
++}
++
++void cEpgHandlers::EndSegmentTransfer(bool Modified, bool OnlyRunningStatus)
++{
++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
++ if (eh->EndSegmentTransfer(Modified, OnlyRunningStatus))
++ return;
++ }
++}
+--- ../vdr-2.0.2.plain//epg.h 2012-09-24 14:53:53.000000000 +0200
++++ epg.h 2013-05-22 16:50:16.867028850 +0200
+@@ -273,6 +273,12 @@
+ virtual bool DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version) { return false; }
+ ///< Takes a look at all EPG events between SegmentStart and SegmentEnd and
+ ///< drops outdated events.
++ virtual bool BeginSegmentTransfer(const cChannel *Channel, bool OnlyRunningStatus) { return false; }
++ ///< called directly after IgnoreChannel before any other handler method called
++ ///< designed to give handlers the ossibility to prepare a transaction
++ virtual bool EndSegmentTransfer(bool Modified, bool OnlyRunningStatus) { return false; }
++ ///< called at last after the segment data is processed
++ ///< at this oint handlers should close/commt/rollback their transactions
+ };
+
+ class cEpgHandlers : public cList<cEpgHandler> {
+@@ -295,6 +301,8 @@
+ void HandleEvent(cEvent *Event);
+ void SortSchedule(cSchedule *Schedule);
+ void DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version);
++ void BeginSegmentTransfer(const cChannel *Channel, bool OnlyRunningStatus);
++ void EndSegmentTransfer(bool Modified, bool OnlyRunningStatus);
+ };
+
+ extern cEpgHandlers EpgHandlers;
diff --git a/patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch b/patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch
new file mode 100644
index 0000000..7be1802
--- /dev/null
+++ b/patches/vdr-1.7.27-to-epghandler-of-1.7.31.patch
@@ -0,0 +1,219 @@
+--- /home/wendel/vdr-1.7.27.plain//eit.c 2012-03-14 11:11:15.000000000 +0100
++++ eit.c 2012-10-01 09:38:51.526839349 +0200
+@@ -45,6 +45,7 @@
+ return;
+ }
+
++ bool handledExternally = EpgHandlers.HandledExternally(channel);
+ cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(channel, true);
+
+ bool Empty = true;
+@@ -70,14 +71,18 @@
+ cEvent *newEvent = NULL;
+ cEvent *rEvent = NULL;
+ cEvent *pEvent = (cEvent *)pSchedule->GetEvent(SiEitEvent.getEventId(), StartTime);
+- if (!pEvent) {
++ if (!pEvent || handledExternally) {
+ if (OnlyRunningStatus)
+ continue;
++ if (handledExternally)
++ if (!EpgHandlers.IsUpdate(SiEitEvent.getEventId(), StartTime, Tid, getVersionNumber()))
++ continue;
+ // If we don't have that event yet, we create a new one.
+ // Otherwise we copy the information into the existing event anyway, because the data might have changed.
+ pEvent = newEvent = new cEvent(SiEitEvent.getEventId());
+ newEvent->SetStartTime(StartTime);
+ newEvent->SetDuration(Duration);
++ if (!handledExternally)
+ pSchedule->AddEvent(newEvent);
+ }
+ else {
+@@ -290,6 +295,9 @@
+ channel->SetLinkChannels(LinkChannels);
+ Modified = true;
+ EpgHandlers.HandleEvent(pEvent);
++
++ if (handledExternally)
++ delete pEvent;
+ }
+ if (Tid == 0x4E) {
+ if (Empty && getSectionNumber() == 0)
+--- /home/wendel/vdr-1.7.27.plain//epg.c 2012-03-10 14:14:27.000000000 +0100
++++ epg.c 2012-10-01 09:41:35.010845128 +0200
+@@ -18,6 +18,7 @@
+ #include "timers.h"
+
+ #define RUNNINGSTATUSTIMEOUT 30 // seconds before the running status is considered unknown
++#define EPGDATAWRITEDELTA 600 // seconds between writing the epg.data file
+
+ // --- tComponent ------------------------------------------------------------
+
+@@ -1109,6 +1110,47 @@
+ return false;
+ }
+
++// --- cEpgDataWriter ---------------------------------------------------------
++
++class cEpgDataWriter : public cThread {
++private:
++ cMutex mutex;
++protected:
++ virtual void Action(void);
++public:
++ cEpgDataWriter(void);
++ void Perform(void);
++ };
++
++cEpgDataWriter::cEpgDataWriter(void)
++:cThread("epg data writer")
++{
++}
++
++void cEpgDataWriter::Action(void)
++{
++ SetPriority(19);
++ SetIOPriority(7);
++ Perform();
++}
++
++void cEpgDataWriter::Perform(void)
++{
++ cMutexLock MutexLock(&mutex); // to make sure fore- and background calls don't cause parellel dumps!
++ {
++ cSchedulesLock SchedulesLock(true, 1000);
++ cSchedules *s = (cSchedules *)cSchedules::Schedules(SchedulesLock);
++ if (s) {
++ time_t now = time(NULL);
++ for (cSchedule *p = s->First(); p; p = s->Next(p))
++ p->Cleanup(now);
++ }
++ }
++ cSchedules::Dump();
++}
++
++static cEpgDataWriter EpgDataWriter;
++
+ // --- cSchedulesLock --------------------------------------------------------
+
+ cSchedulesLock::cSchedulesLock(bool WriteLock, int TimeoutMs)
+@@ -1152,28 +1194,13 @@
+ if (Force)
+ lastDump = 0;
+ time_t now = time(NULL);
+- struct tm tm_r;
+- struct tm *ptm = localtime_r(&now, &tm_r);
+- if (now - lastCleanup > 3600) {
+- isyslog("cleaning up schedules data");
+- cSchedulesLock SchedulesLock(true, 1000);
+- cSchedules *s = (cSchedules *)Schedules(SchedulesLock);
+- if (s) {
+- for (cSchedule *p = s->First(); p; p = s->Next(p))
+- p->Cleanup(now);
+- }
+- lastCleanup = now;
+- if (ptm->tm_hour == 5)
+- ReportEpgBugFixStats(true);
+- }
+- if (epgDataFileName && now - lastDump > 600) {
+- cSafeFile f(epgDataFileName);
+- if (f.Open()) {
+- Dump(f);
+- f.Close();
++ if (now - lastDump > EPGDATAWRITEDELTA) {
++ if (epgDataFileName) {
++ if (Force)
++ EpgDataWriter.Perform();
++ else if (!EpgDataWriter.Active())
++ EpgDataWriter.Start();
+ }
+- else
+- LOG_ERROR;
+ lastDump = now;
+ }
+ }
+@@ -1207,8 +1234,23 @@
+ cSchedulesLock SchedulesLock;
+ cSchedules *s = (cSchedules *)Schedules(SchedulesLock);
+ if (s) {
++ cSafeFile *sf = NULL;
++ if (!f) {
++ sf = new cSafeFile(epgDataFileName);
++ if (sf->Open())
++ f = *sf;
++ else {
++ LOG_ERROR;
++ delete sf;
++ return false;
++ }
++ }
+ for (cSchedule *p = s->First(); p; p = s->Next(p))
+ p->Dump(f, Prefix, DumpMode, AtTime);
++ if (sf) {
++ sf->Close();
++ delete sf;
++ }
+ return true;
+ }
+ return false;
+@@ -1329,6 +1371,24 @@
+ return true;
+ }
+ return false;
++}
++
++bool cEpgHandlers::HandledExternally(const cChannel *Channel)
++{
++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
++ if (eh->HandledExternally(Channel))
++ return true;
++ }
++ return false;
++}
++
++bool cEpgHandlers::IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version)
++{
++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
++ if (eh->IsUpdate(EventID, StartTime, TableID, Version))
++ return true;
++ }
++ return false;
+ }
+
+ void cEpgHandlers::SetEventID(cEvent *Event, tEventID EventID)
+--- /home/wendel/vdr-1.7.27.plain//epg.h 2012-03-10 14:50:10.000000000 +0100
++++ epg.h 2012-10-01 09:43:28.162849134 +0200
+@@ -207,7 +207,7 @@
+ static void Cleanup(bool Force = false);
+ static void ResetVersions(void);
+ static bool ClearAll(void);
+- static bool Dump(FILE *f, const char *Prefix = "", eDumpMode DumpMode = dmAll, time_t AtTime = 0);
++ static bool Dump(FILE *f = NULL, const char *Prefix = "", eDumpMode DumpMode = dmAll, time_t AtTime = 0);
+ static bool Read(FILE *f = NULL);
+ cSchedule *AddSchedule(tChannelID ChannelID);
+ const cSchedule *GetSchedule(tChannelID ChannelID) const;
+@@ -244,6 +244,16 @@
+ ///< EPG handlers are queried to see if any of them would like to do the
+ ///< complete processing by itself. TableID and Version are from the
+ ///< incoming section data.
++ virtual bool HandledExternally(const cChannel *Channel) { return false; }
++ ///< If any EPG handler returns true in this function, it is assumed that
++ ///< the EPG for the given Channel is handled completely from some external
++ ///< source. Incoming EIT data is processed as usual, but any new EPG event
++ ///< will not be added to the respective schedule. It's up to the EPG
++ ///< handler to take care of this.
++ virtual bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) { return false; }
++ ///< VDR can't perform the update check (version, tid) for external handled events
++ ///< therefore the handle have to take care. Otherwise the parsing of 'non' updates will
++ ///< take a lot of resources
+ virtual bool SetEventID(cEvent *Event, tEventID EventID) { return false; }
+ virtual bool SetTitle(cEvent *Event, const char *Title) { return false; }
+ virtual bool SetShortText(cEvent *Event, const char *ShortText) { return false; }
+@@ -269,6 +279,8 @@
+ public:
+ bool IgnoreChannel(const cChannel *Channel);
+ bool HandleEitEvent(cSchedule *Schedule, const SI::EIT::Event *EitEvent, uchar TableID, uchar Version);
++ bool HandledExternally(const cChannel *Channel);
++ bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version);
+ void SetEventID(cEvent *Event, tEventID EventID);
+ void SetTitle(cEvent *Event, const char *Title);
+ void SetShortText(cEvent *Event, const char *ShortText);
diff --git a/patches/vdr-1.7.28-epghandledexternally.diff b/patches/vdr-1.7.28-epghandledexternally.diff
new file mode 100644
index 0000000..52dfab6
--- /dev/null
+++ b/patches/vdr-1.7.28-epghandledexternally.diff
@@ -0,0 +1,118 @@
+--- ./eit.c 2012/06/02 14:05:22 2.17
++++ ./eit.c 2012/06/04 10:10:11
+@@ -45,6 +45,7 @@
+ return;
+ }
+
++ bool handledExternally = EpgHandlers.HandledExternally(channel);
+ cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(channel, true);
+
+ bool Empty = true;
+@@ -70,7 +71,7 @@
+ cEvent *newEvent = NULL;
+ cEvent *rEvent = NULL;
+ cEvent *pEvent = (cEvent *)pSchedule->GetEvent(SiEitEvent.getEventId(), StartTime);
+- if (!pEvent) {
++ if (!pEvent || handledExternally) {
+ if (OnlyRunningStatus)
+ continue;
+ // If we don't have that event yet, we create a new one.
+@@ -78,7 +79,8 @@
+ pEvent = newEvent = new cEvent(SiEitEvent.getEventId());
+ newEvent->SetStartTime(StartTime);
+ newEvent->SetDuration(Duration);
+- pSchedule->AddEvent(newEvent);
++ if (!handledExternally)
++ pSchedule->AddEvent(newEvent);
+ }
+ else {
+ // We have found an existing event, either through its event ID or its start time.
+@@ -290,11 +292,8 @@
+ channel->SetLinkChannels(LinkChannels);
+ Modified = true;
+ EpgHandlers.HandleEvent(pEvent);
+-
+- if (EpgHandlers.DeleteEvent(pEvent)) {
+- pSchedule->DelEvent(pEvent);
+- pEvent = NULL;
+- }
++ if (handledExternally)
++ delete pEvent;
+ }
+ if (Tid == 0x4E) {
+ if (Empty && getSectionNumber() == 0)
+--- ./epg.c 2012/06/02 14:08:12 2.14
++++ ./epg.c 2012/06/04 10:06:22
+@@ -1331,6 +1331,15 @@
+ return false;
+ }
+
++bool cEpgHandlers::HandledExternally(const cChannel *Channel)
++{
++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
++ if (eh->HandledExternally(Channel))
++ return true;
++ }
++ return false;
++}
++
+ void cEpgHandlers::SetEventID(cEvent *Event, tEventID EventID)
+ {
+ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
+@@ -1429,15 +1438,6 @@
+ }
+ }
+
+-bool cEpgHandlers::DeleteEvent(const cEvent *Event)
+-{
+- for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
+- if (eh->DeleteEvent(Event))
+- return true;
+- }
+- return false;
+-}
+-
+ void cEpgHandlers::SortSchedule(cSchedule *Schedule)
+ {
+ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
+--- ./epg.h 2012/06/02 14:07:51 2.10
++++ ./epg.h 2012/06/04 10:05:21
+@@ -244,6 +244,12 @@
+ ///< EPG handlers are queried to see if any of them would like to do the
+ ///< complete processing by itself. TableID and Version are from the
+ ///< incoming section data.
++ virtual bool HandledExternally(const cChannel *Channel) { return false; }
++ ///< If any EPG handler returns true in this function, it is assumed that
++ ///< the EPG for the given Channel is handled completely from some external
++ ///< source. Incoming EIT data is processed as usual, but any new EPG event
++ ///< will not be added to the respective schedule. It's up to the EPG
++ ///< handler to take care of this.
+ virtual bool SetEventID(cEvent *Event, tEventID EventID) { return false; }
+ virtual bool SetTitle(cEvent *Event, const char *Title) { return false; }
+ virtual bool SetShortText(cEvent *Event, const char *ShortText) { return false; }
+@@ -258,9 +264,6 @@
+ virtual bool HandleEvent(cEvent *Event) { return false; }
+ ///< After all modifications of the Event have been done, the EPG handler
+ ///< can take a final look at it.
+- virtual bool DeleteEvent(const cEvent *Event) { return false; }
+- ///< After the complete processing of the Event is finished, an EPG handler
+- ///< can decide that this Event shall be deleted from its schedule.
+ virtual bool SortSchedule(cSchedule *Schedule) { return false; }
+ ///< Sorts the Schedule after the complete table has been processed.
+ virtual bool DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version) { return false; }
+@@ -272,6 +275,7 @@
+ public:
+ bool IgnoreChannel(const cChannel *Channel);
+ bool HandleEitEvent(cSchedule *Schedule, const SI::EIT::Event *EitEvent, uchar TableID, uchar Version);
++ bool HandledExternally(const cChannel *Channel);
+ void SetEventID(cEvent *Event, tEventID EventID);
+ void SetTitle(cEvent *Event, const char *Title);
+ void SetShortText(cEvent *Event, const char *ShortText);
+@@ -283,7 +287,6 @@
+ void SetVps(cEvent *Event, time_t Vps);
+ void FixEpgBugs(cEvent *Event);
+ void HandleEvent(cEvent *Event);
+- bool DeleteEvent(const cEvent *Event);
+ void SortSchedule(cSchedule *Schedule);
+ void DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version);
+ };
diff --git a/patches/vdr-1.7.29-epgIsUpdate.diff b/patches/vdr-1.7.29-epgIsUpdate.diff
new file mode 100644
index 0000000..61549ca
--- /dev/null
+++ b/patches/vdr-1.7.29-epgIsUpdate.diff
@@ -0,0 +1,52 @@
+--- ../vdr-1.7.29.plain//eit.c 2012-06-04 12:26:10.000000000 +0200
++++ eit.c 2012-07-30 10:19:34.841894485 +0200
+@@ -74,6 +74,9 @@
+ if (!pEvent || handledExternally) {
+ if (OnlyRunningStatus)
+ continue;
++ if (handledExternally)
++ if (!EpgHandlers.IsUpdate(SiEitEvent.getEventId(), StartTime, Tid, getVersionNumber()))
++ continue;
+ // If we don't have that event yet, we create a new one.
+ // Otherwise we copy the information into the existing event anyway, because the data might have changed.
+ pEvent = newEvent = new cEvent(SiEitEvent.getEventId());
+--- ../vdr-1.7.29.plain//epg.c 2012-06-04 12:26:10.000000000 +0200
++++ epg.c 2012-07-30 10:21:51.153899306 +0200
+@@ -1340,6 +1340,15 @@
+ return false;
+ }
+
++bool cEpgHandlers::IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version)
++{
++ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
++ if (eh->IsUpdate(EventID, StartTime, TableID, Version))
++ return true;
++ }
++ return false;
++}
++
+ void cEpgHandlers::SetEventID(cEvent *Event, tEventID EventID)
+ {
+ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
+--- ../vdr-1.7.29.plain//epg.h 2012-06-04 12:26:10.000000000 +0200
++++ epg.h 2012-07-30 10:20:15.705895929 +0200
+@@ -250,6 +250,10 @@
+ ///< source. Incoming EIT data is processed as usual, but any new EPG event
+ ///< will not be added to the respective schedule. It's up to the EPG
+ ///< handler to take care of this.
++ virtual bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version) { return false; }
++ ///< VDR can't perform the update check (version, tid) for external handled events
++ ///< therefore the handle have to take care. Otherwise the parsing of 'non' updates will
++ ///< take a lot of resources
+ virtual bool SetEventID(cEvent *Event, tEventID EventID) { return false; }
+ virtual bool SetTitle(cEvent *Event, const char *Title) { return false; }
+ virtual bool SetShortText(cEvent *Event, const char *ShortText) { return false; }
+@@ -277,6 +281,7 @@
+ bool IgnoreChannel(const cChannel *Channel);
+ bool HandleEitEvent(cSchedule *Schedule, const SI::EIT::Event *EitEvent, uchar TableID, uchar Version);
+ bool HandledExternally(const cChannel *Channel);
++ bool IsUpdate(tEventID EventID, time_t StartTime, uchar TableID, uchar Version);
+ void SetEventID(cEvent *Event, tEventID EventID);
+ void SetTitle(cEvent *Event, const char *Title);
+ void SetShortText(cEvent *Event, const char *ShortText);
+
diff --git a/patches/vdr-2.3.1.patch b/patches/vdr-2.3.1.patch
new file mode 100644
index 0000000..c37bb54
--- /dev/null
+++ b/patches/vdr-2.3.1.patch
@@ -0,0 +1,11 @@
+--- ../vdr-2.3.2.plain/./epg.h 2015-08-09 13:25:04.000000000 +0200
++++ ./epg.h 2017-02-08 14:42:26.304063928 +0100
+@@ -66,7 +66,7 @@
+
+ class cSchedule;
+
+-typedef u_int16_t tEventID;
++typedef u_int32_t tEventID;
+
+ class cEvent : public cListObject {
+ friend class cSchedule;
diff --git a/patches/vdr-2.3.2.patch b/patches/vdr-2.3.2.patch
new file mode 100644
index 0000000..102b362
--- /dev/null
+++ b/patches/vdr-2.3.2.patch
@@ -0,0 +1,56 @@
+--- ../vdr-2.3.2.plain//./epg.c 2015-09-10 12:58:19.000000000 +0200
++++ ./epg.c 2017-02-09 18:40:29.597671711 +0100
+@@ -1527,12 +1527,13 @@
+ Schedule->DropOutdated(SegmentStart, SegmentEnd, TableID, Version);
+ }
+
+-void cEpgHandlers::BeginSegmentTransfer(const cChannel *Channel)
++bool cEpgHandlers::BeginSegmentTransfer(const cChannel *Channel)
+ {
+ for (cEpgHandler *eh = First(); eh; eh = Next(eh)) {
+- if (eh->BeginSegmentTransfer(Channel, false))
+- return;
++ if (!eh->BeginSegmentTransfer(Channel, false))
++ return false;
+ }
++ return true;
+ }
+
+ void cEpgHandlers::EndSegmentTransfer(bool Modified)
+--- ../vdr-2.3.2.plain//./eit.c 2015-08-23 12:43:36.000000000 +0200
++++ ./eit.c 2017-02-09 18:40:29.597671711 +0100
+@@ -67,8 +67,13 @@
+ return;
+ }
+
++ if (!EpgHandlers.BeginSegmentTransfer(Channel)) {
++ SchedulesStateKey.Remove(false);
++ ChannelsStateKey.Remove(false);
++ return;
++ }
++
+ bool ChannelsModified = false;
+- EpgHandlers.BeginSegmentTransfer(Channel);
+ bool handledExternally = EpgHandlers.HandledExternally(Channel);
+ cSchedule *pSchedule = (cSchedule *)Schedules->GetSchedule(Channel, true);
+
+--- ../vdr-2.3.2.plain//./epg.h 2015-08-09 13:25:04.000000000 +0200
++++ ./epg.h 2017-02-09 18:40:29.601671655 +0100
+@@ -66,7 +66,7 @@
+
+ class cSchedule;
+
+-typedef u_int16_t tEventID;
++typedef u_int32_t tEventID;
+
+ class cEvent : public cListObject {
+ friend class cSchedule;
+@@ -311,7 +311,7 @@
+ void HandleEvent(cEvent *Event);
+ void SortSchedule(cSchedule *Schedule);
+ void DropOutdated(cSchedule *Schedule, time_t SegmentStart, time_t SegmentEnd, uchar TableID, uchar Version);
+- void BeginSegmentTransfer(const cChannel *Channel);
++ bool BeginSegmentTransfer(const cChannel *Channel);
+ void EndSegmentTransfer(bool Modified);
+ };
+
diff --git a/plgconfig.c b/plgconfig.c
new file mode 100644
index 0000000..4dda3d2
--- /dev/null
+++ b/plgconfig.c
@@ -0,0 +1,36 @@
+/*
+ * config.c:
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "plgconfig.h"
+
+cEpg2VdrConfig Epg2VdrConfig;
+
+//***************************************************************************
+// cEpg2VdrConfig
+//***************************************************************************
+
+cEpg2VdrConfig::cEpg2VdrConfig()
+ : cEpgConfig()
+{
+ mainmenuVisible = yes;
+ mainmenuFullupdate = 0;
+ blacklist = no;
+
+ activeOnEpgd = no;
+ scheduleBoot = no;
+ masterMode = 0;
+ shareInWeb = yes;
+ createTimerLocal = no;
+ useCommonRecFolder = yes;
+ xchgOkBlue = no;
+
+ replaceScheduleMenu = no;
+ replaceTimerMenu = no;
+ userIndex = 0;
+ *user = 0;
+ showEmptyChannels = no;
+}
diff --git a/plgconfig.h b/plgconfig.h
new file mode 100644
index 0000000..fce7b0d
--- /dev/null
+++ b/plgconfig.h
@@ -0,0 +1,44 @@
+/*
+ * plgconfig.h:
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id: config.h,v 1.2 2012/10/26 08:44:13 wendel Exp $
+ */
+
+#ifndef __EPG2VDR_CONFIG_H
+#define __EPG2VDR_CONFIG_H
+
+#include "lib/config.h"
+
+//***************************************************************************
+// Config
+//***************************************************************************
+
+struct cEpg2VdrConfig : public cEpgConfig
+{
+ public:
+
+ cEpg2VdrConfig();
+
+ int mainmenuVisible;
+ int mainmenuFullupdate;
+ int activeOnEpgd;
+ int scheduleBoot;
+ int blacklist; // to enable noepg feature
+ int masterMode;
+ int shareInWeb; // 'am verbund teilnehmen'
+ int createTimerLocal;
+ int useCommonRecFolder; // NAS
+ int xchgOkBlue;
+
+ int replaceScheduleMenu;
+ int replaceTimerMenu;
+ int userIndex;
+ char user[100+TB];
+ int showEmptyChannels;
+};
+
+extern cEpg2VdrConfig Epg2VdrConfig;
+
+#endif // __EPG2VDR_CONFIG_H
diff --git a/po/de_DE.po b/po/de_DE.po
new file mode 100644
index 0000000..9a1eb62
--- /dev/null
+++ b/po/de_DE.po
@@ -0,0 +1,355 @@
+# VDR plugin language source file.
+# Copyright (C) 2007 Klaus Schmidinger <kls@cadsoft.de>
+# This file is distributed under the same license as the VDR package.
+# Klaus Schmidinger <kls@cadsoft.de>, 2000
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: VDR 1.5.7\n"
+"Report-Msgid-Bugs-To: <vdr@jwendel.de>\n"
+"POT-Creation-Date: 2017-02-14 12:24+0100\n"
+"PO-Revision-Date: 2009-08-27 21:40+0200\n"
+"Last-Translator: Klaus Schmidinger <kls@cadsoft.de>\n"
+"Language-Team: <vdr@linuxtv.org>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "Timer"
+msgstr ""
+
+msgid "Search Timer"
+msgstr "Suchtimer"
+
+msgid "Timer journal"
+msgstr "Timerhistorie"
+
+msgid "Program"
+msgstr "Programm"
+
+#, fuzzy
+msgid "Update"
+msgstr "Automatisches Update"
+
+msgid "Reload"
+msgstr "Neu laden"
+
+#, fuzzy
+msgid "Update EPG"
+msgstr "Automatisches Update"
+
+msgid "Reload EPG"
+msgstr "EPG komplett neu einlesen"
+
+msgid "auto"
+msgstr ""
+
+msgid "yes"
+msgstr "ja"
+
+msgid "no"
+msgstr "nein"
+
+#, fuzzy
+msgid "EPG Update"
+msgstr "Automatisches Update"
+
+msgid "Update DVB EPG Database"
+msgstr "DVB/EPG in Datenbank eintragen"
+
+msgid "Load Images"
+msgstr "Bilder laden"
+
+msgid "Prohibit Shutdown On Busy 'epgd'"
+msgstr "Herunterfahren verhindern, wenn EPGD aktiv"
+
+msgid "Schedule Boot For Update"
+msgstr "Booten für geplante Updates"
+
+msgid "Blacklist not configured Channels"
+msgstr "Nicht konfigurierte Kanäle 'blacklisten'"
+
+msgid "Menu"
+msgstr "Menü"
+
+msgid "Show In Main Menu"
+msgstr "Im Hauptmenü anzeigen"
+
+msgid "Replace Program Menu"
+msgstr "Programmübersicht ersetzen"
+
+msgid "Replace Timer Menu"
+msgstr "Timerübersicht ersetzen"
+
+msgid "XChange Key Ok/Blue"
+msgstr "Tausche Taste OK/Blau"
+
+msgid "Show Channels without EPG"
+msgstr "Zeige Kanäle ohne EPG"
+
+msgid "Web"
+msgstr "Netzwerk"
+
+msgid "Share in Web"
+msgstr "Im Netz teilen"
+
+msgid "Create Timer Local"
+msgstr "Erstelle Timer lokal"
+
+msgid "Have Common Recordings Folder (NAS)"
+msgstr "Zentrale Aufnahmeablage (NAS)"
+
+msgid "SVDRP Interface"
+msgstr ""
+
+msgid "User"
+msgstr "Benutzer"
+
+msgid "MySQL"
+msgstr ""
+
+msgid "Host"
+msgstr ""
+
+msgid "Port"
+msgstr ""
+
+msgid "Database Name"
+msgstr "Datenbankname"
+
+msgid "Password"
+msgstr "Passwort"
+
+msgid "Technical Stuff"
+msgstr "Technisches"
+
+msgid "Log level"
+msgstr "Protokollierungsstufe"
+
+msgid "EPG2VDR Waiting on epgd"
+msgstr "EPG2VDR wartet auf EPGD"
+
+msgid "Timer not found, maybe deleted from other user"
+msgstr "Timer nicht gefunden, evtl. von anderem Benutzer gelöscht"
+
+msgid "Recorded"
+msgstr "Aufgenommen"
+
+msgid "Created"
+msgstr "Angelegt"
+
+msgid "Failed"
+msgstr "Fehlgeschlagen"
+
+msgid "Deleted"
+msgstr "Gelöscht"
+
+msgid "All"
+msgstr "Alle"
+
+msgid "Delete"
+msgstr "Löschen"
+
+msgid "Timer journal - Recorded"
+msgstr "Timerhistorie - Aufgenommen"
+
+msgid "Timer journal - Created"
+msgstr "Timerhistorie - Erzeugt"
+
+msgid "Timer journal - Failed"
+msgstr "Timerhistorie - Fehlgeschlagen"
+
+msgid "Timer journal - Delete"
+msgstr "Timerhistorie - Gelöscht"
+
+msgid "Delete timer from journal?"
+msgstr "Eintrag aus Timerhistorie löschen?"
+
+msgid "Search matching Events"
+msgstr ""
+
+msgid "Search matching Recordings"
+msgstr ""
+
+msgid "Searchtimers"
+msgstr ""
+
+msgid "Matching recordings"
+msgstr "Vergleiche Aufnahmen"
+
+msgid "Direct Matches:"
+msgstr "Direkte Ãœbereinstimmungen:"
+
+msgid "All Matches:"
+msgstr "Alle Ãœbereinstimmungen:"
+
+msgid "Event"
+msgstr ""
+
+msgid "Button$Timer"
+msgstr "Timer"
+
+msgid "Button$Record"
+msgstr "Aufnehmen"
+
+msgid "Button$Switch"
+msgstr "Umschalten"
+
+#, c-format
+msgid "Overview - %s"
+msgstr "Ãœbersicht - %s"
+
+#, c-format
+msgid "Overview - %s (%s)"
+msgstr "Ãœbersicht - %s (%s)"
+
+#, c-format
+msgid "Schedule - %s"
+msgstr "Programm - %s"
+
+msgid "Search results"
+msgstr "Suchergebnisse"
+
+msgid "Button$Schedule"
+msgstr "Programm"
+
+msgid "Button$Info"
+msgstr "Information"
+
+msgid "Can't switch channel!"
+msgstr "Umschalten nicht möglich!"
+
+msgid "Edit Search Timer"
+msgstr "Suchtimer bearbeiten"
+
+msgid "Remove all done entries of this event?"
+msgstr "Alle erledigten Einträge dieses Ereignisses entfernen?"
+
+msgid "Search Timers"
+msgstr "Suchtimer"
+
+msgid "Deactivate"
+msgstr "Deaktivieren"
+
+msgid "Activate"
+msgstr "Aktivieren"
+
+msgid "Test"
+msgstr ""
+
+msgid "Delete search timer?"
+msgstr "Suchtimer löschen?"
+
+msgid "Edit Timer"
+msgstr "Timer bearbeiten"
+
+msgid "Active"
+msgstr "Aktiv"
+
+msgid "VDR"
+msgstr ""
+
+msgid "Channel"
+msgstr "Kanal"
+
+msgid "Day"
+msgstr "Tag"
+
+msgid "Start"
+msgstr "Start"
+
+msgid "Stop"
+msgstr "Stop"
+
+msgid "VPS"
+msgstr ""
+
+msgid "Priority"
+msgstr "Priorität"
+
+msgid "Lifetime"
+msgstr "Lebensdauer"
+
+msgid "Childlock"
+msgstr "Kindersperre"
+
+msgid "File"
+msgstr "Datei"
+
+msgid "Information"
+msgstr ""
+
+msgid "Button$Folder"
+msgstr "Ordner"
+
+msgid "Button$Single"
+msgstr "Einzeln"
+
+msgid "Button$Repeating"
+msgstr "Wiederholung"
+
+msgid "First day"
+msgstr "Erster Tag"
+
+msgid "*** Invalid Channel ***"
+msgstr "*** ungültiger Kanal ***"
+
+msgid "Timer still recording - really move to other VDR?"
+msgstr "Timer zeichnet auf, dennoch verschieben?"
+
+msgid "Select folder"
+msgstr "Verzeichnis wählen"
+
+msgid "Timers"
+msgstr "Timer"
+
+msgid "Button$On/Off"
+msgstr "An/Aus"
+
+msgid "Button$New"
+msgstr "Neu"
+
+msgid "Button$Delete"
+msgstr "Löschen"
+
+msgid "Delete timer?"
+msgstr "Timer löschen"
+
+msgid "Timer still recording - really delete?"
+msgstr "Timer zeichnet auf, dennoch löschen?"
+
+#, c-format
+msgid "Skipping '%s' request, epgd busy. Trying again later"
+msgstr ""
+
+msgid "reload"
+msgstr "neu laden"
+
+msgid "update"
+msgstr "aktualisieren"
+
+#, c-format
+msgid "EPG '%s' done"
+msgstr "EPG '%s' abgeschlossen"
+
+#~ msgid "Now"
+#~ msgstr "Jetzt"
+
+#~ msgid "Next"
+#~ msgstr "Nächste"
+
+#~ msgid "This event - %s"
+#~ msgstr "Dieses Ereignis - %s"
+
+#~ msgid "This event - all channels"
+#~ msgstr "Dieses Ereignis - alle Kanäle"
+
+#~ msgid "All events - all channels"
+#~ msgstr "Alle Ereignisse - alle Kanäle"
+
+#~ msgid "Button$Now"
+#~ msgstr "Jetzt"
+
+#~ msgid "Button$Next"
+#~ msgstr "Nächste"
diff --git a/po/it_IT.po b/po/it_IT.po
new file mode 100644
index 0000000..356928e
--- /dev/null
+++ b/po/it_IT.po
@@ -0,0 +1,343 @@
+# VDR plugin language source file.
+# Copyright (C) 2007 Klaus Schmidinger <kls@cadsoft.de>
+# This file is distributed under the same license as the VDR package.
+# Alberto Carraro <bertocar@tin.it>, 2001
+# Antonio Ospite <ospite@studenti.unina.it>, 2003
+# Sean Carlos <seanc@libero.it>, 2005
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: VDR 1.5.7\n"
+"Report-Msgid-Bugs-To: <vdr@jwendel.de>\n"
+"POT-Creation-Date: 2017-02-14 12:24+0100\n"
+"PO-Revision-Date: 2009-08-27 21:45+0100\n"
+"Last-Translator: Diego Pierotto <vdr-italian@tiscali.it>\n"
+"Language-Team: <vdr@linuxtv.org>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Language: Italian\n"
+"X-Poedit-Country: ITALY\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+msgid "Timer"
+msgstr ""
+
+msgid "Search Timer"
+msgstr ""
+
+msgid "Timer journal"
+msgstr ""
+
+msgid "Program"
+msgstr ""
+
+#, fuzzy
+msgid "Update"
+msgstr "Aggiorn. automatico"
+
+msgid "Reload"
+msgstr ""
+
+#, fuzzy
+msgid "Update EPG"
+msgstr "Aggiorn. automatico"
+
+msgid "Reload EPG"
+msgstr ""
+
+msgid "auto"
+msgstr ""
+
+msgid "yes"
+msgstr ""
+
+msgid "no"
+msgstr ""
+
+#, fuzzy
+msgid "EPG Update"
+msgstr "Aggiorn. automatico"
+
+msgid "Update DVB EPG Database"
+msgstr ""
+
+msgid "Load Images"
+msgstr ""
+
+msgid "Prohibit Shutdown On Busy 'epgd'"
+msgstr ""
+
+msgid "Schedule Boot For Update"
+msgstr ""
+
+msgid "Blacklist not configured Channels"
+msgstr ""
+
+msgid "Menu"
+msgstr ""
+
+msgid "Show In Main Menu"
+msgstr "Mostra nel menu principale"
+
+msgid "Replace Program Menu"
+msgstr ""
+
+msgid "Replace Timer Menu"
+msgstr ""
+
+msgid "XChange Key Ok/Blue"
+msgstr ""
+
+msgid "Show Channels without EPG"
+msgstr ""
+
+msgid "Web"
+msgstr ""
+
+msgid "Share in Web"
+msgstr ""
+
+msgid "Create Timer Local"
+msgstr ""
+
+msgid "Have Common Recordings Folder (NAS)"
+msgstr ""
+
+msgid "SVDRP Interface"
+msgstr ""
+
+msgid "User"
+msgstr ""
+
+msgid "MySQL"
+msgstr ""
+
+msgid "Host"
+msgstr ""
+
+msgid "Port"
+msgstr ""
+
+msgid "Database Name"
+msgstr ""
+
+msgid "Password"
+msgstr ""
+
+msgid "Technical Stuff"
+msgstr ""
+
+msgid "Log level"
+msgstr "xxx #AS# "
+
+msgid "EPG2VDR Waiting on epgd"
+msgstr ""
+
+msgid "Timer not found, maybe deleted from other user"
+msgstr ""
+
+msgid "Recorded"
+msgstr ""
+
+msgid "Created"
+msgstr ""
+
+msgid "Failed"
+msgstr ""
+
+msgid "Deleted"
+msgstr ""
+
+msgid "All"
+msgstr ""
+
+msgid "Delete"
+msgstr ""
+
+msgid "Timer journal - Recorded"
+msgstr ""
+
+msgid "Timer journal - Created"
+msgstr ""
+
+msgid "Timer journal - Failed"
+msgstr ""
+
+msgid "Timer journal - Delete"
+msgstr ""
+
+msgid "Delete timer from journal?"
+msgstr ""
+
+msgid "Search matching Events"
+msgstr ""
+
+msgid "Search matching Recordings"
+msgstr ""
+
+msgid "Searchtimers"
+msgstr ""
+
+msgid "Matching recordings"
+msgstr ""
+
+msgid "Direct Matches:"
+msgstr ""
+
+msgid "All Matches:"
+msgstr ""
+
+msgid "Event"
+msgstr ""
+
+msgid "Button$Timer"
+msgstr ""
+
+msgid "Button$Record"
+msgstr ""
+
+msgid "Button$Switch"
+msgstr ""
+
+#, c-format
+msgid "Overview - %s"
+msgstr ""
+
+#, c-format
+msgid "Overview - %s (%s)"
+msgstr ""
+
+#, c-format
+msgid "Schedule - %s"
+msgstr ""
+
+msgid "Search results"
+msgstr ""
+
+msgid "Button$Schedule"
+msgstr ""
+
+msgid "Button$Info"
+msgstr ""
+
+msgid "Can't switch channel!"
+msgstr ""
+
+msgid "Edit Search Timer"
+msgstr ""
+
+msgid "Remove all done entries of this event?"
+msgstr ""
+
+msgid "Search Timers"
+msgstr ""
+
+msgid "Deactivate"
+msgstr ""
+
+msgid "Activate"
+msgstr ""
+
+msgid "Test"
+msgstr ""
+
+msgid "Delete search timer?"
+msgstr ""
+
+msgid "Edit Timer"
+msgstr ""
+
+msgid "Active"
+msgstr ""
+
+msgid "VDR"
+msgstr ""
+
+msgid "Channel"
+msgstr ""
+
+msgid "Day"
+msgstr ""
+
+msgid "Start"
+msgstr ""
+
+msgid "Stop"
+msgstr ""
+
+msgid "VPS"
+msgstr ""
+
+msgid "Priority"
+msgstr ""
+
+msgid "Lifetime"
+msgstr ""
+
+msgid "Childlock"
+msgstr ""
+
+msgid "File"
+msgstr ""
+
+msgid "Information"
+msgstr ""
+
+msgid "Button$Folder"
+msgstr ""
+
+msgid "Button$Single"
+msgstr ""
+
+msgid "Button$Repeating"
+msgstr ""
+
+msgid "First day"
+msgstr ""
+
+msgid "*** Invalid Channel ***"
+msgstr ""
+
+msgid "Timer still recording - really move to other VDR?"
+msgstr ""
+
+msgid "Select folder"
+msgstr ""
+
+msgid "Timers"
+msgstr ""
+
+msgid "Button$On/Off"
+msgstr ""
+
+msgid "Button$New"
+msgstr ""
+
+msgid "Button$Delete"
+msgstr ""
+
+msgid "Delete timer?"
+msgstr ""
+
+msgid "Timer still recording - really delete?"
+msgstr ""
+
+#, c-format
+msgid "Skipping '%s' request, epgd busy. Trying again later"
+msgstr ""
+
+msgid "reload"
+msgstr ""
+
+msgid "update"
+msgstr ""
+
+#, c-format
+msgid "EPG '%s' done"
+msgstr ""
+
+#, fuzzy
+#~ msgid "Updatetime (hours)"
+#~ msgstr "Tempo aggiorn. (min)"
diff --git a/recinfofile.c b/recinfofile.c
new file mode 100644
index 0000000..cc957da
--- /dev/null
+++ b/recinfofile.c
@@ -0,0 +1,245 @@
+/*
+ * recinfofile.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "update.h"
+
+//***************************************************************************
+// Class Event Details
+//***************************************************************************
+
+const char* cEventDetails::fields[] =
+{
+ "ACTOR",
+ "AUDIO",
+ "CATEGORY",
+ "COUNTRY",
+ "DIRECTOR",
+ "FLAGS",
+ "GENRE",
+ "MUSIC",
+ "PRODUCER",
+ "SCREENPLAY",
+ "SHORTREVIEW",
+ "TIPP",
+ "TOPIC",
+ "YEAR",
+ "RATING",
+ "NUMRATING",
+ "MODERATOR",
+ "OTHER",
+ "GUEST",
+ "CAMERA",
+
+ "SCRSERIESID",
+ "SCRSERIESEPISODE",
+ "SCRMOVIEID",
+
+ // just in recordinglist, not in events or useevents row
+
+ "CHANNELNAME",
+ "SCRINFOMOVIEID",
+ "SCRINFOSERIESID",
+ "SCRINFOEPISODEID",
+
+ 0
+};
+
+//***************************************************************************
+// Set Value
+//***************************************************************************
+
+void cEventDetails::setValue(const char* name, const char* value)
+{
+ std::map<std::string,std::string>::iterator it;
+
+ it = values.find(name);
+
+ if (it == values.end() || it->first != value)
+ {
+ changes++;
+ values[name] = value;
+ }
+}
+
+void cEventDetails::setValue(const char* name, int value)
+{
+ setValue(name, num2Str(value).c_str());
+}
+
+//***************************************************************************
+// Update By Row
+//***************************************************************************
+
+int cEventDetails::updateByRow(cDbRow* row)
+{
+ std::map<std::string,std::string>::iterator it;
+
+ for (int i = 0; fields[i]; i++)
+ {
+ if (!row->getTableDef()->getField(fields[i], /*silent*/ yes))
+ continue; // skip silent!
+
+ cDbValue* value = row->getValue(fields[i]);
+
+ if (!value)
+ {
+ tell(2, "Warning: Field '%s' not found", fields[i]); // only for debug
+ continue;
+ }
+
+ if (!value->isNull())
+ {
+ std::string v;
+
+ if (value->getField()->isString())
+ v = row->getStrValue(fields[i]);
+ else if (value->getField()->isInt())
+ v = num2Str(row->getIntValue(fields[i]));
+ else
+ {
+ tell(0, "Info: Field '%s' unhandled for info.epg2vdr", fields[i]);
+ continue;
+ }
+
+ it = values.find(fields[i]);
+
+ if (it == values.end() || it->second != v)
+ {
+ changes++;
+ values[fields[i]] = v;
+ }
+ }
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Update To Row
+//***************************************************************************
+
+int cEventDetails::updateToRow(cDbRow* row)
+{
+ std::map<std::string, std::string>::iterator it;
+
+ for (it = values.begin(); it != values.end(); it++)
+ {
+ cDbValue* value = row->getValue(it->first.c_str());
+
+ if (!value)
+ {
+ tell(0, "Warning: Field '%s' not found", it->first.c_str());
+ continue;
+ }
+
+ if (!it->first.length())
+ continue;
+
+ if (value->getField()->isString())
+ value->setValue(it->second.c_str());
+ else if (value->getField()->isInt())
+ value->setValue(atoi(it->second.c_str()));
+ else
+ {
+ tell(0, "Info: Field '%s' unhandled for info.epg2vdr", it->first.c_str());
+ continue;
+ }
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Store To Fs
+//***************************************************************************
+
+int cEventDetails::storeToFs(const char* path)
+{
+ FILE* f;
+ char* fileName = 0;
+ std::map<std::string,std::string>::iterator it;
+
+ asprintf(&fileName, "%s/info.epg2vdr", path);
+
+ if (!(f = fopen(fileName, "w")))
+ {
+ tell(0, "Error opening file '%s' failed, %s", fileName, strerror(errno));
+ free(fileName);
+
+ return fail;
+ }
+
+ tell(0, "Storing event details to '%s'", fileName);
+
+ // store fields
+
+ for (it = values.begin(); it != values.end(); it++)
+ fprintf(f, "%s: %s\n", it->first.c_str(), it->second.c_str());
+
+ free(fileName);
+ fclose(f);
+
+ return success;
+}
+
+//***************************************************************************
+// Load From Fs
+//***************************************************************************
+
+int cEventDetails::loadFromFs(const char* path)
+{
+ FILE* f;
+ char* fileName = 0;
+ std::map<std::string,std::string>::iterator it;
+
+ values.clear();
+ changes = 0;
+
+ asprintf(&fileName, "%s/info.epg2vdr", path);
+
+ if (!fileExists(fileName))
+ {
+ free(fileName);
+ return done;
+ }
+
+ if (!(f = fopen(fileName, "r")))
+ {
+ tell(0, "Error opening file '%s' failed, %s", fileName, strerror(errno));
+ free(fileName);
+
+ return fail;
+ }
+
+ tell(3, "Loading event details from '%s'", fileName);
+
+ // load fields
+
+ char* p;
+ char* s;
+ cReadLine readLine;
+
+ while (s = readLine.Read(f))
+ {
+ if (!(p = strchr(s, ':')))
+ {
+ tell(0, " ");
+ continue;
+ }
+
+ *(p++) = 0;
+ p = skipspace(rTrim(p));
+
+ if (!isEmpty(p))
+ values[s] = p;
+ }
+
+ free(fileName);
+ fclose(f);
+
+ return success;
+}
diff --git a/recording.c b/recording.c
new file mode 100644
index 0000000..c3d5ac4
--- /dev/null
+++ b/recording.c
@@ -0,0 +1,533 @@
+/*
+ * recording.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <set>
+
+#include <vdr/videodir.h>
+
+#include "lib/common.h"
+#include "update.h"
+
+//***************************************************************************
+// Is Protected
+//***************************************************************************
+
+int isProtected(const char* path)
+{
+ char* dir;
+ int fsk = no;
+ char* file = 0;
+ char* p = 0;
+
+ const char* videoDir = cVideoDirectory::Name();
+
+ if (strncmp(path, videoDir, strlen(videoDir)) != 0)
+ {
+ tell(0, "Fatal: Unexpected video dir in '%s'", path);
+ return no;
+ }
+
+ dir = strdup(path + strlen(videoDir));
+
+ do
+ {
+ if (p) *p = 0;
+
+ asprintf(&file, "%s/%s/protection.fsk", videoDir, dir);
+ fsk = fileExists(file);
+ free(file);
+
+ } while (!fsk && (p = strrchr(dir, '/')));
+
+ tell(3, "'%s' is %sprotected", path, fsk ? "" : "not ");
+ free(dir);
+
+ return fsk;
+}
+
+//***************************************************************************
+// Cleanup Deleted Recordings from Table
+//***************************************************************************
+
+int cUpdate::cleanupDeletedRecordings(int force)
+{
+ int delCnt = 0;
+ md5Buf md5path;
+ std::set<std::string> recMd5List;
+
+ // --------------------------
+ // get recordings lock
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ LOCK_RECORDINGS_READ;
+ const cRecordings* recordings = Recordings;
+#else
+ const cRecordings* recordings = &Recordings;
+#endif
+
+ if (recordings->Count() == lastRecordingCount && !force)
+ return done;
+
+ tell(0, "Cleanup deleted recordings at database%s", force ? " (forced)" : "");
+
+ // create set of existing recordings (to improve speed)
+
+ for (const cRecording* rec = recordings->First(); rec; rec = recordings->Next(rec))
+ {
+ int pathOffset = 0;
+
+ if (strncmp(rec->FileName(), videoBasePath, strlen(videoBasePath)) == 0)
+ {
+ pathOffset = strlen(videoBasePath);
+
+ if (*(rec->FileName()+pathOffset) == '/')
+ pathOffset++;
+ }
+
+ createMd5(rec->FileName()+pathOffset, md5path);
+
+ tell(5, "DEBUG: Recording: '%s' [%s]", rec->Title(), rec->FileName());
+ recMd5List.insert(md5path);
+ }
+
+ connection->startTransaction();
+
+ recordingListDb->clear();
+ recordingListDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+
+ for (int f = selectRecordings->find(); f && dbConnected(); f = selectRecordings->fetch())
+ {
+ // bin 'ich' zuständig?
+
+ if (!recordingListDb->hasValue("VDRUUID", Epg2VdrConfig.uuid))
+ {
+ if (!recordingListDb->hasValue("OWNER", "") || !Epg2VdrConfig.useCommonRecFolder)
+ continue;
+
+ // sonderlocke, das mounted ggf. nicht jeder überall
+
+ if (recordingListDb->getIntValue("FSK"))
+ continue;
+ }
+
+ if (recMd5List.find(recordingListDb->getStrValue("MD5PATH")) == recMd5List.end())
+ {
+ // not found, mark as deleted
+
+ delCnt++;
+ recordingListDb->setValue("STATE", "D");
+ recordingListDb->update();
+ }
+ }
+
+ selectRecordings->freeResult();
+
+ connection->commit();
+ lastRecordingCount = recordings->Count();
+
+ tell(0, "Info: Marked %d recordings as deleted", delCnt);
+
+ return success;
+}
+
+//***************************************************************************
+// Update Pending Recording Info Files
+//***************************************************************************
+
+int cUpdate::updatePendingRecordingInfoFiles(const cRecordings* recordings)
+{
+ cEventDetails evd;
+ int count = 0;
+
+ if (pendingNewRecordings.empty())
+ return done;
+
+ tell(1, "Updating recording info in info.epg2vdr");
+
+ // iterate over pending recordings
+
+ while (!pendingNewRecordings.empty())
+ {
+ std::string path = pendingNewRecordings.front();
+ const cRecording* rec = ((cRecordings*)recordings)->GetByName(path.c_str());
+
+ pendingNewRecordings.pop();
+
+ if (!rec || !rec->Info() || !rec->Info()->GetEvent())
+ {
+ tell(0, "Warning: Recording '%s' not found or missin recording info,"
+ " can't create info.epg2vdr", path.c_str());
+ continue;
+ }
+
+ useeventsDb->clear();
+ useeventsDb->setValue("USEID", (int)rec->Info()->GetEvent()->EventID());
+
+ if (selectEventById->find())
+ {
+ evd.loadFromFs(path.c_str());
+ evd.updateByRow(useeventsDb->getRow());
+
+ if (evd.getChanges())
+ {
+ count++;
+ evd.storeToFs(path.c_str());
+
+ tell(1, "Updated/Created recording info for %d in info.epg2vdr with %d changes",
+ rec->Info()->GetEvent()->EventID(), evd.getChanges());
+ }
+ }
+ else
+ tell(0, "Warning: Event %d not found in table", rec->Info()->GetEvent()->EventID());
+
+ selectEventById->freeResult();
+ }
+
+ tell(1, "Updated %d pending info.epg2vdr files", count);
+
+ return done;
+}
+
+//***************************************************************************
+// Store All Recording Info Files (only used by manual trigger)
+//***************************************************************************
+
+int cUpdate::storeAllRecordingInfoFiles()
+{
+ cEventDetails evd;
+ int count = 0;
+
+ tell(1, "Store info.epg2vdr for all recordings");
+
+ connection->startTransaction();
+ recordingListDb->clear();
+ recordingListDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+
+ for (int f = selectRecordings->find(); f && dbConnected(); f = selectRecordings->fetch())
+ {
+ char* path;
+
+ // bin 'ich' zuständig?
+
+ if (!recordingListDb->hasValue("VDRUUID", Epg2VdrConfig.uuid))
+ {
+ // nicht zuständig wenn OWNER nicht leer oder ich nicht am NAS Verbund teilnehme
+
+ if (!recordingListDb->hasValue("OWNER", "") || !Epg2VdrConfig.useCommonRecFolder)
+ continue;
+ }
+
+ asprintf(&path, "%s/%s", videoBasePath, recordingListDb->getStrValue("PATH"));
+
+ evd.loadFromFs(path);
+ evd.updateByRow(recordingListDb->getRow());
+
+ if (evd.getChanges())
+ {
+ count++;
+ evd.storeToFs(path);
+
+ tell(2, "Store recording info for %ld in info.epg2vdr with %d changes",
+ recordingListDb->getIntValue("EVENTID"), evd.getChanges());
+ }
+
+ time_t sp = time(0);
+ recordingListDb->setValue("LASTIFOUPD", sp);
+ recordingListDb->update(sp);
+
+ free(path);
+ }
+
+ connection->commit();
+ selectRecordings->freeResult();
+ storeAllRecordingInfoFilesTrigger = no;
+
+ tell(1, "Stored %d info.epg2vdr files", count);
+
+ return done;
+}
+
+//***************************************************************************
+// Update Recording Info Files
+//***************************************************************************
+
+int cUpdate::updateRecordingInfoFiles()
+{
+ cEventDetails evd;
+ int count = 0;
+
+ tell(1, "Update info.epg2vdr recordings");
+
+ connection->startTransaction();
+ recordingListDb->clear();
+
+ for (int f = selectRecForInfoUpdate->find(); f && dbConnected(); f = selectRecForInfoUpdate->fetch())
+ {
+ char* path;
+
+ asprintf(&path, "%s/%s", videoBasePath, recordingListDb->getStrValue("PATH"));
+
+ // only update if this VDR can access the recording folder ...
+
+ if (folderExists(path))
+ {
+ evd.loadFromFs(path);
+ evd.updateByRow(recordingListDb->getRow());
+
+ if (evd.getChanges())
+ {
+ count++;
+ evd.storeToFs(path);
+
+ tell(1, "Update recording info for %ld in info.epg2vdr with %d changes",
+ recordingListDb->getIntValue("EVENTID"), evd.getChanges());
+ }
+
+ time_t sp = time(0);
+ recordingListDb->setValue("LASTIFOUPD", sp);
+ recordingListDb->update(sp); // force same stamp!
+ }
+ else
+ {
+ tell(3, "Info: Folder '%s' not found, suppose it belong to another vdr", path);
+ }
+
+ free(path);
+ }
+
+ connection->commit();
+ selectRecForInfoUpdate->freeResult();
+ tell(1, "Updated %d info.epg2vdr files", count);
+
+ return done;
+}
+
+//***************************************************************************
+// Update Recording Table
+//***************************************************************************
+
+int cUpdate::updateRecordingTable(int fullReload)
+{
+ int count = 0, insCnt = 0, updCnt = 0, dirCnt = 0;
+
+ // first cleanup, at least to adjust count in 'lastRecordingCount'
+
+ if (fullReload)
+ recordingListDb->deleteWhere("vdruuid = '%s'", Epg2VdrConfig.uuid);
+ else
+ cleanupDeletedRecordings(yes);
+
+ // get channel and recordings lock
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cChannelsLock channelsLock(false);
+ const cChannels* channels = channelsLock.Channels();
+#else
+ cChannels* channels = &Channels;
+#endif
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cRecordingsLock recordingsLock(false);
+ const cRecordings* recordings = recordingsLock.Recordings();
+#else
+ const cRecordings* recordings = &Recordings;
+#endif
+
+ // update
+
+ tell(0, "Updating recording list table");
+
+ connection->startTransaction();
+
+ recordingDirDb->deleteWhere("vdruuid = '%s'", Epg2VdrConfig.uuid);
+
+ // ----------------
+ // update ...
+
+ for (const cRecording* rec = recordings->First(); rec; rec = recordings->Next(rec))
+ {
+ int insert;
+ int fsk;
+ md5Buf md5path;
+ const char* subTitle = "";
+ int eventId = 0;
+ std::string channelId = "";
+ const char* description = "";
+ const char* title = rec->Name();
+ const cRecordingInfo* recInfo = rec->Info();
+ int pathOffset = 0;
+ const cChannel* channel = 0;
+ int baseChanges = 0;
+
+ // check if directory is registered
+
+ if (rec->HierarchyLevels() > 0)
+ dirCnt += updateRecordingDirectory(rec);
+
+ // check if recording is registered
+
+ if (strncmp(rec->FileName(), videoBasePath, strlen(videoBasePath)) == 0)
+ {
+ pathOffset = strlen(videoBasePath);
+
+ if (*(rec->FileName()+pathOffset) == '/')
+ pathOffset++;
+ }
+
+ createMd5(rec->FileName()+pathOffset, md5path);
+
+ if (recInfo)
+ {
+ channelId = *(recInfo->ChannelID().ToString());
+ subTitle = recInfo->ShortText() ? recInfo->ShortText() : "";
+ description = recInfo->Description() ? recInfo->Description() : "";
+ channel = channels->GetByChannelID(recInfo->ChannelID());
+
+ if (recInfo->Title()) title = recInfo->Title();
+ if (recInfo->GetEvent()) eventId = recInfo->GetEvent()->EventID();
+ }
+
+ fsk = isProtected(rec->FileName());
+
+ recordingListDb->clear();
+
+ recordingListDb->setValue("MD5PATH", md5path);
+ recordingListDb->setValue("STARTTIME", rec->Start());
+ recordingListDb->setValue("OWNER", Epg2VdrConfig.useCommonRecFolder ? "" : Epg2VdrConfig.uuid);
+
+ insert = !recordingListDb->find();
+ recordingListDb->clearChanged();
+
+ // base data ..
+
+ recordingListDb->setValue("STATE", rec->IsInUse() == ruTimer ? "R" : "F");
+ recordingListDb->setValue("INUSE", rec->IsInUse());
+ recordingListDb->setValue("PATH", rec->FileName()+pathOffset);
+ recordingListDb->setValue("NAME", rec->BaseName());
+ recordingListDb->setValue("FOLDER", rec->Folder());
+ recordingListDb->setValue("LONGDESCRIPTION", description);
+ recordingListDb->setValue("DURATION", rec->LengthInSeconds() > 0 ? rec->LengthInSeconds() : 0);
+ recordingListDb->setValue("EVENTID", eventId);
+ recordingListDb->setValue("CHANNELID", channelId.c_str());
+ recordingListDb->setValue("FSK", fsk);
+
+ if (channel)
+ recordingListDb->setValue("CHANNELNAME", channel->Name());
+
+ // scraping relevand data ..
+
+ baseChanges = recordingListDb->getChanges();
+
+ recordingListDb->setValue("TITLE", title);
+ recordingListDb->setValue("SHORTTEXT", subTitle);
+
+ // load event details
+
+ cEventDetails evd;
+ evd.loadFromFs(rec->FileName());
+ evd.updateToRow(recordingListDb->getRow());
+
+ // any scrap relevand data changed?
+
+ if (recordingListDb->getChanges() != baseChanges)
+ {
+ int isSeries = recordingListDb->hasValue("CATEGORY", "Serie");
+ int changed = no;
+
+ if (isSeries)
+ {
+ if (recordingListDb->getValue("SCRSERIESID")->isEmpty() ||
+ !recordingListDb->hasValue("SCRSERIESEPISODE", recordingListDb->getIntValue("SCRINFOEPISODEID")) ||
+ !recordingListDb->hasValue("SCRSERIESID", recordingListDb->getIntValue("SCRINFOSERIESID")))
+ {
+ changed = yes;
+ }
+ }
+ else
+ {
+ if (recordingListDb->getValue("SCRMOVIEID")->isEmpty() ||
+ !recordingListDb->hasValue("SCRMOVIEID", recordingListDb->getIntValue("SCRINFOMOVIEID")))
+ {
+ changed = yes;
+ }
+ }
+
+ if (changed)
+ {
+ recordingListDb->setValue("SCRNEW", yes); // force scrap
+ recordingListDb->setValue("SCRSP", time(0)); // force load from vdr
+ }
+ }
+
+ // don't toggle uuid if already set!
+
+ if (recordingListDb->getValue("VDRUUID")->isNull())
+ recordingListDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+
+ if (insert || recordingListDb->getChanges())
+ {
+ insert ? insCnt++ : updCnt++;
+ tell(2, "Info: '%s' recording '%s / %s' due to %d changes [%s]",
+ insert ? "Insert" : "Update", title, subTitle,
+ recordingListDb->getChanges(), recordingListDb->getChangedFields().c_str());
+ recordingListDb->store();
+ }
+
+ count++;
+
+ recordingListDb->reset();
+ }
+
+ connection->commit();
+
+ tell(0, "Info: Found %d recordings; %d inserted; %d updated "
+ "and %d directories", count, insCnt, updCnt, dirCnt);
+
+ // create info files for new recordings
+
+ updatePendingRecordingInfoFiles(recordings);
+
+ return success;
+}
+
+//***************************************************************************
+// Update Recording Directory
+//***************************************************************************
+
+int cUpdate::updateRecordingDirectory(const cRecording* recording)
+{
+ int cnt = 0;
+ char* dir = strdup(recording->Name());
+ char* pos = strrchr(dir, '~');
+
+ if (pos)
+ {
+ *pos = 0;
+
+ for (int level = 0; level < recording->HierarchyLevels(); level++)
+ {
+ recordingDirDb->clear();
+ recordingDirDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+ recordingDirDb->setValue("DIRECTORY", dir);
+
+ if (!recordingDirDb->find())
+ {
+ cnt++;
+ recordingDirDb->store();
+ }
+
+ recordingDirDb->reset();
+
+ char* pos = strrchr(dir, '~');
+ if (pos) *pos=0;
+ }
+ }
+
+ free(dir);
+
+ return cnt;
+}
diff --git a/service.c b/service.c
new file mode 100644
index 0000000..20ccf14
--- /dev/null
+++ b/service.c
@@ -0,0 +1,71 @@
+/*
+ * service.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include "service.h"
+#include "plgconfig.h"
+
+//***************************************************************************
+// Class cEpgTimer
+//***************************************************************************
+
+cEpgTimer::cEpgTimer(bool Instant, bool Pause, const cChannel* Channel)
+ : cEpgTimer_Interface_V1(Instant, Pause, Channel)
+{
+ timerid = na; eventid = na;
+ vdrName = 0; vdrUuid = 0;
+ vdrRunning = no;
+ stateInfo = 0;
+ local = yes;
+}
+
+cEpgTimer::~cEpgTimer()
+{
+ free(vdrUuid);
+ free(vdrName);
+ free(stateInfo);
+}
+
+void cEpgTimer::setState(char s, const char* info)
+{
+ state = s;
+ free(stateInfo);
+ stateInfo = 0;
+
+ if (!isEmpty(info))
+ stateInfo = strdup(info);
+}
+
+void cEpgTimer::setAction(char a)
+{
+ action = a;
+}
+
+void cEpgTimer::setVdr(const char* name, const char* uuid, int running)
+{
+ local = yes; // the default
+ free(vdrUuid);
+ free(vdrName);
+ vdrName = strdup(name);
+
+ if (!isEmpty(uuid))
+ vdrUuid = strdup(uuid);
+
+ vdrRunning = running;
+
+ if (!isEmpty(vdrUuid) && strcmp(vdrUuid, Epg2VdrConfig.uuid) != 0)
+ local = no;
+}
+
+//***************************************************************************
+// Class cEpgEvent
+//***************************************************************************
+
+cEpgEvent::cEpgEvent(tEventID EventID)
+ : cEpgEvent_Interface_V1(EventID)
+{
+
+}
diff --git a/service.h b/service.h
new file mode 100644
index 0000000..7651316
--- /dev/null
+++ b/service.h
@@ -0,0 +1,127 @@
+/*
+ * service.h: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef _SERVICE_H_
+#define _SERVICE_H_
+
+#include <vdr/timers.h>
+#include <vdr/epg.h>
+
+#include <list>
+
+//***************************************************************************
+// Timer - Skin Interface
+//***************************************************************************
+
+class cEpgEvent_Interface_V1 : public cEvent
+{
+ public:
+
+ cEpgEvent_Interface_V1(tEventID EventID)
+ : cEvent(EventID) {}
+
+ // #TODO ... getter
+
+ protected:
+
+ // #TODO ... attributes
+};
+
+//***************************************************************************
+// Timer - Skin Interface
+//***************************************************************************
+
+class cEpgTimer_Interface_V1 : public cTimer
+{
+ public:
+
+ cEpgTimer_Interface_V1(bool Instant = false, bool Pause = false, const cChannel* Channel = 0)
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ : cTimer(Instant, Pause, Channel) {}
+#else
+ : cTimer(Instant, Pause, (cChannel*)Channel) {}
+#endif
+
+ long TimerId() { return timerid; }
+ long EventId() { return eventid; }
+ const char* VdrName() { return vdrName ? vdrName : ""; }
+ const char* VdrUuid() { return vdrUuid ? vdrUuid : ""; }
+ int isVdrRunning() { return vdrRunning; }
+ int isLocal() { return local; }
+ int isRemote() { return !isLocal(); }
+
+ char State() { return state; }
+ int hasState(char s) const { return state == s; }
+ const char* StateInfo() { return stateInfo ? stateInfo : ""; }
+ char Action() { return action; }
+
+ protected:
+
+ long timerid;
+ long eventid;
+
+ char* vdrName;
+ char* vdrUuid;
+ int local;
+ int vdrRunning;
+
+ char state;
+ char* stateInfo;
+ char action;
+};
+
+//***************************************************************************
+// Timer - Service Interface
+//***************************************************************************
+
+struct cEpgTimer_Service_V1
+{
+ std::list<cEpgTimer_Interface_V1*> epgTimers;
+};
+
+#define EPG2VDR_TIMER_UPDATED "Epg2Vdr_Timer_Updated-v1.0"
+#define EPG2VDR_TIMER_SERVICE "Epg2Vdr_Timer_Service-v1.0"
+
+#ifdef EPG2VDR
+
+//***************************************************************************
+// Class cEpgEvent
+//***************************************************************************
+
+class cEpgEvent : public cEpgEvent_Interface_V1
+{
+ public:
+
+ cEpgEvent(tEventID EventID);
+ virtual ~cEpgEvent() {}
+
+ // #TODO ... setter
+};
+
+//***************************************************************************
+// Class cEpgTimer
+//***************************************************************************
+
+class cEpgTimer : public cEpgTimer_Interface_V1
+{
+ public:
+
+ cEpgTimer(bool Instant = false, bool Pause = false, const cChannel* Channel = 0);
+ virtual ~cEpgTimer();
+
+ void setTimerId(long id) { timerid = id; }
+ void setEventId(long id) { eventid = id; }
+ void setState(char s, const char* info);
+ void setAction(char a);
+ void setVdr(const char* name, const char* uuid = 0, int running = 0);
+};
+
+#endif // EPG2VDR
+
+//***************************************************************************
+
+#endif // _SERVICE_H_
diff --git a/status.c b/status.c
new file mode 100644
index 0000000..e58d641
--- /dev/null
+++ b/status.c
@@ -0,0 +1,272 @@
+/*
+ * status.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <vdr/tools.h>
+
+#include "update.h"
+#include "ttools.h"
+
+//***************************************************************************
+// ----> Copied from epgsearch ;-)
+//***************************************************************************
+
+#define LOC_INDEXFILESUFFIX "/index"
+
+bool IsPesRecording(const cRecording *pRecording)
+{
+#if VDRVERSNUM < 10703
+ return true;
+#else
+ return pRecording && pRecording->IsPesRecording();
+#endif
+}
+
+#if VDRVERSNUM < 10703
+
+int RecLengthInSecs(const cRecording* pRecording)
+{
+ struct stat buf;
+ cString fullname = cString::sprintf("%s%s", pRecording->FileName(), "/index.vdr");
+
+ if (stat(fullname, &buf) == 0)
+ {
+ struct tIndex { int offset; uchar type; uchar number; short reserved; };
+ int delta = buf.st_size % sizeof(tIndex);
+
+ if (delta)
+ {
+ delta = sizeof(tIndex) - delta;
+ tell(0, "ERROR: invalid file size (%ld) in '%s'", buf.st_size, *fullname);
+ }
+
+ return (buf.st_size + delta) / sizeof(tIndex) / SecondsToFrames(1);
+ }
+
+ return -1;
+}
+
+#else
+
+struct tIndexTs
+{
+ uint64_t offset:40; // up to 1TB per file (not using off_t here - must definitely be exactly 64 bit!)
+ int reserved:7; // reserved for future use
+ int independent:1; // marks frames that can be displayed by themselves (for trick modes)
+ uint16_t number:16; // up to 64K files per recording
+
+ tIndexTs(off_t Offset, bool Independent, uint16_t Number)
+ {
+ offset = Offset;
+ reserved = 0;
+ independent = Independent;
+ number = Number;
+ }
+};
+
+int RecLengthInSecs(const cRecording *pRecording)
+{
+ struct stat buf;
+ cString fullname = cString::sprintf("%s%s", pRecording->FileName(), IsPesRecording(pRecording) ? LOC_INDEXFILESUFFIX ".vdr" : LOC_INDEXFILESUFFIX);
+
+ if (pRecording->FileName() && *fullname && access(fullname, R_OK) == 0 && stat(fullname, &buf) == 0)
+ {
+ double frames = buf.st_size ? (buf.st_size - 1) / sizeof(tIndexTs) + 1 : 0;
+ double Seconds = 0;
+ modf((frames + 0.5) / pRecording->FramesPerSecond(), &Seconds);
+ return Seconds;
+ }
+
+ return -1;
+}
+
+#endif
+
+//***************************************************************************
+// Copied from epgsearch ;-) <----------
+//***************************************************************************
+
+//***************************************************************************
+// Notifications from VDRs Status Interface
+//***************************************************************************
+//***************************************************************************
+// Timers Change Notification
+//***************************************************************************
+
+void cUpdate::TimerChange(const cTimer* Timer, eTimerChange Change)
+{
+ if (!Epg2VdrConfig.shareInWeb)
+ return;
+
+ tell(1, "Timer changed, trigger update. Action was (%d)", Change);
+
+ timerTableUpdateTriggered = yes;
+ waitCondition.Broadcast(); // wakeup
+}
+
+//***************************************************************************
+// Recording Notification
+//***************************************************************************
+
+void cUpdate::Recording(const cDevice* Device, const char* Name, const char* FileName, bool On)
+{
+ cMutexLock lock(&runningRecMutex);
+ const int allowedBreakDuration = 2;
+
+ // Recording of 'Peter Hase' has 'started' [/srv/vdr/video.00/Peter_Hase/2014-10-08.11.05.18-0.rec]
+ // Recording of '(null)' has 'stopped' [/srv/vdr/video.00/Peter_Hase/2014-10-08.11.05.18-0.rec]
+
+ tell(1, "Recording of '%s' has '%s' [%s]", Name, On ? "started" : "stopped", FileName);
+
+ // at start of recording store event details to recording directory (info.epg2vdr)
+
+ if (On)
+ pendingNewRecordings.push(FileName);
+
+ // get timers lock
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cTimersLock timersLock(false);
+ const cTimers* timers = timersLock.Timers();
+#else
+ const cTimers* timers = &Timers;
+#endif
+
+ // recording started ...
+
+ if (On && Name)
+ {
+ for (const cTimer* ti = timers->First(); ti; ti = timers->Next(ti))
+ {
+ if (ti->Recording()) // timer nimmt gerade auf
+ {
+ cRunningRecording* recording = 0;
+
+ // check if already known
+
+ for (cRunningRecording* rr = runningRecordings.First(); rr; rr = runningRecordings.Next(rr))
+ {
+ if (rr->timer == ti)
+ {
+ recording = rr;
+ break;
+ }
+ }
+
+ if (recording) // already handled -> a resume?!
+ {
+ tell(1, "Info: Detected resume of '%s' on device %d", Name, Device->CardIndex());
+ continue;
+ }
+
+ int doneid = na;
+ contentOfTag(ti, "doneid", doneid);
+
+ recording = new cRunningRecording(ti, doneid);
+ runningRecordings.Add(recording);
+ tell(1, "Info: Recording '%s' with doneid %d added to running list", Name, doneid);
+ }
+ }
+ }
+
+ // recording stopped ...
+
+ if (!On)
+ {
+ // loop over running recordings ..
+
+ for (cRunningRecording* rr = runningRecordings.First(); rr; rr = runningRecordings.Next(rr))
+ {
+ const cTimer* pendingTimer = 0;
+ int complete;
+ int recFraction = 100;
+ long timerLengthSecs = rr->timer->StopTime() - rr->timer->StartTime();
+ bool vpsUsed = rr->timer->HasFlags(tfVps) && rr->timer->Event() && rr->timer->Event()->Vps();
+
+ // check if timer still exists
+
+ for (pendingTimer = timers->First(); pendingTimer; pendingTimer = timers->Next(pendingTimer))
+ {
+ if (pendingTimer == rr->timer)
+ break;
+ }
+
+ // still recording :o ?
+
+ if (pendingTimer && pendingTimer->Recording())
+ continue;
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cStateKey stateKey;
+
+ if (const cRecordings* recordings = cRecordings::GetRecordingsRead(stateKey))
+ {
+ const cRecording* pRecording = recordings->GetByName(FileName);
+
+ if (pRecording && timerLengthSecs)
+ {
+ int recLen = RecLengthInSecs(pRecording);
+ recFraction = double(recLen) * 100 / timerLengthSecs;
+ }
+
+ stateKey.Remove();
+ }
+#else
+ const cRecording* pRecording = Recordings.GetByName(FileName);
+
+ if (pRecording && timerLengthSecs)
+ {
+ int recLen = RecLengthInSecs(pRecording);
+ recFraction = double(recLen) * 100 / timerLengthSecs;
+ }
+#endif
+
+ // assure timer has reached it's end or at least 90% (vps) / 98% were recorded
+
+ complete = recFraction >= (vpsUsed ? 90 : 98);
+
+ if (complete)
+ tell(1, "Info: Finished: '%s'; recorded %d%%; VPS %s",
+ rr->timer->File(), recFraction, vpsUsed ? "Yes": "No");
+ else
+ tell(1, "Info: Finished: '%s' (not complete! - recorded only %d%%); VPS %s",
+ rr->timer->File(), recFraction, vpsUsed ? "Yes": "No");
+
+ if (complete)
+ rr->lastBreak = 0; // reset break
+ else if (!rr->lastBreak)
+ rr->lastBreak = time(0); // store first break
+
+ if (!rr->lastBreak || (time(0) - rr->lastBreak) > allowedBreakDuration)
+ {
+ char* infoTxt;
+
+ asprintf(&infoTxt, "Recording '%s' finished - %s complete (%d%%)",
+ rr->timer->File(), complete ? "" : "NOT", recFraction);
+
+ tell(1, "Info: %s", infoTxt);
+
+ rr->finished = yes;
+ rr->failed = !complete;
+ rr->setInfo(infoTxt);
+
+ free(infoTxt);
+ }
+ }
+ }
+
+ recordingStateChangedTrigger = yes;
+ waitCondition.Broadcast(); // wakeup
+}
+
+//***************************************************************************
+// Channel Switch Notification
+//***************************************************************************
+
+void cUpdate::ChannelSwitch(const cDevice* Device, int ChannelNumber, bool LiveView)
+{
+ // to be implemented ... ???
+}
diff --git a/svdrpclient.c b/svdrpclient.c
new file mode 100644
index 0000000..53be015
--- /dev/null
+++ b/svdrpclient.c
@@ -0,0 +1,749 @@
+ /*
+ * svdrpclient.c
+ *
+ * See the README file for copyright information
+ *
+ */
+
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/select.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <sys/time.h>
+
+#include <unistd.h>
+#include <netdb.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <errno.h>
+
+#include "lib/common.h"
+
+#include "svdrpclient.h"
+
+#ifndef VDR_PLUGIN
+
+ssize_t safe_read(int filedes, void *buffer, size_t size)
+{
+ for (;;)
+ {
+ ssize_t p = read(filedes, buffer, size);
+
+ if (p < 0 && errno == EINTR)
+ {
+ tell(0, "EINTR while reading from file handle %d - retrying", filedes);
+ continue;
+ }
+ return p;
+ }
+}
+
+ssize_t safe_write(int filedes, const void *buffer, size_t size)
+{
+ ssize_t p = 0;
+ ssize_t written = size;
+ const unsigned char *ptr = (const unsigned char *)buffer;
+
+ while (size > 0)
+ {
+ p = write(filedes, ptr, size);
+
+ if (p < 0)
+ {
+ if (errno == EINTR)
+ {
+ tell(0, "EINTR while writing to file handle %d - retrying", filedes);
+ continue;
+ }
+
+ break;
+ }
+ ptr += p;
+ size -= p;
+ }
+
+ return p < 0 ? p : written;
+}
+
+// --- cListObject -----------------------------------------------------------
+
+cListObject::cListObject(void)
+{
+ prev = next = NULL;
+}
+
+cListObject::~cListObject()
+{
+}
+
+void cListObject::Append(cListObject *Object)
+{
+ next = Object;
+ Object->prev = this;
+}
+
+void cListObject::Insert(cListObject *Object)
+{
+ prev = Object;
+ Object->next = this;
+}
+
+void cListObject::Unlink(void)
+{
+ if (next)
+ next->prev = prev;
+ if (prev)
+ prev->next = next;
+ next = prev = NULL;
+}
+
+int cListObject::Index(void) const
+{
+ cListObject *p = prev;
+ int i = 0;
+
+ while (p) {
+ i++;
+ p = p->prev;
+ }
+ return i;
+}
+
+// --- cListBase -------------------------------------------------------------
+
+cListBase::cListBase(void)
+{
+ objects = lastObject = NULL;
+ count = 0;
+}
+
+cListBase::~cListBase()
+{
+ Clear();
+}
+
+void cListBase::Add(cListObject *Object, cListObject *After)
+{
+ if (After && After != lastObject) {
+ After->Next()->Insert(Object);
+ After->Append(Object);
+ }
+ else {
+ if (lastObject)
+ lastObject->Append(Object);
+ else
+ objects = Object;
+ lastObject = Object;
+ }
+ count++;
+}
+
+void cListBase::Ins(cListObject *Object, cListObject *Before)
+{
+ if (Before && Before != objects) {
+ Before->Prev()->Append(Object);
+ Before->Insert(Object);
+ }
+ else {
+ if (objects)
+ objects->Insert(Object);
+ else
+ lastObject = Object;
+ objects = Object;
+ }
+ count++;
+}
+
+void cListBase::Del(cListObject *Object, bool DeleteObject)
+{
+ if (Object == objects)
+ objects = Object->Next();
+ if (Object == lastObject)
+ lastObject = Object->Prev();
+ Object->Unlink();
+ if (DeleteObject)
+ delete Object;
+ count--;
+}
+
+void cListBase::Move(int From, int To)
+{
+ Move(Get(From), Get(To));
+}
+
+void cListBase::Move(cListObject *From, cListObject *To)
+{
+ if (From && To && From != To) {
+ if (From->Index() < To->Index())
+ To = To->Next();
+ if (From == objects)
+ objects = From->Next();
+ if (From == lastObject)
+ lastObject = From->Prev();
+ From->Unlink();
+ if (To) {
+ if (To->Prev())
+ To->Prev()->Append(From);
+ From->Append(To);
+ }
+ else {
+ lastObject->Append(From);
+ lastObject = From;
+ }
+ if (!From->Prev())
+ objects = From;
+ }
+}
+
+void cListBase::Clear(void)
+{
+ while (objects) {
+ cListObject *object = objects->Next();
+ delete objects;
+ objects = object;
+ }
+ objects = lastObject = NULL;
+ count = 0;
+}
+
+cListObject *cListBase::Get(int Index) const
+{
+ if (Index < 0)
+ return NULL;
+ cListObject *object = objects;
+ while (object && Index-- > 0)
+ object = object->Next();
+ return object;
+}
+
+static int CompareListObjects(const void *a, const void *b)
+{
+ const cListObject *la = *(const cListObject **)a;
+ const cListObject *lb = *(const cListObject **)b;
+ return la->Compare(*lb);
+}
+
+void cListBase::Sort(void)
+{
+ int n = Count();
+ cListObject** a;
+ cListObject* object = objects;
+ int i = 0;
+
+ a = (cListObject**)malloc(n * sizeof(cListObject*));
+
+ while (object && i < n)
+ {
+ a[0] = 0;
+ a[i++] = object;
+ object = object->Next();
+ }
+
+ qsort(a, n, sizeof(cListObject*), CompareListObjects);
+ objects = lastObject = NULL;
+
+ for (i = 0; i < n; i++)
+ {
+ a[i]->Unlink();
+ count--;
+ Add(a[i]);
+ }
+
+ free(a);
+}
+
+// --- cTimeMs ---------------------------------------------------------------
+
+cTimeMs::cTimeMs(int Ms)
+{
+ if (Ms >= 0)
+ Set(Ms);
+ else
+ begin = 0;
+}
+
+uint64_t cTimeMs::Now(void)
+{
+#if _POSIX_TIMERS > 0 && defined(_POSIX_MONOTONIC_CLOCK)
+#define MIN_RESOLUTION 5 // ms
+ static bool initialized = false;
+ static bool monotonic = false;
+ struct timespec tp;
+ if (!initialized) {
+ // check if monotonic timer is available and provides enough accurate resolution:
+ if (clock_getres(CLOCK_MONOTONIC, &tp) == 0) {
+ // long Resolution = tp.tv_nsec;
+ // require a minimum resolution:
+ if (tp.tv_sec == 0 && tp.tv_nsec <= MIN_RESOLUTION * 1000000) {
+ if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0) {
+ // tell(0, "cTimeMs: using monotonic clock (resolution is %ld ns)", Resolution);
+ monotonic = true;
+ }
+ else
+ tell(0, "cTimeMs: clock_gettime(CLOCK_MONOTONIC) failed");
+ }
+ else
+ tell(0, "cTimeMs: not using monotonic clock - resolution is too bad (%ld s %ld ns)", tp.tv_sec, tp.tv_nsec);
+ }
+ else
+ tell(0, "cTimeMs: clock_getres(CLOCK_MONOTONIC) failed");
+ initialized = true;
+ }
+ if (monotonic) {
+ if (clock_gettime(CLOCK_MONOTONIC, &tp) == 0)
+ return (uint64_t(tp.tv_sec)) * 1000 + tp.tv_nsec / 1000000;
+ tell(0, "cTimeMs: clock_gettime(CLOCK_MONOTONIC) failed");
+ monotonic = false;
+ // fall back to gettimeofday()
+ }
+#else
+# warning Posix monotonic clock not available
+#endif
+ struct timeval t;
+ if (gettimeofday(&t, NULL) == 0)
+ return (uint64_t(t.tv_sec)) * 1000 + t.tv_usec / 1000;
+ return 0;
+}
+
+void cTimeMs::Set(int Ms)
+{
+ begin = Now() + Ms;
+}
+
+bool cTimeMs::TimedOut(void)
+{
+ return Now() >= begin;
+}
+
+uint64_t cTimeMs::Elapsed(void)
+{
+ return Now() - begin;
+}
+
+
+// --- cFile -----------------------------------------------------------------
+
+bool cFile::files[FD_SETSIZE] = { false };
+int cFile::maxFiles = 0;
+
+cFile::cFile(void)
+{
+ f = -1;
+}
+
+cFile::~cFile()
+{
+ Close();
+}
+
+bool cFile::Open(const char *FileName, int Flags, mode_t Mode)
+{
+ if (!IsOpen())
+ return Open(open(FileName, Flags, Mode));
+ tell(0, "ERROR: attempt to re-open %s", FileName);
+ return false;
+}
+
+bool cFile::Open(int FileDes)
+{
+ if (FileDes >= 0) {
+ if (!IsOpen()) {
+ f = FileDes;
+ if (f >= 0) {
+ if (f < FD_SETSIZE) {
+ if (f >= maxFiles)
+ maxFiles = f + 1;
+ if (!files[f])
+ files[f] = true;
+ else
+ tell(0, "ERROR: file descriptor %d already in files[]", f);
+ return true;
+ }
+ else
+ tell(0, "ERROR: file descriptor %d is larger than FD_SETSIZE (%d)", f, FD_SETSIZE);
+ }
+ }
+ else
+ tell(0, "ERROR: attempt to re-open file descriptor %d", FileDes);
+ }
+ return false;
+}
+
+void cFile::Close(void)
+{
+ if (f >= 0) {
+ close(f);
+ files[f] = false;
+ f = -1;
+ }
+}
+
+bool cFile::Ready(bool Wait)
+{
+ return f >= 0 && AnyFileReady(f, Wait ? 1000 : 0);
+}
+
+bool cFile::AnyFileReady(int FileDes, int TimeoutMs)
+{
+ fd_set set;
+ FD_ZERO(&set);
+ for (int i = 0; i < maxFiles; i++) {
+ if (files[i])
+ FD_SET(i, &set);
+ }
+ if (0 <= FileDes && FileDes < FD_SETSIZE && !files[FileDes])
+ FD_SET(FileDes, &set); // in case we come in with an arbitrary descriptor
+ if (TimeoutMs == 0)
+ TimeoutMs = 10; // load gets too heavy with 0
+ struct timeval timeout;
+ timeout.tv_sec = TimeoutMs / 1000;
+ timeout.tv_usec = (TimeoutMs % 1000) * 1000;
+ return select(FD_SETSIZE, &set, NULL, NULL, &timeout) > 0 && (FileDes < 0 || FD_ISSET(FileDes, &set));
+}
+
+bool cFile::FileReady(int FileDes, int TimeoutMs)
+{
+ fd_set set;
+ struct timeval timeout;
+ FD_ZERO(&set);
+ FD_SET(FileDes, &set);
+ if (TimeoutMs >= 0) {
+ if (TimeoutMs < 100)
+ TimeoutMs = 100;
+ timeout.tv_sec = TimeoutMs / 1000;
+ timeout.tv_usec = (TimeoutMs % 1000) * 1000;
+ }
+ return select(FD_SETSIZE, &set, NULL, NULL, (TimeoutMs >= 0) ? &timeout : NULL) > 0 && FD_ISSET(FileDes, &set);
+}
+
+bool cFile::FileReadyForWriting(int FileDes, int TimeoutMs)
+{
+ fd_set set;
+ struct timeval timeout;
+ FD_ZERO(&set);
+ FD_SET(FileDes, &set);
+ if (TimeoutMs < 100)
+ TimeoutMs = 100;
+ timeout.tv_sec = 0;
+ timeout.tv_usec = TimeoutMs * 1000;
+ return select(FD_SETSIZE, NULL, &set, NULL, &timeout) > 0 && FD_ISSET(FileDes, &set);
+}
+
+#endif // VDR_PLUGIN
+
+//***************************************************************************
+// SVDRP Client
+//***************************************************************************
+
+const char* eoc = "\n"; // End Of Command
+
+cSvdrpClient::cSvdrpClient(const char* aIp, int aPort)
+{
+ port = aPort;
+ ip = aIp ? ::strdup(aIp) : 0;
+ bufSize = BUFSIZ;
+ buffer = (char*)malloc(bufSize);
+}
+
+cSvdrpClient::~cSvdrpClient(void)
+{
+ close();
+
+ free(ip);
+ free(buffer);
+}
+
+//***************************************************************************
+// Connect
+//***************************************************************************
+
+int cSvdrpClient::connect()
+{
+ if (!ip)
+ {
+ tell(0, "SVDRPCL: No server IP specified");
+ return -1;
+ }
+
+ struct hostent* hostInfo;
+ struct sockaddr_in server_addr;
+ server_addr.sin_family = AF_INET;
+ server_addr.sin_port = htons(port);
+ in_addr_t remoteAddr;
+
+ if (!isdigit(ip[1]))
+ {
+ // map hostname to ip
+
+ if (hostInfo = ::gethostbyname(ip))
+ memcpy((char*)&remoteAddr, hostInfo->h_addr, hostInfo->h_length);
+
+ else if ((remoteAddr = inet_addr(ip)) == INADDR_NONE)
+ return -1;
+
+ memcpy(&server_addr.sin_addr, &remoteAddr, sizeof(struct in_addr));
+ }
+ else if (!::inet_aton(ip, &server_addr.sin_addr))
+ {
+ tell(0, "SVDRPCL: Invalid server IP '%s'", ip);
+ return -1;
+ }
+
+ int sock = ::socket(PF_INET, SOCK_STREAM, 0);
+
+ if (sock < 0)
+ {
+ tell(0, "SVDRPCL: Error creating socket for connection to %s: %s", ip, strerror(errno));
+ return -1;
+ }
+
+ // set nonblocking
+
+ int flags = ::fcntl(sock, F_GETFL, 0);
+
+ if (flags < 0 || ::fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0)
+ {
+ tell(0, "SVDRPCL: Unable to use nonblocking I/O for %s: %s", ip, strerror(errno));
+ return -1;
+ }
+
+ if (::connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0)
+ {
+ if (errno != EINPROGRESS)
+ {
+ tell(0, "SVDRPCL: connect to %s:%hu failed: %s", ip, port, strerror(errno));
+ return -1;
+ }
+
+ int result;
+ fd_set fds;
+ struct timeval tv;
+ cTimeMs starttime;
+ int timeout = 10 * 1000;
+
+ do {
+ FD_ZERO(&fds);
+ FD_SET(sock, &fds);
+ tv.tv_usec = (timeout % 1000) * 1000;
+ tv.tv_sec = timeout / 1000;
+ result = ::select(sock + 1, 0, &fds, 0, &tv);
+
+ } while (result == -1 && errno == EINTR
+ && (timeout = 10 * 1000 - starttime.Elapsed()) > 100);
+
+ if (!result) // timeout
+ {
+ result = -1;
+ errno = ETIMEDOUT;
+ }
+ else if (result == 1) // check socket for errors
+ {
+ int error;
+ socklen_t size = sizeof(error);
+ result = ::getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &size);
+ if (result == 0 && error != 0)
+ {
+ result = -1;
+ errno = error;
+ }
+ }
+
+ if (result != 0)
+ {
+ tell(0, "SVDRPCL: Error connecting to %s:%hu: %s", ip, port, strerror(errno));
+ ::close(sock);
+
+ return -1;
+ }
+ }
+
+ return sock;
+}
+
+int cSvdrpClient::open()
+{
+ if (file.IsOpen())
+ return 0;
+
+ int fd = connect();
+
+ if (fd < 0)
+ return -1;
+
+ if (!file.Open(fd))
+ {
+ ::close(fd);
+ return -1;
+ }
+
+ // check for greeting
+
+ cList<cLine> greeting;
+
+ if (receive(&greeting) != 220)
+ {
+ tell(0, "SVDRPCL: did not receive greeting from %s. Closing...", ip);
+ abort();
+
+ return -1;
+ }
+
+ const char* msg = 0;
+
+ if (greeting.First() && greeting.First()->Text())
+ msg = greeting.First()->Text();
+
+ tell(2, "SVDRPCL: connected to %s:%hu '%s'", ip, port, msg);
+
+ return 0;
+}
+
+void cSvdrpClient::close(int sendQuit)
+{
+ if (!file.IsOpen())
+ return;
+
+ if (sendQuit)
+ {
+ if (send("QUIT", false))
+ receive();
+ }
+
+ file.Close();
+}
+
+void cSvdrpClient::abort(void)
+{
+ file.Close();
+}
+
+int cSvdrpClient::send(const char* cmd, int reconnect)
+{
+ if (!cmd)
+ return false;
+
+ if (reconnect && !file.IsOpen())
+ open();
+
+ if (!file.IsOpen())
+ {
+ tell(0, "SVDRPCL: unable to send command to %s. Socket is closed", ip);
+ return false;
+ }
+
+ int len = ::strlen(cmd);
+
+ if (safe_write(file, cmd, len) < 0 || safe_write(file, eoc, strlen(eoc)) < 0)
+ {
+ tell(0, "SVDRPCL: error while writing to %s: %s", ip, strerror(errno));
+ abort();
+
+ return false;
+ }
+
+ return true;
+}
+
+int cSvdrpClient::receive(cList<cLine>* list, int timeoutMs)
+{
+ // #TODO iconv ?!
+
+ while (readLine(timeoutMs))
+ {
+ char* tail;
+ long int code = ::strtol(buffer, &tail, 10);
+
+ if (tail - buffer == 3
+ && code >= 100
+ && code <= 999
+ && (*tail == ' ' || *tail == '-'))
+ {
+ const char* s = buffer + 4;
+
+ if (code >= 451 && code <= 699)
+ tell(0, "Error: Got (%ld) '%s' form ", code, s);
+
+ if (list)
+ list->Add(new cLine(s));
+
+ if (*tail == ' ')
+ return code;
+ }
+ else
+ {
+ tell(0, "SVDRPCL: Unexpected reply from %s '%s'", ip, buffer);
+ close();
+ break;
+ }
+ }
+
+ if (list)
+ list->Clear();
+
+ return 0;
+}
+
+int cSvdrpClient::readLine(int timeoutMs)
+{
+ if (!file.IsOpen())
+ return false;
+
+ int tail = 0;
+
+ while (cFile::FileReady(file, timeoutMs))
+ {
+ unsigned char c;
+ int r = safe_read(file, &c, 1);
+
+ if (r > 0)
+ {
+ if (c == *eoc || c == 0x00)
+ {
+ // line complete, make sure the string is terminated
+
+ buffer[tail] = 0;
+ return true;
+ }
+ else if ((c <= 0x1F || c == 0x7F) && c != 0x09)
+ {
+ // ignore control characters
+ }
+ else
+ {
+ if (tail >= bufSize - 1)
+ {
+ bufSize += BUFSIZ;
+ buffer = srealloc(buffer, bufSize);
+
+ if (!buffer)
+ {
+ tell(0, "SVDRPCL: unable to increase buffer size to %d byte", bufSize);
+ close();
+
+ return false;
+ }
+ }
+
+ buffer[tail++] = c;
+ }
+ }
+ else
+ {
+ tell(0, "SVDRPCL: lost connection '%s'", ip);
+ buffer[0] = 0;
+ abort();
+
+ return false;
+ }
+ }
+
+ tell(0, "SVDRPCL: timeout waiting server reply '%s'", ip);
+ buffer[0] = 0;
+ abort();
+
+ return false;
+}
diff --git a/svdrpclient.h b/svdrpclient.h
new file mode 100644
index 0000000..f0f7078
--- /dev/null
+++ b/svdrpclient.h
@@ -0,0 +1,158 @@
+/*
+ * svdrpclient.h: SVDRP connection
+ *
+ * See the README file for copyright information and how to reach the author.
+ */
+
+#ifndef __SVDRP_CLIENT_H_
+#define __SVDRP_CLIENT_H_
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <stdint.h> // uint_64_t
+#include <string.h>
+#include <stdio.h>
+
+#include "lib/common.h"
+
+#ifdef VDR_PLUGIN
+# define __STL_CONFIG_H
+# include <vdr/tools.h>
+#else
+
+//***************************************************************************
+//
+//***************************************************************************
+
+class cListObject {
+private:
+ cListObject *prev, *next;
+public:
+ cListObject(void);
+ virtual ~cListObject();
+ virtual int Compare(const cListObject &ListObject) const { return 0; }
+ ///< Must return 0 if this object is equal to ListObject, a positive value
+ ///< if it is "greater", and a negative value if it is "smaller".
+ void Append(cListObject *Object);
+ void Insert(cListObject *Object);
+ void Unlink(void);
+ int Index(void) const;
+ cListObject *Prev(void) const { return prev; }
+ cListObject *Next(void) const { return next; }
+};
+
+class cListBase {
+protected:
+ cListObject *objects, *lastObject;
+ cListBase(void);
+ int count;
+public:
+ virtual ~cListBase();
+ void Add(cListObject *Object, cListObject *After = NULL);
+ void Ins(cListObject *Object, cListObject *Before = NULL);
+ void Del(cListObject *Object, bool DeleteObject = true);
+ virtual void Move(int From, int To);
+ void Move(cListObject *From, cListObject *To);
+ virtual void Clear(void);
+ cListObject *Get(int Index) const;
+ int Count(void) const { return count; }
+ void Sort(void);
+};
+
+template<class T> class cList : public cListBase {
+public:
+ T *Get(int Index) const { return (T *)cListBase::Get(Index); }
+ T *First(void) const { return (T *)objects; }
+ T *Last(void) const { return (T *)lastObject; }
+ T *Prev(const T *object) const { return (T *)object->cListObject::Prev(); } // need to call cListObject's members to
+ T *Next(const T *object) const { return (T *)object->cListObject::Next(); } // avoid ambiguities in case of a "list of lists"
+};
+
+class cFile {
+private:
+ static bool files[];
+ static int maxFiles;
+ int f;
+public:
+ cFile(void);
+ ~cFile();
+ operator int () { return f; }
+ bool Open(const char *FileName, int Flags, mode_t Mode = DEFFILEMODE);
+ bool Open(int FileDes);
+ void Close();
+ bool IsOpen(void) { return f >= 0; }
+ bool Ready(bool Wait = true);
+ static bool AnyFileReady(int FileDes = -1, int TimeoutMs = 1000);
+ static bool FileReady(int FileDes, int TimeoutMs = 1000);
+ static bool FileReadyForWriting(int FileDes, int TimeoutMs = 1000);
+};
+
+class cTimeMs {
+private:
+ uint64_t begin;
+public:
+ cTimeMs(int Ms = 0);
+ ///< Creates a timer with ms resolution and an initial timeout of Ms.
+ ///< If Ms is negative the timer is not initialized with the current
+ ///< time.
+ static uint64_t Now(void);
+ void Set(int Ms = 0);
+ bool TimedOut(void);
+ uint64_t Elapsed(void);
+ };
+
+#endif // VDR_PLUGIN
+
+
+//***************************************************************************
+// Line
+//***************************************************************************
+
+class cLine : public cListObject
+{
+ public:
+
+ cLine(const char *s) { line = s ? strdup(s) : 0; };
+ virtual ~cLine() { if (line) free(line); };
+
+ const char* Text() { return line; }
+ int Length() { return strlen(line); }
+
+ private:
+
+ char* line;
+};
+
+//***************************************************************************
+// SVDRP Client
+//***************************************************************************
+
+class cSvdrpClient
+{
+ private:
+
+ char* ip;
+ int port;
+ cFile file;
+ char* buffer;
+ int bufSize;
+
+ int connect();
+ int readLine(int timeoutMs);
+
+ public:
+
+ cSvdrpClient(const char *aIp, int aPort);
+ virtual ~cSvdrpClient();
+
+ int open();
+ void close(int sendQuit = yes);
+ void abort();
+
+ int send(const char* cmd, int reconnect = true);
+ int receive(cList<cLine>* list = 0, int timeoutMs = 20 * 1000);
+};
+
+//***************************************************************************
+#endif // __SVDRP_CLIENT_H_
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 <vdr/tools.h>
+#include <vdr/menu.h>
+
+#include "update.h"
+#include "ttools.h"
+#include "service.h"
+
+//***************************************************************************
+// Has Timer Changed
+//***************************************************************************
+
+int cUpdate::timerChanged()
+{
+ int maxSp = 0;
+ int changed = no;
+
+ selectMaxUpdSp->execute();
+ maxSp = timerDb->getIntValue("UPDSP");
+
+ if (timersTableMaxUpdsp != maxSp)
+ {
+ timersTableMaxUpdsp = maxSp;
+ changed = yes;
+
+ cPluginManager::CallAllServices(EPG2VDR_TIMER_UPDATED, &timersTableMaxUpdsp);
+ }
+
+ selectMaxUpdSp->freeResult();
+
+ return changed;
+}
+
+//***************************************************************************
+// Perform Timer Jobs
+//***************************************************************************
+
+int cUpdate::performTimerJobs()
+{
+ int createCount = 0;
+ int modifyCount = 0;
+ int deleteCount = 0;
+ uint64_t start = cTimeMs::Now();
+
+ tell(1, "Checking pending timer actions ..");
+
+ // check if timer pending
+ {
+ timerDb->clear();
+ timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+
+ if (!selectPendingTimerActions->find())
+ {
+ selectPendingTimerActions->freeResult();
+ tell(1, ".. nothing to do");
+ timerJobsUpdateTriggered = no;
+ return done;
+ }
+
+ selectPendingTimerActions->freeResult();
+ }
+
+ // get timers lock
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cTimersLock timersLock(true);
+ cTimers* timers = timersLock.Timers();
+#else
+ cTimers* timers = &Timers;
+#endif
+
+ // get schedules lock
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cStateKey schedulesKey;
+ const cSchedules* schedules = cSchedules::GetSchedulesRead(schedulesKey, 100/*ms*/);
+#else
+ cSchedulesLock* schedulesLock = new cSchedulesLock(false, 100/*ms*/);
+ const cSchedules* schedules = (cSchedules*)cSchedules::Schedules(*schedulesLock);
+#endif
+
+ if (!schedules)
+ {
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ schedulesKey.Remove();
+#else
+ delete schedulesLock;
+#endif
+ tell(0, "Info: Can't get lock on schedules, skipping timer update!");
+ return fail;
+ }
+
+ // iterate pending actions ...
+
+ timerDb->clear();
+ timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+
+ for (int f = selectPendingTimerActions->find(); f && dbConnected(); f = selectPendingTimerActions->fetch())
+ {
+ int timerid = timerDb->getIntValue("ID");
+ int doneid = na;
+ long eventid = timerDb->getValue("EVENTID")->isNull() ? na : timerDb->getIntValue("EVENTID");
+ tChannelID channelId = tChannelID::FromString(timerDb->getStrValue("CHANNELID"));
+ const cEvent* event = 0;
+ char requetedAction = timerDb->getStrValue("ACTION")[0];
+ cTimer* timer = getTimerById(timers, timerid); // lookup VDRs timer object
+ int insert = !timer;
+
+ if (!timerDb->getValue("DONEID")->isEmpty())
+ doneid = timerDb->getIntValue("DONEID");
+
+ tell(1, "DEBUG: Pending Action '%c' for timer (%d), event %ld, doneid %d", requetedAction, timerid, eventid, doneid);
+
+ // --------------------------------
+ // Delete timer request
+
+ if (requetedAction == taDelete || requetedAction == taReject)
+ {
+ if (timerDb->hasValue("VDRUUID", "any"))
+ {
+ tell(0, "Error: Ignoring delete/reject request of timer (%d) without VDRUUID", timerid);
+ timerDb->getValue("INFO")->sPrintf("Error: Ignoring delete/reject request of timer (%d) without VDRUUID", timerid);
+ timerDb->setCharValue("ACTION", taFailed);
+ timerDb->setCharValue("STATE", tsError);
+ timerDb->update();
+ updateTimerDone(timerid, doneid, tdsTimerCreateFailed);
+ continue;
+ }
+
+ // delete VDRs timer
+
+ if (timer)
+ {
+ if (timer->Recording())
+ {
+ timer->Skip();
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20302)
+ cRecordControls::Process(timers, time(0));
+#else
+ cRecordControls::Process(time(0));
+#endif
+ }
+
+ timers->Del(timer);
+ deleteCount++;
+ tell(0, "Deleted timer %d", timerid);
+ }
+ else
+ {
+ tell(0, "Info: Timer (%d) not found, ignoring delete/reject request", timerid);
+ }
+
+ updateTimerDone(timerid, doneid, requetedAction == taDelete ? tdsTimerDeleted : tdsTimerRejected);
+ timerDb->setCharValue("ACTION", taAssumed);
+ timerDb->setCharValue("STATE", tsDeleted);
+ timerDb->update();
+ }
+
+ // --------------------------------
+ // Create or Modify timer request
+
+ else if (requetedAction == taModify || requetedAction == taAdjust || requetedAction == taCreate)
+ {
+ cSchedule* s = 0;
+
+ if (!timer && (requetedAction == taModify || requetedAction == taAdjust))
+ {
+ tell(0, "Fatal: Timer (%d) not found, skipping modify request", timerid);
+
+ timerDb->getValue("INFO")->sPrintf("Fatal: Timer (%d) not found, skipping modify request", timerid);
+ timerDb->setCharValue("ACTION", taFailed);
+ timerDb->setCharValue("STATE", tsError);
+ timerDb->update();
+ updateTimerDone(timerid, doneid, tdsTimerCreateFailed);
+ continue;
+ }
+
+ // get schedule (channel) and optional the event
+
+ if (!(s = (cSchedule*)schedules->GetSchedule(channelId)))
+ {
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cChannelsLock channelsLock(false);
+ const cChannels* channels = channelsLock.Channels();
+#else
+ cChannels* channels = &Channels;
+#endif
+ const cChannel* channel = channels->GetByChannelID(channelId);
+
+ tell(0, "Error: Time (%d), missing channel '%s' (%s) or channel not found, ignoring request",
+ timerid, channel ? channel->Name() : "", timerDb->getStrValue("CHANNELID"));
+
+ timerDb->getValue("INFO")->sPrintf("Error: Timer, (%d), missing channel '%s' (%s) or channel not found, ignoring request",
+ timerid, channel ? channel->Name() : "", timerDb->getStrValue("CHANNELID"));
+ timerDb->setCharValue("ACTION", taFailed);
+ timerDb->setCharValue("STATE", tsError);
+ timerDb->update();
+ updateTimerDone(timerid, doneid, tdsTimerCreateFailed);
+
+ // mark this channel as 'unknown'
+
+ mapDb->clear();
+ mapDb->setValue("CHANNELID", channelId.ToString());
+ markUnknownChannel->execute();
+ markUnknownChannel->freeResult();
+ continue;
+ }
+
+ if (eventid > 0 && !(event = s->GetEvent(eventid)))
+ {
+ {
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cChannelsLock channelsLock(false);
+ const cChannels* channels = channelsLock.Channels();
+#else
+ cChannels* channels = &Channels;
+#endif
+ const cChannel* channel = channels->GetByChannelID(channelId);
+
+ tell(0, "Error: Timer (%d), missing event '%ld' on channel '%s' (%s), ignoring request",
+ timerid, eventid, channel->Name(), timerDb->getStrValue("CHANNELID"));
+
+ timerDb->getValue("INFO")->sPrintf("Error: Timer (%d), missing event '%ld' on channel '%s' (%s), ignoring request",
+ timerid, eventid, channel->Name(), timerDb->getStrValue("CHANNELID"));
+ timerDb->setCharValue("ACTION", taFailed);
+ timerDb->setCharValue("STATE", tsError);
+ timerDb->update();
+ updateTimerDone(timerid, doneid, tdsTimerCreateFailed);
+ }
+
+ // force reload of events
+
+ tell(0, "Info: Trigger EPG full-reload due to missing event!");
+ triggerEpgUpdate(yes);
+
+ continue;
+ }
+
+ if (insert)
+ {
+ tell(1, "DEBUG: Create of timer %d for event %ld", timerid, eventid);
+
+ // create timer ...
+
+ if (event)
+ {
+ // event should run at least more the 2 Minutes
+
+ if (getTimerByEvent(timers, event))
+ tell(0, "Warning: Timer for event (%d) '%s' already exist, creating additional timer due to request!",
+ event->EventID(), event->Title());
+
+ if (event->StartTime() + event->Duration() - (2 * tmeSecondsPerMinute) <= time(0))
+ {
+ tell(0, "Info: Event '%s' finished in the past, ignoring timer request!", event->Title());
+ timerDb->getValue("INFO")->sPrintf("Info: Event '%s' finished in the past, ignoring timer request!", event->Title());
+ timerDb->setCharValue("ACTION", taFailed);
+ timerDb->setCharValue("STATE", tsError);
+ timerDb->update();
+ updateTimerDone(timerid, doneid, tdsTimerCreateFailed);
+ continue;
+ }
+
+ timer = new cTimer(event);
+ }
+ else
+ {
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+ const cChannel* channel = channels->GetByChannelID(channelId);
+#else
+ cChannels* channels = &Channels;
+ cChannel* channel = channels->GetByChannelID(channelId);
+#endif
+ // timer without a event
+
+ timer = new cTimer(no, no, channel);
+ }
+
+ // reset error message in 'reason'
+
+ if (!timerDb->getValue("INFO")->isEmpty())
+ timerDb->setValue("INFO", "");
+
+ tell(1, "Create timer '%d'", timerid);
+
+ if (timerDb->hasValue("VDRUUID", "any"))
+ {
+ tell(0, "Took timer (%d) for uuid 'any', event (%ld)", timerid, eventid);
+
+ // mark 'na' record as assumed and create new with my UUID in primary key
+
+ timerDb->setCharValue("ACTION", taAssumed);
+ timerDb->setCharValue("STATE", tsIgnore);
+
+ timerDb->update();
+
+ // I take the timer -> new record will created!
+
+ timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+ timerDb->insert();
+
+ // update timerid !!!
+
+ timerid = timerDb->getLastInsertId();
+ timerDb->setValue("ID", timerid);
+ }
+ }
+ else
+ {
+ // modify timer ...
+
+ if (timerDb->hasValue("VDRUUID", "any"))
+ {
+ tell(0, "Error: Ignoring modify request of timer (%d) without VDRUUID", timerid);
+ timerDb->getValue("INFO")->sPrintf("Error: Ignoring modify request of timer (%d) without VDRUUID", timerid);
+ timerDb->setCharValue("ACTION", taFailed);
+ timerDb->setCharValue("STATE", tsError);
+ timerDb->update();
+ updateTimerDone(timerid, doneid, tdsTimerCreateFailed);
+ continue;
+ }
+
+ tell(1, "Modify timer (%d), set event to (%d)", timerid, event->EventID());
+
+// #TODO ?!?!
+// if (event && timer->Event() != event)
+// timer->SetEvent(event);
+ }
+
+ // update the timer with data from timers table ...
+
+ updateTimerObjectFromRow(timer, timerDb->getRow(), event);
+
+ // add / store ...
+
+ if (insert)
+ {
+ timers->Add(timer);
+ updateTimerDone(timerid, doneid, tdsTimerCreated);
+ createCount++;
+ }
+ else
+ {
+ if (requetedAction == taAdjust && event)
+ {
+ // adjust time to given event ..
+
+ cTimer* dummyTimer = new cTimer(event);
+ timer->SetStart(dummyTimer->Start());
+ timer->SetStop(dummyTimer->Stop());
+ timer->SetDay(dummyTimer->Day());
+ delete dummyTimer;
+ }
+
+ modifyCount++;
+ }
+
+ timerDb->setValue("AUX", timer->Aux());
+ timerDb->setCharValue("ACTION", taAssumed);
+ timerDb->setCharValue("STATE", tsPending);
+ timerDb->store();
+ }
+ }
+
+ selectPendingTimerActions->freeResult();
+
+ // schedules lock freigeben
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ schedulesKey.Remove();
+#else
+ delete schedulesLock;
+#endif
+
+ if (createCount || modifyCount || deleteCount)
+ timers->SetModified();
+
+ timerJobsUpdateTriggered = no;
+
+ tell(0, "Timer requests done, created %d, modified %d, deleted %d in %s",
+ createCount, modifyCount, deleteCount, ms2Dur(cTimeMs::Now()-start).c_str());
+
+ return success;
+}
+
+//***************************************************************************
+// Timer Done to Failed
+//***************************************************************************
+
+int cUpdate::updateTimerDone(int timerid, int doneid, char state)
+{
+ if (doneid == na)
+ return done;
+
+ timerDoneDb->clear();
+ timerDoneDb->setValue("ID", doneid);
+
+ if (timerDoneDb->find())
+ {
+ // don't toggle 'D'eleted by user to re'J'ected
+
+ if (timerDoneDb->hasCharValue("STATE", tdsTimerDeleted) && state == tdsTimerRejected)
+ return done;
+
+ timerDoneDb->setValue("TIMERID", timerid);
+ timerDoneDb->setCharValue("STATE", state);
+ timerDoneDb->update();
+ }
+ else
+ {
+ tell(0, "Warning: Id (%d) in timersdone for timer (%d) not found.",
+ doneid, timerid);
+ }
+
+ return done;
+}
+
+//***************************************************************************
+// Update Timer Table
+//***************************************************************************
+
+int cUpdate::updateTimerTable()
+{
+ cMutexLock lock(&timerMutex);
+ int timerMod = 0;
+ int cnt = 0;
+
+ tell(1, "Updating table timers (and remove deleted and finished timers older than 2 days)");
+
+ timerDb->deleteWhere("%s < unix_timestamp() - %d and %s in ('D','F')",
+ timerDb->getField("UPDSP")->getDbName(),
+ 2 * tmeSecondsPerDay,
+ timerDb->getField("STATE")->getDbName());
+
+ connection->startTransaction();
+
+ // --------------------------
+ // get timers lock
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cTimersLock timersLock(true);
+ cTimers* timers = timersLock.Timers();
+#else
+ cTimers* timers = &Timers;
+#endif
+
+ // --------------------------
+ // remove deleted timers
+
+ timerDb->clear();
+ timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+
+ for (int f = selectMyTimer->find(); f && dbConnected(); f = selectMyTimer->fetch())
+ {
+ int exist = no;
+
+ // delete only assumed timers
+
+ if (!timerDb->getValue("ACTION")->isEmpty() && !timerDb->hasCharValue("ACTION", taAssumed))
+ continue;
+
+ // count my timers to detect truncated (epmty) table
+ // on empty table ignore known timer ids
+
+ cnt++;
+
+ for (const cTimer* t = timers->First(); t; t = timers->Next(t))
+ {
+ int timerid = getTimerIdOf(t);
+
+ // compare by timerid
+
+ if (timerid != na && timerDb->getIntValue("ID") == timerid)
+ {
+ exist = yes;
+ break;
+ }
+
+ // compare by start-time and channelid
+
+ if (t->StartTime() == timerDb->getIntValue("StartTime") &&
+ strcmp(t->Channel()->GetChannelID().ToString(), timerDb->getStrValue("ChannelId")) == 0)
+ {
+ exist = yes;
+ break;
+ }
+ }
+
+ if (!exist)
+ {
+ int doneid = timerDb->getIntValue("DONEID");
+
+ if (!timerDb->hasCharValue("STATE", tsFinished))
+ {
+ timerDb->setCharValue("STATE", tsDeleted);
+ timerDb->update();
+ }
+
+ if (doneid > 0)
+ {
+ timerDoneDb->clear();
+ timerDoneDb->setValue("ID", doneid);
+
+ if (timerDoneDb->find() &&
+ (timerDoneDb->hasCharValue("STATE", tdsTimerCreated) || timerDoneDb->hasCharValue("STATE", tdsTimerRequested)))
+ {
+ timerDoneDb->setCharValue("STATE", tdsTimerDeleted);
+ timerDoneDb->update();
+ }
+ }
+ }
+ }
+
+ selectMyTimer->freeResult();
+
+ if (!cnt && timers->Count())
+ tell(0, "No timer of my uuid found, assuming cleared table and ignoring the known timerids");
+
+ // --------------------------
+ // update timers
+
+ for (cTimer* t = timers->First(); t && dbConnected(); t = timers->Next(t))
+ {
+ int insert = yes;
+ int timerId = getTimerIdOf(t);
+
+ // no timer id or not in table -> handle as insert!
+
+ timerDb->clear();
+
+ if (timerId != na && cnt)
+ {
+ timerDb->setValue("ID", timerId);
+ timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+
+ insert = !timerDb->find();
+ timerDb->clearChanged();
+ }
+
+ updateRowByTimer(timerDb->getRow(), t);
+
+ if (insert)
+ {
+ timerDb->setCharValue("STATE", tsPending);
+ timerDb->insert();
+
+ // get timerid of auto increment field and store to timers 'aux' data
+
+ timerId = timerDb->getLastInsertId();
+ setTimerId(t, timerId);
+ timerMod++;
+ }
+ else if (!timerDb->getValue("STATE")->isEmpty() && strchr("DE", timerDb->getStrValue("STATE")[0]))
+ {
+ timerDb->setValue("STATE", "P"); // timer pending
+ }
+
+ if (insert || timerDb->getChanges())
+ {
+ timerDb->setValue("ID", timerId); // set ID for update!!
+ timerDb->update(); // at least for aux (on insert case)
+
+ tell(1, "'%s' timer for event %u '%s' at database",
+ insert ? "Insert" : "Update",
+ t->Event() ? t->Event()->EventID() : na,
+ t->Event() ? t->Event()->Title() : t->File());
+ }
+ else
+ {
+ tell(3, "Nothing changed ... skipping db update");
+ }
+
+ timerDb->reset();
+ }
+
+ connection->commit();
+ timerTableUpdateTriggered = no;
+
+ if (timerMod)
+ timers->SetModified();
+
+ tell(1, "Updating table timers done");
+
+ return success;
+}
+
+//***************************************************************************
+// Recording Changed
+//***************************************************************************
+
+int cUpdate::recordingChanged()
+{
+ cMutexLock lock(&runningRecMutex);
+
+ cRunningRecording* rr = runningRecordings.First();
+
+ tell(3, "recording changed; rr = %p", (void*)rr);
+
+ while (rr && dbConnected())
+ {
+ int timerWasDeleted = no;
+
+ // first: update timer state ..
+
+ if (!isEmpty(rr->aux))
+ {
+ int timerid = getTimerIdOf(rr->aux);
+ timerDb->clear();
+ timerDb->setValue("ID", timerid);
+ timerDb->setValue("VDRUUID", Epg2VdrConfig.uuid);
+
+ tell(1, "Try to lookup timer with id %d", timerid);
+
+ if (timerDb->find())
+ {
+ tell(1, "Found timer %ld", timerDb->getIntValue("ID"));
+
+ if (timerDb->hasCharValue("STATE", tsDeleted))
+ timerWasDeleted = yes;
+ else
+ timerDb->setCharValue("STATE", rr->failed ? tsError : rr->finished ? tsFinished : tsRunning);
+
+ timerDb->setValue("INFO", rr->info);
+ timerDb->store();
+ }
+ else
+ tell(0, "Error: Can't lookup timer, id %d not found!", timerid);
+
+ timerDb->reset();
+ }
+
+ // second: update done entry and remove from 'running' if 'finished' ..
+
+ if (rr->finished)
+ {
+ if (rr->doneid != na)
+ {
+ timerDoneDb->clear();
+ timerDoneDb->setValue("ID", rr->doneid);
+
+ if (timerDoneDb->find())
+ {
+ if (timerWasDeleted)
+ timerDoneDb->setCharValue("STATE", tdsTimerDeleted); // -> Recording deleted
+ else if (rr->failed)
+ timerDoneDb->setCharValue("STATE", tdsRecordingFailed); // -> Recording failed
+ else
+ timerDoneDb->setCharValue("STATE", tdsRecordingDone); // -> Recording done
+
+ tell(1, "Update 'done state' for doneid %ld to %s", rr->doneid, timerDoneDb->getStrValue("STATE"));
+
+ timerDoneDb->update();
+ }
+ else
+ {
+ tell(0, "Error: Can't update timersdone state, doneid %ld not found!", rr->doneid);
+ }
+ }
+
+ cRunningRecording* rrNext = runningRecordings.Next(rr);
+ runningRecordings.Del(rr);
+ rr = rrNext;
+
+ continue;
+ }
+
+ rr = runningRecordings.Next(rr);
+ }
+
+ return done;
+}
diff --git a/ttools.c b/ttools.c
new file mode 100644
index 0000000..c30f960
--- /dev/null
+++ b/ttools.c
@@ -0,0 +1,583 @@
+/*
+ * ttools.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <string>
+
+#include "update.h"
+#include "ttools.h"
+
+using namespace std;
+
+//***************************************************************************
+// Content Of Tag
+//***************************************************************************
+
+int contentOfTag(const char* tag, const char* xml, char* buf, int size)
+{
+ string sTag = "<" + string(tag) + ">";
+ string eTag = "</" + string(tag) + ">";
+
+ const char* s;
+ const char* e;
+
+ if (buf)
+ *buf = 0;
+
+ if ((s = strstr(xml, sTag.c_str())) && (e = strstr(xml, eTag.c_str())))
+ {
+ s += strlen(sTag.c_str());
+
+ if (buf)
+ sprintf(buf, "%.*s", (int)(e-s), s);
+
+ return success;
+ }
+
+ return fail;
+}
+
+//***************************************************************************
+// Content Value Of Tag
+//***************************************************************************
+
+int contentOfTag(const cTimer* timer, const char* tag, char* buf, int size)
+{
+ char epgaux[512+TB];
+
+ if (!timer || isEmpty(timer->Aux()))
+ return fail;
+
+ if (contentOfTag("epgd", timer->Aux(), epgaux, 512) != success)
+ return fail;
+
+ if (contentOfTag(tag, epgaux, buf, size) != success)
+ return fail;
+
+ return success;
+}
+
+int contentOfTag(const cTimer* timer, const char* tag, int& value)
+{
+ char epgaux[512+TB];
+ char buf[100+TB];
+
+ if (!timer || isEmpty(timer->Aux()))
+ return fail;
+
+ if (contentOfTag("epgd", timer->Aux(), epgaux, 512) != success)
+ return fail;
+
+ if (contentOfTag(tag, epgaux, buf, 100) != success)
+ return fail;
+
+ value = atoi(buf);
+
+ return success;
+}
+
+//***************************************************************************
+// Get Timer Id Of
+//***************************************************************************
+
+int getTimerIdOf(const cTimer* timer)
+{
+ char tid[100+TB];
+
+ if (!timer || isEmpty(timer->Aux()))
+ return na;
+
+ if (contentOfTag(timer, "timerid", tid, 100) != success)
+ return na;
+
+ return atoi(tid);
+}
+
+int getTimerIdOf(const char* aux)
+{
+ char tid[100+TB];
+ char epgaux[512+TB];
+
+ if (isEmpty(aux))
+ return na;
+
+ if (contentOfTag("epgd", aux, epgaux, 512) != success)
+ return fail;
+
+ if (contentOfTag("timerid", epgaux, tid, 100) != success)
+ return fail;
+
+ return atoi(tid);
+}
+
+//***************************************************************************
+// Remove Tag
+//***************************************************************************
+
+void removeTag(char* xml, const char* tag)
+{
+ string sTag = "<" + string(tag) + ">";
+ string eTag = "</" + string(tag) + ">";
+
+ const char* s;
+ const char* e;
+
+ if ((s = strstr(xml, sTag.c_str())) && (e = strstr(xml, eTag.c_str())))
+ {
+ char tmp[10000+TB];
+
+ e += strlen(eTag.c_str());
+
+ // sicher ist sicher ;)
+
+ if (e <= s)
+ return;
+
+ sprintf(tmp, "%.*s%s", int(s-xml), xml, e);
+
+ strcpy(xml, tmp);
+ }
+}
+
+//***************************************************************************
+// Insert Tag
+//***************************************************************************
+
+int insertTag(char* xml, const char* parent, const char* tag, int value)
+{
+ char* tmp;
+ string sTag = "<" + string(parent) + ">";
+ const char* s;
+
+ if ((s = strstr(xml, sTag.c_str())))
+ {
+ s += strlen(sTag.c_str());
+ asprintf(&tmp, "%.*s<%s>%d</%s>%s", int(s-xml), xml, tag, value, tag, s);
+ }
+ else
+ {
+ asprintf(&tmp, "%s<%s><%s>%d</%s></%s>", xml, parent, tag, value, tag, parent);
+ }
+
+ strcpy(xml, tmp);
+ free(tmp);
+
+ return success;
+}
+
+int insertTag(char* xml, const char* parent, const char* tag, const char* value)
+{
+ char* tmp;
+ string sTag = "<" + string(parent) + ">";
+ const char* s;
+
+ if ((s = strstr(xml, sTag.c_str())))
+ {
+ s += strlen(sTag.c_str());
+ asprintf(&tmp, "%.*s<%s>%s</%s>%s", int(s-xml), xml, tag, value, tag, s);
+ }
+ else
+ {
+ asprintf(&tmp, "%s<%s><%s>%s</%s></%s>", xml, parent, tag, value, tag, parent);
+ }
+
+ strcpy(xml, tmp);
+ free(tmp);
+
+ return success;
+}
+
+//***************************************************************************
+// Set Tag To
+//***************************************************************************
+
+int setTagTo(cTimer* timer, const char* tag, int value)
+{
+ char aux[10000+TB] = "";
+
+ if (!isEmpty(timer->Aux()))
+ strcpy(aux, timer->Aux());
+
+ removeTag(aux, tag);
+ insertTag(aux, "epgd", tag, value);
+
+ timer->SetAux(aux);
+
+ return done;
+}
+
+int setTagTo(cTimer* timer, const char* tag, const char* value)
+{
+ char aux[10000+TB] = "";
+
+ if (!isEmpty(timer->Aux()))
+ strcpy(aux, timer->Aux());
+
+ removeTag(aux, tag);
+ insertTag(aux, "epgd", tag, value);
+
+ timer->SetAux(aux);
+
+ return done;
+}
+
+//***************************************************************************
+// Set Timer Id
+//***************************************************************************
+
+int setTimerId(cTimer* timer, int tid)
+{
+ char aux[10000+TB] = "";
+
+ if (!isEmpty(timer->Aux()))
+ strcpy(aux, timer->Aux());
+
+ // remove old timerid - if exist
+
+ removeTag(aux, "timerid");
+ insertTag(aux, "epgd", "timerid", tid);
+
+ timer->SetAux(aux);
+
+ return done;
+}
+
+//***************************************************************************
+// Get Timer By Id
+//***************************************************************************
+
+cTimer* getTimerById(cTimers* timers, int timerid)
+{
+ for (cTimer* t = timers->First(); t; t = timers->Next(t))
+ if (timerid == getTimerIdOf(t))
+ return t;
+
+ return 0;
+}
+
+//***************************************************************************
+// Get Timer By Event
+//***************************************************************************
+
+cTimer* getTimerByEvent(cTimers* timers, const cEvent* event)
+{
+ cTimer* timer = 0;
+
+ eTimerMatch tm = tmNone;
+
+ timer = timers->GetMatch(event, &tm);
+
+ if (tm != tmFull)
+ timer = 0;
+
+ return timer;
+}
+
+//***************************************************************************
+// New Row From Event
+//***************************************************************************
+
+cDbRow* newTimerRowFromEvent(const cEvent* event)
+{
+ cTimer* timer = new cTimer(event);
+ cDbRow* timerRow = newRowFromTimer(timer);
+
+ delete timer;
+
+ return timerRow;
+}
+
+//***************************************************************************
+// New Row From Timer
+//***************************************************************************
+
+cDbRow* newRowFromTimer(const cTimer* timer)
+{
+ cDbRow* timerRow = new cDbRow("timers");
+
+ updateRowByTimer(timerRow, timer);
+
+ return timerRow;
+}
+
+//***************************************************************************
+// Update Row By Timer
+//***************************************************************************
+
+int updateRowByTimer(cDbRow* timerRow, const cTimer* t)
+{
+ int autotimerid = na;
+ int autotimerinssp = na;
+ int doneid = na;
+ int namingmode = na;
+ char tmplExpression[100+TB] = "";
+ int childLock = no;
+ int epgs = no;
+ char directory[512+TB] = "";
+ char source[40+TB] = "";
+ cString channelId = t->Event() ? t->Event()->ChannelID().ToString() : t->Channel()->GetChannelID().ToString();
+
+ contentOfTag(t, "autotimerid", autotimerid);
+ contentOfTag(t, "autotimerinssp", autotimerinssp);
+ contentOfTag(t, "doneid", doneid);
+ contentOfTag(t, "namingmode", namingmode);
+ contentOfTag(t, "template", tmplExpression, 100);
+ contentOfTag(t, "directory", directory, 512);
+ contentOfTag(t, "source", source, 40);
+
+ timerRow->setValue("VDRUUID", Epg2VdrConfig.uuid);
+ timerRow->setValue("EVENTID", t->Event() ? (long)t->Event()->EventID() : 0);
+ timerRow->setValue("_STARTTIME", t->Event() ? t->Event()->StartTime() : 0);
+ timerRow->setValue("CHANNELID", channelId);
+ timerRow->setValue("DAY", t->Day());
+ timerRow->setValue("STARTTIME", t->Start());
+ timerRow->setValue("ENDTIME", t->Stop());
+ timerRow->setValue("WEEKDAYS", t->WeekDays());
+ timerRow->setValue("PRIORITY", t->Priority());
+ timerRow->setValue("LIFETIME", t->Lifetime());
+ timerRow->setValue("VPS", t->HasFlags(tfVps) ? yes : no);
+ timerRow->setValue("ACTIVE", t->HasFlags(tfActive) ? yes : no);
+
+ timerRow->setValue("DIRECTORY", directory);
+
+ if (!isEmpty(directory) && strncmp(t->File(), directory, strlen(directory)) == 0)
+ {
+ int len = strlen(directory);
+
+ if (t->File()[len] == '~')
+ len++;
+
+ timerRow->setValue("FILE", t->File()+len);
+ }
+ else
+ timerRow->setValue("FILE", t->File());
+
+ if (namingmode != na)
+ timerRow->setValue("NAMINGMODE", namingmode);
+
+ if (!isEmpty(tmplExpression))
+ timerRow->setValue("TEMPLATE", tmplExpression);
+
+ if (autotimerinssp != na)
+ timerRow->setValue("AUTOTIMERINSSP", autotimerinssp);
+
+ if (autotimerid != na)
+ timerRow->setValue("AUTOTIMERID", autotimerid);
+
+ if (doneid != na)
+ timerRow->setValue("DONEID", doneid);
+
+ // AUX: "<epgsearch><channel>62 - TNTSerieHD</channel><searchtimer>Falling Skies</searchtimer><start>1411498680</start><stop>1411502100</stop><s-id>3</s-id><eventid>565129</eventid></epgsearch>
+ // AUX: <epgd>......</epgd><pin-plugin><protected>yes</protected></pin-plugin>"
+
+ if (t->Aux())
+ {
+ epgs = strstr(t->Aux(), "<epgsearch>") != 0;
+ childLock = strstr(t->Aux(), "<pin-plugin><protected>yes</protected></pin-plugin>") != 0;
+ }
+
+ timerRow->setValue("SOURCE", !isEmpty(source) ? source : epgs ? "epgs" : Epg2VdrConfig.uuid);
+ timerRow->setValue("CHILDLOCK", childLock);
+ timerRow->setValue("AUX", t->Aux()); // update aux also in table
+
+ return done;
+}
+
+//***************************************************************************
+// New Timer From Row
+//***************************************************************************
+
+cEpgTimer* newTimerObjectFromRow(cDbRow* timerRow, cDbRow* vdrRow)
+{
+ cEpgTimer* timer = 0;
+ const cEvent* event = 0;
+ cString buf;
+ tChannelID channelId = tChannelID::FromString(timerRow->getStrValue("CHANNELID"));
+ uint flags = tfNone;
+ char* file;
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+ const cChannel* channel = channels->GetByChannelID(channelId);
+#else
+ cChannels* channels = &Channels;
+ const cChannel* channel = channels->GetByChannelID(channelId);
+#endif
+
+ const char* dir = timerRow->getStrValue("DIRECTORY");
+
+ file = strdup(timerRow->getStrValue("FILE"));
+ strReplace(file, ':', '|');
+
+ if (timerRow->getIntValue("VPS"))
+ flags |= tfVps;
+
+ if (timerRow->getIntValue("ACTIVE"))
+ flags |= tfActive;
+
+ if (timerRow->hasCharValue("STATE", tsRunning))
+ flags |= tfRecording;
+
+ timer = new cEpgTimer(no, no, channel);
+
+ buf = cString::sprintf("%d:%d:%s:%04d:%04d:%d:%d:%s%s%s:%s",
+ flags,
+ channel ? channel->Number() : 0,
+ (const char*)cTimer::PrintDay(timerRow->getIntValue("DAY"),
+ timerRow->getIntValue("WEEKDAYS"), yes),
+ (int)timerRow->getIntValue("STARTTIME"),
+ (int)timerRow->getIntValue("ENDTIME"),
+ (int)timerRow->getIntValue("PRIORITY"),
+ (int)timerRow->getIntValue("LIFETIME"),
+ !isEmpty(dir) ? dir : "",
+ !isEmpty(dir) && !isEmpty(file) ? "~" : "",
+ file,
+ timerRow->getStrValue("AUX"));
+
+ free(file);
+ timer->Parse(buf);
+
+ if (channel)
+ {
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cStateKey schedulesKey;
+ const cSchedules* schedules = cSchedules::GetSchedulesRead(schedulesKey);
+#else
+ cSchedulesLock* schedulesLock = new cSchedulesLock(false);
+ const cSchedules* schedules = (cSchedules*)cSchedules::Schedules(*schedulesLock);
+#endif
+ if (schedules)
+ {
+ const cSchedule* schedule = schedules->GetSchedule(channel);
+
+ if (schedule)
+ event = schedule->GetEvent(timerRow->getIntValue("EVENTID"));
+ }
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ if (schedules) schedulesKey.Remove();
+#else
+ delete schedulesLock;
+#endif
+ }
+
+ if (event)
+ timer->SetEvent(event);
+
+ timer->setTimerId(timerRow->getIntValue("ID"));
+ timer->setEventId(timerRow->getIntValue("EVENTID"));
+ timer->setAction(!timerRow->getValue("ACTION")->isNull() ? timerRow->getStrValue("ACTION")[0] : ' ');
+ timer->setVdr(vdrRow->getStrValue("NAME"), vdrRow->getStrValue("UUID"), vdrRow->hasValue("STATE", "attached"));
+ timer->setState(!timerRow->getValue("STATE")->isNull() ? timerRow->getStrValue("STATE")[0] : ' ', timerRow->getStrValue("INFO"));
+
+#ifdef WITH_PIN
+ timer->SetFskProtection(timerRow->getIntValue("CHILDLOCK"));
+#endif
+
+ return timer;
+}
+
+//***************************************************************************
+// update Timer Object From Row
+//***************************************************************************
+
+int updateTimerObjectFromRow(cTimer* timer, cDbRow* timerRow, const cEvent* event)
+{
+ if (!timerRow->getValue("FILE")->isEmpty())
+ {
+ string path = "";
+
+ if (!timerRow->getValue("DIRECTORY")->isEmpty())
+ path = timerRow->getStrValue("DIRECTORY") + string("~");
+
+ if (!timerRow->getValue("FILE")->isEmpty())
+ path += timerRow->getStrValue("FILE");
+
+ timer->SetFile(path.c_str());
+ }
+ else if (!timerRow->getValue("DIRECTORY")->isEmpty())
+ {
+ string path = timerRow->getStrValue("DIRECTORY") + string("~") + string(timer->File());
+ timer->SetFile(path.c_str());
+ }
+ else if (!event)
+ {
+#if APIVERSNUM >= 20301
+ LOCK_CHANNELS_READ;
+ const cChannels* channels = Channels;
+#else
+ cChannels* channels = &Channels;
+#endif
+
+ tChannelID channelId = tChannelID::FromString(timerRow->getStrValue("CHANNELID"));
+ const cChannel* channel = channels->GetByChannelID(channelId);
+
+ timer->SetFile(channel->Name());
+ tell(0, "Missing file, using channel name instead '%s'", channel->Name());
+ }
+
+ if (!timerRow->getValue("DAY")->isNull())
+ timer->SetDay(timerRow->getIntValue("DAY"));
+
+ if (!timerRow->getValue("WEEKDAYS")->isNull())
+ timer->SetWeekDays(timerRow->getIntValue("WEEKDAYS"));
+
+ if (!timerRow->getValue("STARTTIME")->isNull())
+ timer->SetStart(timerRow->getIntValue("STARTTIME"));
+
+ if (!timerRow->getValue("ENDTIME")->isNull())
+ timer->SetStop(timerRow->getIntValue("ENDTIME"));
+
+ if (!timerRow->getValue("PRIORITY")->isNull())
+ timer->SetPriority(timerRow->getIntValue("PRIORITY"));
+
+ if (!timerRow->getValue("LIFETIME")->isNull())
+ timer->SetLifetime(timerRow->getIntValue("LIFETIME"));
+
+ if (!timerRow->getValue("VPS")->isNull())
+ {
+ if (timerRow->getIntValue("VPS"))
+ timer->SetFlags(tfVps);
+ else
+ timer->ClrFlags(tfVps);
+ }
+
+ if (timerRow->getIntValue("ACTIVE"))
+ timer->SetFlags(tfActive);
+ else
+ timer->ClrFlags(tfActive);
+
+ setTagTo(timer, "timerid", timerRow->getIntValue("ID"));
+
+ if (!timerRow->getValue("SOURCE")->isNull())
+ setTagTo(timer, "source", timerRow->getStrValue("SOURCE"));
+
+ if (!timerRow->getValue("DIRECTORY")->isEmpty())
+ setTagTo(timer, "directory", timerRow->getStrValue("DIRECTORY"));
+
+ if (!timerRow->getValue("NAMINGMODE")->isNull())
+ setTagTo(timer, "namingmode", timerRow->getIntValue("NAMINGMODE"));
+
+ if (!timerRow->getValue("TEMPLATE")->isNull())
+ setTagTo(timer, "template", timerRow->getStrValue("TEMPLATE"));
+
+ if (!timerRow->getValue("AUTOTIMERID")->isNull())
+ setTagTo(timer, "autotimerid", timerRow->getIntValue("AUTOTIMERID"));
+
+ if (!timerRow->getValue("DONEID")->isNull())
+ setTagTo(timer, "doneid", timerRow->getIntValue("DONEID"));
+
+ if (!timerRow->getValue("AUTOTIMERINSSP")->isNull())
+ setTagTo(timer, "autotimerinssp", timerRow->getStrValue("AUTOTIMERINSSP"));
+
+ if (!timerRow->getValue("EXPRESSION")->isNull())
+ setTagTo(timer, "expression", timerRow->getStrValue("EXPRESSION"));
+
+ timer->Matches(); // adjust times of timer
+
+ return done;
+}
diff --git a/ttools.h b/ttools.h
new file mode 100644
index 0000000..07f9382
--- /dev/null
+++ b/ttools.h
@@ -0,0 +1,42 @@
+/*
+ * ttools.h: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef _TTOOLS_H_
+#define _TTOOLS_H_
+
+#include <vdr/timers.h>
+
+#include "service.h"
+
+//***************************************************************************
+// Timer Tools
+//***************************************************************************
+
+int contentOfTag(const char* tag, const char* xml, char* buf, int size);
+int contentOfTag(const cTimer* timer, const char* tag, char* buf, int size);
+int contentOfTag(const cTimer* timer, const char* tag, int& value);
+int getTimerIdOf(const cTimer* timer);
+int getTimerIdOf(const char* aux);
+void removeTag(char* xml, const char* tag);
+int insertTag(char* xml, const char* parent, const char* tag, int value);
+int insertTag(char* xml, const char* parent, const char* tag, const char* value);
+int setTagTo(cTimer* timer, const char* tag, int value);
+int setTagTo(cTimer* timer, const char* tag, const char* value);
+int setTimerId(cTimer* timer, int tid);
+cTimer* getTimerById(cTimers* timers, int timerid);
+cTimer* getTimerByEvent(cTimers* timers, const cEvent* event);
+
+cDbRow* newTimerRowFromEvent(const cEvent* event);
+cDbRow* newRowFromTimer(const cTimer* t);
+int updateRowByTimer(cDbRow* timerDb, const cTimer* t);
+
+cEpgTimer* newTimerObjectFromRow(cDbRow* timerRow, cDbRow* vdrRow);
+int updateTimerObjectFromRow(cTimer* timer, cDbRow* timerRow, const cEvent* event);
+
+//***************************************************************************
+
+#endif // _TTOOLS_H_
diff --git a/update.c b/update.c
new file mode 100644
index 0000000..e789c8d
--- /dev/null
+++ b/update.c
@@ -0,0 +1,1822 @@
+/*
+ * update.c: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#include <locale.h>
+
+#include <vdr/videodir.h>
+#include <vdr/tools.h>
+
+#include "epg2vdr.h"
+#include "update.h"
+#include "handler.h"
+
+//***************************************************************************
+// ctor
+//***************************************************************************
+
+cUpdate::cUpdate(cPluginEPG2VDR* aPlugin)
+ : cThread("epg2vdr-update")
+{
+ // thread / update control
+
+ plugin = aPlugin;
+ connection = 0;
+ loopActive = no;
+ timerJobsUpdateTriggered = yes;
+ timerTableUpdateTriggered = yes;
+ recordingStateChangedTrigger = yes;
+ updateRecFolderOptionTrigger = no;
+
+ storeAllRecordingInfoFilesTrigger = no;
+ recordingFullReloadTrigger = no;
+ manualTrigger = no;
+ videoBasePath = 0;
+ dbReconnectTriggered = no;
+
+ fullreload = no;
+ epgdBusy = yes;
+ epgdState = cEpgdState::esUnknown;
+ mainActPending = no;
+ eventsPending = no;
+ nextEpgdUpdateAt = 0;
+
+ lastUpdateAt = 0;
+ lastEventsUpdateAt = 0;
+ lastRecordingCount = 0;
+ lastRecordingDeleteAt = 0;
+ timersTableMaxUpdsp = 0;
+
+ //
+
+ compDb = 0;
+ eventsDb = 0;
+ useeventsDb = 0;
+ fileDb = 0;
+ imageDb = 0;
+ imageRefDb = 0;
+ episodeDb = 0;
+ vdrDb = 0;
+ mapDb = 0;
+ timerDb = 0;
+ timerDoneDb = 0;
+ recordingDirDb = 0;
+ recordingListDb = 0;
+
+ selectMasterVdr = 0;
+ selectAllImages = 0;
+ selectUpdEvents = 0;
+ selectEventById = 0;
+ selectAllChannels = 0;
+ selectChannelById = 0;
+ markUnknownChannel = 0;
+ selectComponentsOf = 0;
+ deleteTimer = 0;
+ selectMyTimer = 0;
+ selectRecordings = 0;
+ selectRecForInfoUpdate = 0;
+ selectTimerByEvent = 0;
+ selectTimerById = 0;
+ selectTimerByDoneId = 0;
+ selectMaxUpdSp = 0;
+ selectPendingTimerActions = 0;
+
+ dvbDescription = 0;
+
+ //
+
+ epgimagedir = 0;
+ withutf8 = no;
+ handlerMaster = no;
+
+ // check/create uuid
+
+ if (isEmpty(Epg2VdrConfig.uuid))
+ {
+ sstrcpy(Epg2VdrConfig.uuid, getUniqueId(), sizeof(Epg2VdrConfig.uuid));
+ plugin->SetupStore("Uuid", Epg2VdrConfig.uuid);
+ Setup.Save();
+
+ tell(0, "Initially created uuid '%s'", Epg2VdrConfig.uuid);
+ }
+}
+
+//***************************************************************************
+// dtor
+//***************************************************************************
+
+cUpdate::~cUpdate()
+{
+ if (loopActive)
+ Stop();
+
+ free(epgimagedir);
+}
+
+//***************************************************************************
+// Init
+//***************************************************************************
+
+int cUpdate::init()
+{
+ char* dictPath = 0;
+ char* pdir;
+ char* lang;
+
+ lang = setlocale(LC_CTYPE, 0);
+
+ if (lang)
+ {
+ tell(0, "Set locale to '%s'", lang);
+
+ if ((strcasestr(lang, "UTF-8") != 0) || (strcasestr(lang, "UTF8") != 0))
+ {
+ tell(0, "detected UTF-8");
+ withutf8 = yes;
+ }
+ }
+ else
+ {
+ tell(0, "Reseting locale for LC_CTYPE failed.");
+ }
+
+ strcpy(imageExtension, "jpg");
+ asprintf(&epgimagedir, "%s/epgimages", EPG2VDR_DATA_DIR);
+ asprintf(&pdir, "%s/images", epgimagedir);
+
+ if (!(DirectoryOk(pdir) || MakeDirs(pdir, true)))
+ tell(0, "could not access or create Directory %s", pdir);
+
+ free(pdir);
+
+ // initialize the dictionary
+
+ asprintf(&dictPath, "%s/epg.dat", cPlugin::ConfigDirectory("epg2vdr/"));
+
+ if (dbDict.in(dictPath) != success)
+ {
+ tell(0, "Fatal: Dictionary not loaded, aborting!");
+ return fail;
+ }
+
+ tell(0, "Dictionary '%s' loaded", dictPath);
+ free(dictPath);
+
+ // init database ...
+
+ cDbConnection::setEncoding(withutf8 ? "utf8" : "latin1"); // mysql uses latin1 for ISO8851-1
+ cDbConnection::setHost(Epg2VdrConfig.dbHost);
+ cDbConnection::setPort(Epg2VdrConfig.dbPort);
+ cDbConnection::setName(Epg2VdrConfig.dbName);
+ cDbConnection::setUser(Epg2VdrConfig.dbUser);
+ cDbConnection::setPass(Epg2VdrConfig.dbPass);
+ cDbConnection::setConfPath(cPlugin::ConfigDirectory("epg2vdr/"));
+
+ videoBasePath = cVideoDirectory::Name();
+
+ return success;
+}
+
+//***************************************************************************
+// Exit
+//***************************************************************************
+
+int cUpdate::exit()
+{
+ exitDb();
+
+ return success;
+}
+
+cDbFieldDef imageSizeDef("image", "image", cDBS::ffUInt, 0, cDBS::ftData);
+
+//***************************************************************************
+// Init/Exit Database Connections
+//***************************************************************************
+
+int cUpdate::initDb()
+{
+ int status = success;
+
+ if (!connection)
+ connection = new cDbConnection();
+
+ vdrDb = new cDbTable(connection, "vdrs");
+ if (vdrDb->open() != success) return fail;
+
+ // DB-API check
+
+ vdrDb->clear();
+ vdrDb->setValue("UUID", EPGDNAME);
+
+ if (!vdrDb->find())
+ {
+ tell(0, "Can't lookup epgd information, start epgd to create the tables first! Aborting now.");
+ return fail;
+ }
+
+ vdrDb->reset();
+
+ if (vdrDb->getIntValue("DBAPI") != DB_API)
+ {
+ tell(0, "Found dbapi %d, expected %d, please alter the tables first! Aborting now.",
+ (int)vdrDb->getIntValue("DBAPI"), DB_API);
+ return fail;
+ }
+
+ // open tables ..
+
+ mapDb = new cDbTable(connection, "channelmap");
+ if (mapDb->open() != success) return fail;
+
+ fileDb = new cDbTable(connection, "fileref");
+ if (fileDb->open() != success) return fail;
+
+ imageDb = new cDbTable(connection, "images");
+ if (imageDb->open() != success) return fail;
+
+ imageRefDb = new cDbTable(connection, "imagerefs");
+ if (imageRefDb->open() != success) return fail;
+
+ episodeDb = new cDbTable(connection, "episodes");
+ if (episodeDb->open() != success) return fail;
+
+ eventsDb = new cDbTable(connection, "events");
+ if (eventsDb->open() != success) return fail;
+
+ useeventsDb = new cDbTable(connection, "useevents");
+ if (useeventsDb->open() != success) return fail;
+
+ compDb = new cDbTable(connection, "components");
+ if (compDb->open() != success) return fail;
+
+ timerDb = new cDbTable(connection, "timers");
+ if (timerDb->open() != success) return fail;
+
+ timerDoneDb = new cDbTable(connection, "timersdone");
+ if (timerDoneDb->open() != success) return fail;
+
+ recordingDirDb = new cDbTable(connection, "recordingdirs");
+ if (recordingDirDb->open() != success) return fail;
+
+ recordingListDb = new cDbTable(connection, "recordinglist");
+ if (recordingListDb->open() != success) return fail;
+
+ if ((status = cParameters::initDb(connection)) != success)
+ return status;
+
+ // -------------------------------------------
+ // init db values
+
+ dvbDescription = new cDbValue("description", cDBS::ffText, 50000);
+
+ // -------------------------------------------
+ // init statements
+
+ tell(2, "Prepare statements ...");
+
+ selectMasterVdr = new cDbStatement(vdrDb);
+
+ // select uuid, name from vdrs
+ // where upper(master) = 'Y' and uuid <> ?;
+
+ selectMasterVdr->build("select ");
+ selectMasterVdr->bind("UUID", cDBS::bndOut);
+ selectMasterVdr->bind("NAME", cDBS::bndOut, ", ");
+ selectMasterVdr->build(" from %s where upper(%s) = 'Y' and ",
+ vdrDb->TableName(), vdrDb->getField("MASTER")->getDbName());
+ selectMasterVdr->bindCmp(0, "Uuid", 0, "<>");
+
+ status += selectMasterVdr->prepare();
+
+ // all images
+
+ selectAllImages = new cDbStatement(imageRefDb);
+
+ // prepare fields
+
+ imageSize.setField(&imageSizeDef);
+ imageUpdSp.setField(imageDb->getField("UpdSp"));
+ masterId.setField(eventsDb->getField("MasterId"));
+
+ // select e.masterid, r.imagename, r.eventid, r.lfn, length(i.image)
+ // from imagerefs r, images i, events e
+ // where i.imagename = r.imagename
+ // and e.eventid = r.eventid
+ // and (i.updsp > ? or r.updsp > ?)
+
+ selectAllImages->build("select ");
+ selectAllImages->setBindPrefix("e.");
+ selectAllImages->bind(&masterId, cDBS::bndOut);
+ selectAllImages->setBindPrefix("r.");
+ selectAllImages->bind("ImgName", cDBS::bndOut, ", ");
+ selectAllImages->bind("EventId", cDBS::bndOut, ", ");
+ selectAllImages->bind("Lfn", cDBS::bndOut, ", ");
+ selectAllImages->setBindPrefix("i.");
+ selectAllImages->build(", length(");
+ selectAllImages->bind(&imageSize, cDBS::bndOut);
+ selectAllImages->build(")");
+ selectAllImages->clrBindPrefix();
+ selectAllImages->build(" from %s r, %s i, %s e where ",
+ imageRefDb->TableName(), imageDb->TableName(), eventsDb->TableName());
+ selectAllImages->build("e.%s = r.%s and i.%s = r.%s and (",
+ eventsDb->getField("EventId")->getDbName(),
+ imageRefDb->getField("EventId")->getDbName(),
+ imageDb->getField("ImgName")->getDbName(),
+ imageRefDb->getField("ImgName")->getDbName());
+ selectAllImages->bindCmp("i", &imageUpdSp, ">");
+ selectAllImages->build(" or ");
+ selectAllImages->bindCmp("r", "UpdSp", 0, ">");
+ selectAllImages->build(")");
+
+ status += selectAllImages->prepare();
+
+ // select distinct channelid, channelname
+ // from channelmap;
+
+ selectAllChannels = new cDbStatement(mapDb);
+
+ selectAllChannels->build("select distinct ");
+ selectAllChannels->bind("CHANNELID", cDBS::bndOut);
+ selectAllChannels->bind("CHANNELNAME", cDBS::bndOut, ", ");
+ selectAllChannels->build(" from %s", mapDb->TableName());
+
+ status += selectAllChannels->prepare();
+
+ // select distinct channelid, channelname
+ // from channelmap
+ // where channleid = ?;
+
+ selectChannelById = new cDbStatement(mapDb);
+
+ selectChannelById->build("select distinct ");
+ selectChannelById->bind("CHANNELID", cDBS::bndOut);
+ selectChannelById->bind("CHANNELNAME", cDBS::bndOut, ", ");
+ selectChannelById->build(" from %s where ", mapDb->TableName());
+ selectChannelById->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet);
+
+ status += selectChannelById->prepare();
+
+ // update channlemap
+ // set unknownatvdr = 1
+ // where channelid = ?
+
+ markUnknownChannel = new cDbStatement(mapDb);
+
+ markUnknownChannel->build("update %s set ", mapDb->TableName());
+ markUnknownChannel->bind("UNKNOWNATVDR", cDBS::bndIn | cDBS::bndSet);
+ markUnknownChannel->build(" where ");
+ markUnknownChannel->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet);
+
+ status += markUnknownChannel->prepare();
+
+ // select changed events
+
+ selectUpdEvents = new cDbStatement(eventsDb);
+
+ // select useid, eventid, source, delflg, updflg, fileref,
+ // tableid, version, title, shorttext, starttime,
+ // duration, parentalrating, vps, description
+ // from eventsview
+ // where
+ // channelid = ?
+ // and updsp > ?
+ // and updflg in (.....)
+
+ selectUpdEvents->build("select ");
+ selectUpdEvents->bind("USEID", cDBS::bndOut);
+ // selectUpdEvents->bind("MASTERID", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("EVENTID", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("SOURCE", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("DELFLG", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("UPDFLG", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("FILEREF", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("TABLEID", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("VERSION", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("TITLE", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("SHORTTEXT", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("STARTTIME", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("DURATION", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("PARENTALRATING", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("VPS", cDBS::bndOut, ", ");
+ selectUpdEvents->bind("CONTENTS", cDBS::bndOut, ", ");
+ selectUpdEvents->bind(dvbDescription, cDBS::bndOut, ", ");
+ selectUpdEvents->build(" from eventsview where ");
+ selectUpdEvents->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet);
+ selectUpdEvents->bindCmp(0, "UPDSP", 0, ">", " and ");
+ selectUpdEvents->build(" and UPDFLG in (%s)", Us::getNeeded());
+
+ status += selectUpdEvents->prepare();
+
+ // select event by useid
+
+ selectEventById = new cDbStatement(useeventsDb);
+
+ // select * from eventsview
+ // where useid = ?
+ // and updflg in (.....)
+
+ selectEventById->build("select ");
+ selectEventById->bindAllOut();
+ selectEventById->build(" from %s where ", useeventsDb->TableName());
+ selectEventById->bind("USEID", cDBS::bndIn | cDBS::bndSet);
+ selectEventById->build(" and %s in (%s)",
+ useeventsDb->getField("UPDFLG")->getDbName(),
+ Us::getNeeded());
+
+ status += selectEventById->prepare();
+
+ // ...
+
+ // select stream, type, lang, description
+ // from components where
+ // eventid = ?;
+ // channelid = ?;
+
+ selectComponentsOf = new cDbStatement(compDb);
+
+ selectComponentsOf->build("select ");
+ selectComponentsOf->bind("Stream", cDBS::bndOut);
+ selectComponentsOf->bind("Type", cDBS::bndOut, ", ");
+ selectComponentsOf->bind("Lang", cDBS::bndOut, ", ");
+ selectComponentsOf->bind("Description", cDBS::bndOut, ", ");
+ selectComponentsOf->build(" from %s where ", compDb->TableName());
+ selectComponentsOf->bind("EventId", cDBS::bndIn | cDBS::bndSet);
+ selectComponentsOf->bind("ChannelId", cDBS::bndIn | cDBS::bndSet, " and ");
+
+ status += selectComponentsOf->prepare();
+
+ // select *
+ // from recordinglist where
+ // state <> 'D'
+
+ selectRecordings = new cDbStatement(recordingListDb);
+
+ selectRecordings->build("select ");
+ selectRecordings->bindAllOut();
+ selectRecordings->build(" from %s where ", recordingListDb->TableName());
+ selectRecordings->build(" (%s <> 'D' or %s is null)",
+ recordingListDb->getField("STATE")->getDbName(),
+ recordingListDb->getField("STATE")->getDbName());
+
+ status += selectRecordings->prepare();
+
+ // select srcmovieid, srcseriesid, scrseriesepisode
+ // from recordinglist where
+ // state <> 'D' or stete is null
+ // and (updsp > lastifoupd or lastifoupd is null)
+
+ selectRecForInfoUpdate = new cDbStatement(recordingListDb);
+
+ selectRecForInfoUpdate->build("select ");
+ selectRecForInfoUpdate->bindAllOut();
+ selectRecForInfoUpdate->build(" from %s where ", recordingListDb->TableName());
+ selectRecForInfoUpdate->build(" (%s <> 'D' or %s is null)",
+ recordingListDb->getField("STATE")->getDbName(),
+ recordingListDb->getField("STATE")->getDbName());
+ selectRecForInfoUpdate->build(" and (%s > %s or %s is null) ",
+ recordingListDb->getField("UPDSP")->getDbName(),
+ recordingListDb->getField("LASTIFOUPD")->getDbName(),
+ recordingListDb->getField("LASTIFOUPD")->getDbName());
+
+ status += selectRecForInfoUpdate->prepare();
+
+ // select *
+ // from timers where
+ // state <> 'D'
+ // and vdruuid = ?
+
+ selectMyTimer = new cDbStatement(timerDb);
+
+ selectMyTimer->build("select ");
+ selectMyTimer->bindAllOut();
+ selectMyTimer->build(" from %s where ", timerDb->TableName());
+ selectMyTimer->build(" %s <> 'D'", timerDb->getField("STATE")->getDbName());
+ selectMyTimer->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and ");
+
+ status += selectMyTimer->prepare();
+
+ // select id, eventid, channelid, starttime, state, endtime
+ // from timers where
+ // eventid = ? and channelid = ? and vdruuid = ?
+
+ selectTimerByEvent = new cDbStatement(timerDb);
+
+ selectTimerByEvent->build("select ");
+ selectTimerByEvent->bind("ID", cDBS::bndOut);
+ selectTimerByEvent->bind("EVENTID", cDBS::bndOut, ", ");
+ selectTimerByEvent->bind("CHANNELID", cDBS::bndOut, ", ");
+ selectTimerByEvent->bind("STARTTIME", cDBS::bndOut, ", ");
+ selectTimerByEvent->bind("STATE", cDBS::bndOut, ", ");
+ selectTimerByEvent->bind("ENDTIME", cDBS::bndOut, ", ");
+ selectTimerByEvent->build(" from %s where ", timerDb->TableName());
+ selectTimerByEvent->bind("EVENTID", cDBS::bndIn | cDBS::bndSet);
+ selectTimerByEvent->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet, " and ");
+ selectTimerByEvent->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and ");
+
+ status += selectTimerByEvent->prepare();
+
+ // select *
+ // from timers where
+ // id = ?
+
+ selectTimerById = new cDbStatement(timerDb);
+
+ selectTimerById->build("select ");
+ selectTimerById->bindAllOut();
+ selectTimerById->build(" from %s where ", timerDb->TableName());
+ selectTimerById->bind("ID", cDBS::bndIn | cDBS::bndSet);
+
+ status += selectTimerById->prepare();
+
+ // select *
+ // from timers where
+ // doneid = ?
+
+ selectTimerByDoneId = new cDbStatement(timerDb);
+
+ selectTimerByDoneId->build("select ");
+ selectTimerByDoneId->bindAllOut();
+ selectTimerByDoneId->build(" from %s where ", timerDb->TableName());
+ selectTimerByDoneId->bind("DONEID", cDBS::bndIn | cDBS::bndSet);
+
+ status += selectTimerByDoneId->prepare();
+
+ //-----------
+ // select
+ // max(updsp)
+ // from timers
+
+ selectMaxUpdSp = new cDbStatement(timerDb);
+
+ selectMaxUpdSp->build("select ");
+ selectMaxUpdSp->bind("UPDSP", cDBS::bndOut, "max(");
+ selectMaxUpdSp->build(") from %s", timerDb->TableName());
+
+ status += selectMaxUpdSp->prepare();
+
+ // select * from timers
+ // where
+ // action != 'A' and action != 'F' and action is not null // !taAssumed and !taFailed
+ // and (vdruuid = ? or vdruuid = 'any')
+
+ selectPendingTimerActions = new cDbStatement(timerDb);
+
+ selectPendingTimerActions->build("select ");
+ selectPendingTimerActions->bindAllOut();
+ selectPendingTimerActions->build(" from %s where ", timerDb->TableName());
+ selectPendingTimerActions->build("%s != 'A' and %s != 'F' and %s is not null",
+ timerDb->getField("ACTION")->getDbName(),
+ timerDb->getField("ACTION")->getDbName(),
+ timerDb->getField("ACTION")->getDbName());
+ selectPendingTimerActions->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and (");
+ selectPendingTimerActions->build(" or %s = 'any')", timerDb->getField("VDRUUID")->getDbName());
+
+ status += selectPendingTimerActions->prepare();
+
+ // delete from timers where
+ // eventid = ?
+
+ deleteTimer = new cDbStatement(timerDb);
+
+ deleteTimer->build("delete from %s where ", timerDb->TableName());
+ deleteTimer->bind("EVENTID", cDBS::bndIn | cDBS::bndSet);
+ deleteTimer->bind("CHANNELID", cDBS::bndIn | cDBS::bndSet, " and ");
+ deleteTimer->bind("VDRUUID", cDBS::bndIn | cDBS::bndSet, " and ");
+
+ status += deleteTimer->prepare();
+
+ if (status == success)
+ {
+ // -------------------------------------------
+ // lookback -> get last stamp
+
+ vdrDb->clear();
+ vdrDb->setValue("UUID", Epg2VdrConfig.uuid);
+
+ if (vdrDb->find())
+ {
+ char buf[50+TB];
+
+ lastUpdateAt = vdrDb->getIntValue("LastUpdate");
+ lastEventsUpdateAt = lastUpdateAt;
+ getParameter("uuid", "lastEventsUpdateAt", lastEventsUpdateAt);
+
+ strftime(buf, 50, "%d.%m.%y %H:%M:%S", localtime(&lastUpdateAt));
+ tell(0, "Info: Last update was at '%s'", buf);
+ }
+
+ // register me to the vdrs table
+
+ int devCount = 0;
+ char* v;
+
+ for (int i = 0; i < cDevice::NumDevices(); i++)
+ {
+ const cDevice* device = cDevice::GetDevice(i);
+
+ if (device && device->NumProvidedSystems())
+ devCount ++;
+ }
+
+ asprintf(&v, "vdr %s epg2vdr %s (%s)", VDRVERSION, VERSION, VERSION_DATE);
+
+ vdrDb->setValue("UUID", Epg2VdrConfig.uuid);
+ vdrDb->setValue("IP", getIpOf(Epg2VdrConfig.netDevice));
+ vdrDb->setValue("MAC", getMacOf(Epg2VdrConfig.netDevice));
+ vdrDb->setValue("NAME", getHostName());
+ vdrDb->setValue("DBAPI", DB_API);
+ vdrDb->setValue("VERSION", v);
+ vdrDb->setValue("STATE", "attached");
+ vdrDb->setValue("MASTER", "n");
+ vdrDb->setValue("TUNERCOUNT", devCount);
+ vdrDb->setValue("SHAREINWEB", Epg2VdrConfig.shareInWeb);
+ vdrDb->setValue("USECOMMONRECFOLDER", Epg2VdrConfig.useCommonRecFolder);
+
+ // set svdrp port if uninitialized, we can't query ther actual port from VDR :(
+
+ if (vdrDb->getIntValue("SVDRP") == 0)
+ vdrDb->setValue("SVDRP", 6419); // #TODO -> plugin-setup?!
+
+ vdrDb->store();
+ updateVdrData();
+ free(v);
+ }
+
+ if (status == success)
+ status += cEpg2VdrEpgHandler::getSingleton()->updateExternalIdsMap(mapDb);
+
+ return status;
+}
+
+int cUpdate::exitDb()
+{
+ // de-register me at the vdrs table
+
+ if (vdrDb && vdrDb->isConnected())
+ {
+ vdrDb->setValue("Uuid", Epg2VdrConfig.uuid);
+ vdrDb->find();
+ vdrDb->setValue("Master", "n");
+ vdrDb->setValue("State", "detached");
+ vdrDb->store();
+ }
+
+ cParameters::exitDb();
+
+ delete selectAllImages; selectAllImages = 0;
+ delete selectUpdEvents; selectUpdEvents = 0;
+ delete selectEventById; selectEventById = 0;
+ delete selectAllChannels; selectAllChannels = 0;
+ delete selectChannelById; selectChannelById = 0;
+ delete markUnknownChannel; markUnknownChannel = 0;
+ delete selectComponentsOf; selectComponentsOf = 0;
+ delete selectMasterVdr; selectMasterVdr = 0;
+ delete deleteTimer; deleteTimer = 0;
+ delete selectMyTimer; selectMyTimer = 0;
+ delete selectRecordings; selectRecordings = 0;
+ delete selectRecForInfoUpdate; selectRecForInfoUpdate = 0;
+ delete selectTimerByEvent; selectTimerByEvent = 0;
+ delete selectTimerById; selectTimerById = 0;
+ delete selectTimerByDoneId; selectTimerByDoneId = 0;
+ delete selectMaxUpdSp; selectMaxUpdSp = 0;
+ delete selectPendingTimerActions; selectPendingTimerActions = 0;
+
+ delete vdrDb; vdrDb = 0;
+ delete mapDb; mapDb = 0;
+ delete fileDb; fileDb = 0;
+ delete imageDb; imageDb = 0;
+ delete imageRefDb; imageRefDb = 0;
+ delete episodeDb; episodeDb = 0;
+ delete eventsDb; eventsDb = 0;
+ delete useeventsDb; useeventsDb = 0;
+ delete compDb; compDb = 0;
+ delete timerDb; timerDb = 0;
+ delete timerDoneDb; timerDoneDb = 0;
+ delete recordingDirDb; recordingDirDb = 0;
+ delete recordingListDb; recordingListDb = 0;
+
+ delete dvbDescription; dvbDescription = 0;
+
+ delete connection; connection = 0;
+
+ return done;
+}
+
+//***************************************************************************
+// Check Connection
+//***************************************************************************
+
+int cUpdate::checkConnection(int& timeout)
+{
+ static int retryCount = 0;
+
+ timeout = retryCount < 5 ? 10 : 60;
+
+ // reconnect requested ?
+
+ if (dbReconnectTriggered)
+ exitDb();
+
+ dbReconnectTriggered = no;
+
+ // check connection
+
+ if (!dbConnected(yes))
+ {
+ // try to connect
+
+ tell(0, "Trying to re-connect to database!");
+ retryCount++;
+
+ if (initDb() != success)
+ {
+ tell(0, "Retry #%d failed, retrying in %d seconds!", retryCount, timeout);
+ exitDb();
+
+ return fail;
+ }
+
+ retryCount = 0;
+ tell(0, "Connection established successfull!");
+ }
+
+ return success;
+}
+
+//***************************************************************************
+// Is Handler Master
+//***************************************************************************
+
+int cUpdate::isHandlerMaster()
+{
+ static int initialized = no;
+
+ char flag = 0;
+
+/*
+ wenn no - handler ausschalten und db auf 'N'
+ wenn auto - aushandeln
+ wenn yes - handler anschalten und db auf 'Y'
+
+*/
+ if (!dbConnected())
+ return no;
+
+ // on first call detect state of epgd
+
+ if (!initialized)
+ {
+ epgdBusy = no;
+
+ vdrDb->clear();
+ vdrDb->setValue("UUID", EPGDNAME);
+
+ if (vdrDb->find())
+ {
+ nextEpgdUpdateAt = vdrDb->getIntValue("NEXTUPDATE");
+ epgdState = cEpgdState::toState(vdrDb->getStrValue("State"));
+
+ if (epgdState >= cEpgdState::esBusy && epgdState < cEpgdState::esBusyImages)
+ epgdBusy = yes;
+
+ tell(1, "Detected epgd state '%s' (%d)", vdrDb->getStrValue("State"), epgdState);
+ initialized = yes;
+ }
+ else
+ {
+ tell(0, "Info: Can't detect epgd state");
+ epgdBusy = yes;
+ }
+ }
+
+ // update handler role
+
+ if (Epg2VdrConfig.masterMode == mmAuto)
+ {
+ tell(3, "Auto check master role");
+
+ // select where "uuid <> ? and upper(master) = 'Y'"
+
+ vdrDb->clear();
+ vdrDb->setValue("Uuid", Epg2VdrConfig.uuid);
+
+ handlerMaster = !selectMasterVdr->find();
+
+ if (!handlerMaster)
+ tell(3, "Master found, uuid '%s' (%s)",
+ vdrDb->getStrValue("UUID"),
+ vdrDb->getStrValue("NAME"));
+
+ selectMasterVdr->freeResult();
+
+ flag = handlerMaster ? 'y' : 'n';
+ }
+ else if (Epg2VdrConfig.masterMode == mmYes)
+ {
+ flag = 'Y';
+ handlerMaster = yes;
+ }
+ else
+ {
+ flag = 'n';
+ handlerMaster = no;
+ }
+
+ // write again to force at least the updsp
+
+ vdrDb->clear();
+ vdrDb->setValue("UUID", Epg2VdrConfig.uuid);
+ vdrDb->find();
+
+ vdrDb->setValue("STATE", "attached");
+ vdrDb->setCharValue("MASTER", flag);
+ vdrDb->store();
+
+ // set handler state
+
+ int handlerState = !epgdBusy && handlerMaster;
+
+ if (cEpg2VdrEpgHandler::getSingleton()->getActive() != handlerState)
+ {
+ tell(1, "Change handler state to '%s'", handlerState ? "active" : "standby");
+ cEpg2VdrEpgHandler::getSingleton()->setActive(handlerState);
+ }
+
+ return handlerMaster;
+}
+
+// // ***************************************************************************
+// // Initially Init Epgd State
+// // ***************************************************************************
+
+// void cUpdate::updateEpgdState()
+// {
+// epgdBusy = no;
+
+// if (!dbConnected())
+// return;
+
+// vdrDb->clear();
+// vdrDb->setValue("UUID", EPGDNAME);
+
+// if (vdrDb->find())
+// {
+// nextEpgdUpdateAt = vdrDb->getIntValue("NEXTUPDATE");
+// epgdState = cEpgdState::toState(vdrDb->getStrValue("State"));
+
+// if (epgdState >= cEpgdState::esBusy && epgdState < cEpgdState::esBusyImages)
+// epgdBusy = yes;
+
+// tell(1, "Detected epgd state '%s' (%d)", vdrDb->getStrValue("State"), epgdState);
+// }
+// else
+// tell(0, "Info: Can't detect epgd state");
+
+// int handlerState = !epgdBusy && isHandlerMaster();
+
+// if (cEpg2VdrEpgHandler::getSingleton()->getActive() != handlerState)
+// tell(0, "Set handler state initially to '%s'", handlerState ? "active" : "standby");
+
+// cEpg2VdrEpgHandler::getSingleton()->setActive(handlerState);
+
+// vdrDb->reset();
+// }
+
+// ***************************************************************************
+// Update VDR Data
+// ***************************************************************************
+
+void cUpdate::updateVdrData()
+{
+ int usedMb = 0;
+ int freeMb = 0;
+
+ cVideoDirectory::VideoDiskSpace(&freeMb, &usedMb);
+
+ vdrDb->clear();
+ vdrDb->setValue("UUID", Epg2VdrConfig.uuid);
+ vdrDb->find();
+
+ vdrDb->setValue("VIDEODIR", videoBasePath);
+ vdrDb->setValue("VIDEOTOTAL", usedMb+freeMb);
+ vdrDb->setValue("VIDEOFREE", freeMb);
+
+ vdrDb->store();
+}
+
+//***************************************************************************
+// Update Rec Folder Option
+//***************************************************************************
+
+int cUpdate::updateRecFolderOption()
+{
+ vdrDb->clear();
+ vdrDb->setValue("UUID", Epg2VdrConfig.uuid);
+
+ if (vdrDb->find())
+ {
+ vdrDb->setValue("USECOMMONRECFOLDER", Epg2VdrConfig.useCommonRecFolder);
+ vdrDb->update();
+ }
+
+ updateRecFolderOptionTrigger = no;
+ recordingStateChangedTrigger = yes;
+ recordingFullReloadTrigger = yes;
+
+ return done;
+}
+
+//***************************************************************************
+// Trigger Update
+//***************************************************************************
+
+void cUpdate::triggerDbReconnect()
+{
+ dbReconnectTriggered = yes;
+ waitCondition.Broadcast();
+}
+
+//***************************************************************************
+// Trigger Update
+//***************************************************************************
+
+int cUpdate::triggerEpgUpdate(int reload)
+{
+ if (!loopActive)
+ {
+ tell(0, "Thread not running, try to recover ...");
+ Cancel(3);
+ Start();
+ }
+
+ fullreload = reload;
+ mainActPending = yes;
+ manualTrigger = yes;
+
+ if (epgdBusy)
+ {
+ Skins.QueueMessage(mtInfo, cString::sprintf(tr("Skipping '%s' request, epgd busy. Trying again later"),
+ reload ? tr("reload") : tr("update")));
+
+ return fail;
+ }
+
+ waitCondition.Broadcast(); // wakeup thread
+
+ return success;
+}
+
+//***************************************************************************
+// Trigger Recording Update
+//***************************************************************************
+
+int cUpdate::triggerRecUpdate()
+{
+ if (!loopActive)
+ {
+ tell(0, "Thread not running, try to recover ...");
+ Cancel(3);
+ Start();
+ }
+
+ recordingStateChangedTrigger = yes;
+ waitCondition.Broadcast(); // wakeup thread
+
+ return success;
+}
+
+int cUpdate::commonRecFolderOptionChanged()
+{
+ updateRecFolderOptionTrigger = yes;
+ waitCondition.Broadcast(); // wakeup thread
+
+ return done;
+}
+
+//***************************************************************************
+// Trigger Store Info Files
+//***************************************************************************
+
+int cUpdate::triggerStoreInfoFiles()
+{
+ if (!loopActive)
+ {
+ tell(0, "Thread not running, try to recover ...");
+ Cancel(3);
+ Start();
+ }
+
+ storeAllRecordingInfoFilesTrigger = yes;
+ waitCondition.Broadcast(); // wakeup thread
+
+ return success;
+}
+
+//***************************************************************************
+// Trigger Timer Jobs
+//***************************************************************************
+
+void cUpdate::triggerTimerJobs()
+{
+ if (!loopActive)
+ {
+ tell(0, "Thread not running, try to recover ...");
+ Cancel(3);
+ Start();
+ }
+
+ timerTableUpdateTriggered = yes;
+ timerJobsUpdateTriggered = yes;
+ waitCondition.Broadcast(); // wakeup thread
+}
+
+//***************************************************************************
+// Epgd State Change
+// - called via SVDRP
+//***************************************************************************
+
+void cUpdate::epgdStateChange(const char* state)
+{
+ // state control
+
+ if (!dbConnected())
+ return;
+
+ epgdState = cEpgdState::toState(state);
+ epgdBusy = epgdState >= cEpgdState::esBusy && epgdState < cEpgdState::esBusyImages;
+
+ tell(1, "Got epgd state '%s' (%d)", state, epgdState);
+
+ if (epgdState == Es::esBusyMatch)
+ eventsPending = yes; // events pending due to merge
+ else if (epgdState == Es::esBusyEvents)
+ mainActPending = yes; // main action pending
+
+ // epg handler control
+
+ int handlerState = !epgdBusy && handlerMaster;
+
+ if (cEpg2VdrEpgHandler::getSingleton()->getActive() != handlerState)
+ tell(1, "Change handler state to '%s'", handlerState ? "active" : "standby");
+
+ cEpg2VdrEpgHandler::getSingleton()->setActive(handlerState);
+
+ // check loop state
+
+ if (!loopActive)
+ {
+ tell(0, "Thread not running, try to recover ...");
+ Cancel(3);
+ Start();
+ }
+
+ if (!epgdBusy)
+ waitCondition.Broadcast(); // wakeup thread
+}
+
+//***************************************************************************
+// Stop Thread
+//***************************************************************************
+
+void cUpdate::Stop()
+{
+ loopActive = no;
+ waitCondition.Broadcast(); // wakeup thread
+
+ Cancel(10); // wait up to 10 seconds for thread was stopping
+}
+
+//***************************************************************************
+// Action
+//***************************************************************************
+
+void cUpdate::Action()
+{
+ // open tables - inside thread!
+
+ if (initDb() != success)
+ exitDb();
+
+ // mutex gets ONLY unlocked when sleeping
+
+ mutex.Lock();
+ loopActive = yes;
+
+ // main action loop ...
+
+ while (loopActive && Running())
+ {
+ int reconnectTimeout; // set by checkConnection()
+
+ // wait 1 minute
+
+ waitCondition.TimedWait(mutex, 60*1000);
+
+ // we pass here at least once per minute ...
+
+ if (checkConnection(reconnectTimeout) != success)
+ continue;
+
+ // recording stuff
+
+ if (dbConnected() && updateRecFolderOptionTrigger)
+ updateRecFolderOption();
+
+ if (dbConnected() && recordingStateChangedTrigger)
+ {
+ if (Epg2VdrConfig.shareInWeb)
+ recordingChanged(); // update timer state
+
+ updateVdrData(); // update video disk size/free, ...
+ updateRecordingTable(recordingFullReloadTrigger);
+
+ recordingFullReloadTrigger = no;
+ recordingStateChangedTrigger = no;
+ }
+ else if (dbConnected() && lastRecordingDeleteAt+5*tmeSecondsPerMinute < time(0))
+ {
+ cleanupDeletedRecordings();
+ updateRecordingInfoFiles();
+ lastRecordingDeleteAt = time(0);
+ }
+
+ if (dbConnected() && storeAllRecordingInfoFilesTrigger)
+ storeAllRecordingInfoFiles();
+
+ // check handler role
+
+ isHandlerMaster();
+
+ if (epgdBusy)
+ continue;
+
+ if (Epg2VdrConfig.shareInWeb)
+ {
+ // check timer distribution and take over 'my' timers
+
+ if (dbConnected() && timerJobsUpdateTriggered)
+ {
+ tell(2, "Updating EPG prior to 'check of timer request'");
+ refreshEpg(0, na); // refresh EPG before performing timer jobs!
+ performTimerJobs();
+ }
+
+ // update timer
+
+ if (dbConnected() && timerTableUpdateTriggered)
+ updateTimerTable();
+
+ if (dbConnected())
+ timerChanged();
+ }
+
+ // if triggered externally or updates pending
+
+ if (dbConnected(yes) && (mainActPending || eventsPending))
+ {
+ time_t lastUpdateStartAt = time(0);
+
+ tell(mainActPending ? 0 : 2, "--- EPG '%s' started ---", fullreload ? "full-reload" : mainActPending ? "update" : "refresh");
+
+ if (mainActPending)
+ {
+ // cleanup unreferenced image-files
+
+ if (cleanupPictures() != success)
+ continue;
+ }
+
+ if (refreshEpg() != success)
+ continue;
+
+ lastEventsUpdateAt = time(0);
+ setParameter("uuid", "lastEventsUpdateAt", lastEventsUpdateAt);
+ cSchedules::Cleanup(true); // force VDR to store of epg.data to filesystem
+
+ if (mainActPending)
+ {
+ // get pictures from database and copy to local FS
+
+ if (storePicturesToFs() != success)
+ continue;
+
+ lastUpdateAt = lastUpdateStartAt; // update lookback information (notice)
+
+ vdrDb->clear();
+ vdrDb->setValue("Uuid", Epg2VdrConfig.uuid);
+ vdrDb->find();
+ vdrDb->setValue("LastUpdate", lastUpdateAt);
+ vdrDb->store();
+ }
+
+ tell(mainActPending ? 0 : 2, "--- EPG %s finished ---", fullreload ? "reload" : mainActPending ? "update" : "refresh");
+
+ if (Epg2VdrConfig.loglevel > 2)
+ connection->showStat("update");
+
+ if (manualTrigger)
+ {
+ Skins.QueueMessage(mtInfo, cString::sprintf(tr("EPG '%s' done"), fullreload ? tr("reload") : tr("update")));
+ manualTrigger = no;
+ }
+
+ mainActPending = no;
+ eventsPending = no;
+ fullreload = no;
+ }
+ }
+
+ exit(); // don't call exit in dtor outside of thread!!
+
+ loopActive = no;
+
+ tell(0, "Update thread ended (tid=%i)", cThread::ThreadId());
+}
+
+//***************************************************************************
+// Clear Epg
+//***************************************************************************
+
+void clearEpg()
+{
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+
+ LOCK_TIMERS_WRITE;
+ LOCK_SCHEDULES_WRITE;
+
+ for (cTimer* timer = Timers->First(); timer; timer = Timers->Next(timer))
+ timer->SetEvent(0); // processing all timers here (local *and* remote)
+
+ for (cSchedule* schedule = Schedules->First(); schedule; schedule = Schedules->Next(schedule))
+ schedule->Cleanup(INT_MAX);
+
+ cEitFilter::SetDisableUntil(time(0) + 10);
+
+#else
+
+ while (!cSchedules::ClearAll())
+ tell(0, "Warning: Clear EPG failed, can't get lock. Retrying ...");
+
+#endif
+}
+
+//***************************************************************************
+// Refresh Epg
+//***************************************************************************
+
+int cUpdate::refreshEpg(const char* forChannelId, int maxTries)
+{
+ const cEvent* event;
+ int tries = 0;
+ int timerChanges = 0;
+ int total = 0;
+ int dels = 0;
+ int channels = 0;
+ uint64_t start = cTimeMs::Now();
+ cDbStatement* select = 0;
+
+ // lookback ...
+
+ getParameter("uuid", "lastEventsUpdateAt", lastEventsUpdateAt);
+
+ // full reload?
+
+ if (fullreload)
+ {
+ tell(1, "Removing all events from epg");
+
+ clearEpg();
+
+ lastUpdateAt = 0;
+ lastEventsUpdateAt = 0;
+ }
+
+ // iterate over all channels in channelmap
+
+ mapDb->clear();
+
+ if (forChannelId)
+ {
+ select = selectChannelById;
+ mapDb->setValue("CHANNELID", forChannelId);
+
+ tell(1, "Load EPG for channel '%s'", forChannelId);
+ }
+ else
+ {
+ select = selectAllChannels;
+
+ if (lastEventsUpdateAt)
+ tell(2, "Update EPG, loading changes since %s", l2pTime(lastEventsUpdateAt).c_str());
+ else
+ tell(2, "Update EPG, reloading all events");
+ }
+
+ for (int f = select->find(); f && dbConnected(yes); f = select->fetch())
+ {
+ int count = 0;
+ cSchedule* s = 0;
+ tChannelID channelId = tChannelID::FromString(mapDb->getStrValue("ChannelId"));
+
+ channels++;
+
+ eventsDb->clear();
+ eventsDb->setValue("UPDSP", forChannelId ? 0 : lastEventsUpdateAt);
+ eventsDb->setValue("CHANNELID", mapDb->getStrValue("CHANNELID"));
+
+ // get timers lock
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cStateKey timersKey;
+ tell(3, "-> Try to get timers lock");
+ cTimers* timers = cTimers::GetTimersWrite(timersKey, 500/*ms*/);
+#else
+ cTimers* timers = &Timers;
+#endif
+
+ // get schedules lock
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ cStateKey schedulesKey;
+ tell(3, "-> Try to get schedules lock");
+ cSchedules* schedules = cSchedules::GetSchedulesWrite(schedulesKey, 500/*ms*/);
+#else
+ cSchedulesLock* schedulesLock = new cSchedulesLock(true, 500/*ms*/);
+ cSchedules* schedules = (cSchedules*)cSchedules::Schedules(*schedulesLock);
+ tell(3, "LOCK (refreshEpg)");
+#endif
+
+ if (!schedules || !timers)
+ {
+ tell(3, "Info: Can't get write lock on '%s'", !schedules ? "schedules" : "timers");
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ if (schedules) schedulesKey.Remove();
+ if (timers) timersKey.Remove();
+#else
+ delete schedulesLock;
+#endif
+
+ if (tries++ > maxTries)
+ {
+ select->freeResult();
+ tell(3, "Warning: Aborting refresh after %d tries", tries);
+ break;
+ }
+
+ tell(3, "Retrying in 3 seconds");
+ sleep(3);
+
+ continue;
+ }
+
+ tries = 0;
+
+ // get schedule (channel)
+
+ if (!(s = (cSchedule*)schedules->GetSchedule(channelId)))
+ s = schedules->AddSchedule(channelId);
+
+ // -----------------------------------------
+ // iterate over all events of this channel
+
+ for (int found = selectUpdEvents->find(); found && dbConnected(); found = selectUpdEvents->fetch())
+ {
+ cTimer* timer = 0;
+ char updFlg = toupper(eventsDb->getStrValue("UPDFLG")[0]);
+
+ updFlg = updFlg == 0 ? 'P' : updFlg; // fix missing flag
+
+ // ignore unneded event rows ..
+
+ if (!Us::isNeeded(updFlg))
+ continue;
+
+ // get event / timer
+
+ if (event = s->GetEvent(eventsDb->getIntValue("USEID")))
+ {
+ if (Us::isRemove(updFlg))
+ tell(2, "Remove event %uld of channel '%s' due to updflg %c",
+ event->EventID(), (const char*)event->ChannelID().ToString(), updFlg);
+
+ if (event->HasTimer())
+ {
+ for (timer = timers->First(); timer; timer = timers->Next(timer))
+ if (timer->Event() == event)
+ break;
+ }
+
+ if (timer)
+ timer->SetEvent(0);
+
+ s->DelEvent((cEvent*)event);
+ }
+
+ if (!Us::isRemove(updFlg))
+ event = s->AddEvent(createEventFromRow(eventsDb->getRow()));
+ else if (event)
+ {
+ event = 0;
+ dels++;
+ }
+
+ if (timer && event)
+ {
+ timer->SetEvent(event);
+ timer->Matches(event);
+ timerChanges++;
+ }
+ else if (timer)
+ {
+ tell(0, "Info: Timer '%s', has no event anymore", *timer->ToDescr());
+ }
+
+ count++;
+ }
+
+ selectUpdEvents->freeResult();
+
+ // Kanal fertig machen ..
+
+ s->Sort();
+ s->SetModified();
+
+ // schedules lock freigeben
+
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ schedulesKey.Remove();
+ tell(3, "-> Released schedules lock");
+ timersKey.Remove();
+ tell(3, "-> Released timers lock");
+#else
+ tell(3, "LOCK free (refreshEpg)");
+ delete schedulesLock;
+#endif
+
+ tell(2, "Processed channel '%s' - '%s' with %d updates",
+ eventsDb->getStrValue("ChannelId"),
+ mapDb->getStrValue("ChannelName"),
+ count);
+
+ total += count;
+ }
+
+ select->freeResult();
+
+ if (timerChanges)
+ {
+#if defined (APIVERSNUM) && (APIVERSNUM >= 20301)
+ LOCK_TIMERS_WRITE;
+ cTimers* timers = Timers;
+#else
+ cTimers* timers = &Timers;
+#endif
+ timers->SetModified();
+ }
+
+ if (lastEventsUpdateAt)
+ tell(2, "Updated changes since '%s'; %d channels, "
+ "%d events (%d deletions) in %s",
+ forChannelId ? "-" : l2pTime(lastEventsUpdateAt).c_str(),
+ channels, total, dels, ms2Dur(cTimeMs::Now()-start).c_str());
+ else
+ tell(2, "Updated all %d channels, %d events (%d deletions) in %s",
+ channels, total, dels, ms2Dur(cTimeMs::Now()-start).c_str());
+
+ return dbConnected(yes) ? success : fail;
+}
+
+//***************************************************************************
+// To/From Row
+//***************************************************************************
+
+cEvent* cUpdate::createEventFromRow(const cDbRow* row)
+{
+ cEvent* e = new cEvent(row->getIntValue("USEID"));
+
+ e->SetTableID(row->getIntValue("TABLEID"));
+ e->SetVersion(row->getIntValue("VERSION"));
+ e->SetTitle(row->getStrValue("TITLE"));
+ e->SetShortText(row->getStrValue("SHORTTEXT"));
+ e->SetStartTime(row->getIntValue("STARTTIME"));
+ e->SetDuration(row->getIntValue("DURATION"));
+ e->SetParentalRating(row->getIntValue("PARENTALRATING"));
+ e->SetVps(row->getIntValue("VPS"));
+ e->SetDescription(dvbDescription->getStrValue());
+ e->SetComponents(0);
+
+ // contents
+
+ uchar contents[MaxEventContents] = { 0 };
+ int numContents = 0;
+
+ for (const char* p = row->getStrValue("CONTENTS"); p && numContents < MaxEventContents; p = strchr(p, ','))
+ {
+ if (*p == ',') p++;
+
+ if (*p)
+ contents[numContents++] = strtol(p, 0, 0);
+ }
+
+ e->SetContents(contents);
+
+ // components
+
+ if (row->hasValue("SOURCE", "vdr"))
+ {
+ cComponents* components = new cComponents;
+
+ compDb->clear();
+ compDb->setBigintValue("EVENTID", row->getBigintValue("EVENTID"));
+ compDb->setValue("CHANNELID", row->getStrValue("CHANNELID"));
+
+ for (int f = selectComponentsOf->find(); f; f = selectComponentsOf->fetch())
+ {
+ components->SetComponent(components->NumComponents(),
+ compDb->getIntValue("STREAM"),
+ compDb->getIntValue("TYPE"),
+ compDb->getStrValue("LANG"),
+ compDb->getStrValue("DESCRIPTION"));
+ }
+
+ selectComponentsOf->freeResult();
+
+ if (components->NumComponents())
+ e->SetComponents(components); // event take ownership of components!
+ else
+ delete components;
+ }
+
+ return e;
+}
+
+//***************************************************************************
+// Store Pictures to local Filesystem
+//***************************************************************************
+
+int cUpdate::storePicturesToFs()
+{
+ int count = 0;
+ int updated = 0;
+ char* path = 0;
+ time_t start = time(0);
+
+ if (!Epg2VdrConfig.getepgimages)
+ return done;
+
+ asprintf(&path, "%s", epgimagedir);
+ chkDir(path);
+ free(path);
+ asprintf(&path, "%s/images", epgimagedir);
+ chkDir(path);
+ free(path);
+
+ tell(0, "Load images from database");
+
+ imageRefDb->clear();
+ imageRefDb->setValue("UpdSp", lastUpdateAt);
+ imageUpdSp.setValue(lastUpdateAt);
+
+ for (int res = selectAllImages->find(); res; res = selectAllImages->fetch())
+ {
+ int eventid = masterId.getIntValue();
+ const char* imageName = imageRefDb->getStrValue("ImgName");
+ int lfn = imageRefDb->getIntValue("Lfn");
+ char* newpath;
+ char* linkdest = 0;
+ char* destfile = 0;
+ int forceLink = no;
+ int size = imageSize.getIntValue();
+
+ asprintf(&destfile, "%s/images/%s", epgimagedir, imageName);
+
+ // check target ... image changed?
+
+ if (!fileExists(destfile) || fileSize(destfile) != size)
+ {
+ // get image
+
+ imageDb->clear();
+ imageDb->setValue("ImgName", imageName);
+
+ if (imageDb->find() && !imageDb->getRow()->getValue("Image")->isNull())
+ {
+ count++;
+
+ // remove existing target
+
+ if (fileExists(destfile))
+ {
+ updated++;
+ removeFile(destfile);
+ }
+
+ forceLink = yes;
+ tell(2, "Store image '%s' with %d bytes", destfile, size);
+
+ if (FILE* fh1 = fopen(destfile, "w"))
+ {
+ fwrite(imageDb->getStrValue("Image"), 1, size, fh1);
+ fclose(fh1);
+ }
+ else
+ {
+ tell(1, "Can't write image to '%s', error was '%s'", destfile, strerror(errno));
+ }
+ }
+
+ imageDb->reset();
+ }
+
+ free(destfile);
+
+ // create links ...
+
+ asprintf(&linkdest, "./images/%s", imageName);
+
+#ifdef _IMG_LINK
+ if (!lfn)
+ {
+ // for lfn 0 create additional link without "_?"
+
+ asprintf(&newpath, "%s/%d.%s", epgimagedir, eventid, imageExtension);
+ createLink(newpath, linkdest, forceLink);
+ free(newpath);
+ }
+#endif
+
+ // create link with index
+
+ asprintf(&newpath, "%s/%d_%d.%s", epgimagedir, eventid, lfn, imageExtension);
+ createLink(newpath, linkdest, forceLink);
+ free(newpath);
+
+ // ...
+
+ free(linkdest);
+
+ if (!dbConnected())
+ break;
+ }
+
+ selectAllImages->freeResult();
+
+ tell(0, "Got %d images from database in %ld seconds (%d updates, %d new)",
+ count, time(0) - start, updated, count-updated);
+
+ return dbConnected(yes) ? success : fail;
+}
+
+//***************************************************************************
+// Remove Pictures
+//***************************************************************************
+
+int cUpdate::cleanupPictures()
+{
+ const char* ext = ".jpg";
+ struct dirent* dirent;
+ DIR* dir;
+ char* pdir;
+ int iCount = 0;
+ int lCount = 0;
+
+ imageRefDb->countWhere("", iCount);
+
+ if (iCount < 100) // less than 100 image lines are suspicious ;)
+ {
+ tell(0, "Exit image cleanup to avoid deleting of all images on empty imagerefs table");
+ return done;
+ }
+
+ // -----------------------
+ // remove unused images
+
+ tell(1, "Starting cleanup of images in '%s'", epgimagedir);
+
+ // -----------------------
+ // cleanup 'images' directory
+
+ cDbStatement* stmt = new cDbStatement(imageRefDb);
+
+ stmt->build("select ");
+ stmt->bind("FileRef", cDBS::bndOut);
+ stmt->build(" from %s where ", imageRefDb->TableName());
+ stmt->bind("ImgName", cDBS::bndIn | cDBS::bndSet);
+
+ if (stmt->prepare() != success)
+ {
+ delete stmt;
+ return fail;
+ }
+
+ iCount = 0;
+
+ // open directory
+
+ asprintf(&pdir, "%s/images", epgimagedir);
+
+ if (!(dir = opendir(pdir)))
+ {
+ tell(1, "Can't open directory '%s', '%s'", pdir, strerror(errno));
+
+ free(pdir);
+
+ return done;
+ }
+
+ free(pdir);
+
+ while (dbConnected() && (dirent = readdir(dir)))
+ {
+ // check extension
+
+ if (strncmp(dirent->d_name + strlen(dirent->d_name) - strlen(ext), ext, strlen(ext)) != 0)
+ continue;
+
+ imageRefDb->clear();
+ imageRefDb->setValue("ImgName", dirent->d_name);
+
+ if (!stmt->find())
+ {
+ asprintf(&pdir, "%s/images/%s", epgimagedir, dirent->d_name);
+
+ if (!removeFile(pdir))
+ iCount++;
+
+ free(pdir);
+ }
+
+ stmt->freeResult();
+ }
+
+ delete stmt;
+ closedir(dir);
+
+ if (!dbConnected(yes))
+ return fail;
+
+ // -----------------------
+ // remove wasted symlinks
+
+ if (!(dir = opendir(epgimagedir)))
+ {
+ tell(1, "Can't open directory '%s', '%s'", epgimagedir, strerror(errno));
+ return done;
+ }
+
+ tell(1, "Remove %s symlinks", fullreload ? "all" : "old");
+
+ while ((dirent = readdir(dir)))
+ {
+ // check extension
+
+ if (strncmp(dirent->d_name + strlen(dirent->d_name) - strlen(ext), ext, strlen(ext)) != 0)
+ continue;
+
+ asprintf(&pdir, "%s/%s", epgimagedir, dirent->d_name);
+
+ // fileExists use access() which dereference links!
+
+ if (isLink(pdir) && (fullreload || !fileExists(pdir)))
+ {
+ if (!removeFile(pdir))
+ lCount++;
+ }
+
+ free(pdir);
+ }
+
+ closedir(dir);
+ tell(1, "Cleanup finished, removed (%d) images and (%d) symlinks", iCount, lCount);
+
+ return success;
+}
+
+//***************************************************************************
+// Link Needed
+//***************************************************************************
+
+int cUpdate::pictureLinkNeeded(const char* linkName)
+{
+ int found;
+
+ if (!dbConnected())
+ return yes;
+
+ // we don't need to patch the linkname "123456_0.jpg"
+ // since atoi() stops at the first non numerical character ...
+
+ imageRefDb->clear();
+ imageRefDb->setValue("LFN", 0L);
+ imageRefDb->setBigintValue("EVENTID", atol(linkName));
+
+ found = imageRefDb->find();
+ imageRefDb->reset();
+
+ return found;
+}
diff --git a/update.h b/update.h
new file mode 100644
index 0000000..3e4f8e6
--- /dev/null
+++ b/update.h
@@ -0,0 +1,280 @@
+/*
+ * update.h: EPG2VDR plugin for the Video Disk Recorder
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ */
+
+#ifndef __UPDATE_H
+#define __UPDATE_H
+
+#include <mysql/mysql.h>
+#include <queue>
+
+#define __STL_CONFIG_H
+
+#include <vdr/status.h>
+
+#include "lib/common.h"
+#include "lib/db.h"
+#include "lib/epgservice.h"
+
+#include "epg2vdr.h"
+#include "parameters.h"
+
+#define EPGDNAME "epgd"
+
+//***************************************************************************
+// Running Recording
+//***************************************************************************
+
+class cRunningRecording : public cListObject
+{
+ public:
+
+ cRunningRecording(const cTimer* t, long did = na)
+ {
+ timer = t;
+ doneid = did;
+ lastBreak = 0;
+ info = 0;
+
+ finished = no;
+ failed = no;
+
+ // copy until timer get waste ..
+
+ aux = strdup(timer->Aux() ? timer->Aux() : "");
+ }
+
+ ~cRunningRecording()
+ {
+ free(aux);
+ free(info);
+ }
+
+ void setInfo(const char* i) { info = strdup(i); }
+
+ // data
+
+ const cTimer* timer; // #TODO, it's may be fatal to hold a pointer to a timer!
+ time_t lastBreak;
+ int finished;
+ int failed;
+ long doneid;
+ char* aux;
+ char* info;
+};
+
+//***************************************************************************
+// Event Details
+//***************************************************************************
+
+class cEventDetails
+{
+ public:
+
+ cEventDetails() { changes = 0; }
+ ~cEventDetails() { }
+
+ int getChanges() { return changes; }
+ void clearChanges() { changes = 0; }
+
+ void setValue(const char* name, const char* value);
+ void setValue(const char* name, int value);
+
+ int storeToFs(const char* path);
+ int loadFromFs(const char* path);
+ int updateByRow(cDbRow* row);
+ int updateToRow(cDbRow* row);
+
+ private:
+
+ int changes;
+ std::map<std::string, std::string> values;
+
+ static const char* fields[];
+};
+
+//***************************************************************************
+// Update
+//***************************************************************************
+
+class cUpdate : public cThread, public cStatus, public cParameters
+{
+ public:
+
+ enum MasterMode
+ {
+ mmAuto,
+ mmYes,
+ mmNo,
+
+ mmCount
+ };
+
+ cUpdate(cPluginEPG2VDR* aPlugin);
+ ~cUpdate();
+
+ // interface
+
+ int init();
+ int exit();
+
+ void Stop();
+ int isEpgdBusy() { return epgdBusy; }
+ time_t getNextEpgdUpdateAt() { return nextEpgdUpdateAt; }
+ void triggerDbReconnect();
+ int triggerEpgUpdate(int reload = no);
+ int triggerRecUpdate();
+ int commonRecFolderOptionChanged();
+ int triggerStoreInfoFiles();
+ void triggerTimerJobs();
+ void epgdStateChange(const char* state);
+
+ protected:
+
+ // notifications from VDRs status interface
+
+ virtual void TimerChange(const cTimer* Timer, eTimerChange Change);
+ virtual void Recording(const cDevice *Device, const char *Name, const char *FileName, bool On);
+ virtual void ChannelSwitch(const cDevice *Device, int ChannelNumber, bool LiveView);
+
+ private:
+
+ struct TimerId
+ {
+ unsigned int eventId;
+ char channelId[100];
+ };
+
+ // functions
+
+ int initDb();
+ int exitDb();
+
+ void Action(void);
+ int isHandlerMaster();
+ void updateVdrData();
+ int updateRecFolderOption();
+ int dbConnected(int force = no)
+ { return connection && connection->isConnected() && (!force || connection->check() == success); }
+ int checkConnection(int& timeout);
+
+ int refreshEpg(const char* channelid = 0, int maxTries = 5);
+ cEvent* createEventFromRow(const cDbRow* row);
+ int lookupVdrEventOf(int eId, const char* cId);
+ int storePicturesToFs();
+ int cleanupPictures();
+ int pictureLinkNeeded(const char* linkName);
+
+ tChannelID toChanID(const char* chanIdStr)
+ {
+ if (isEmpty(chanIdStr))
+ return tChannelID::InvalidID;
+
+ return tChannelID::FromString(chanIdStr);
+ }
+
+ // timer stuff
+
+ int updateTimerTable();
+ int performTimerJobs();
+ int recordingChanged();
+ int updateTimerDone(int timerid, int doneid, char state);
+ int timerChanged();
+
+ // recording stuff
+
+ int updateRecordingTable(int fullReload = no);
+ int cleanupDeletedRecordings(int force = no);
+ int updateRecordingDirectory(const cRecording* recording);
+ int updatePendingRecordingInfoFiles(const cRecordings* recordings);
+ int storeAllRecordingInfoFiles();
+ int updateRecordingInfoFiles();
+
+ // data
+
+ cDbConnection* connection;
+ cPluginEPG2VDR* plugin;
+ int handlerMaster;
+ int loopActive;
+ time_t nextEpgdUpdateAt;
+ time_t lastUpdateAt;
+ time_t lastEventsUpdateAt;
+ time_t lastRecordingDeleteAt;
+ int lastRecordingCount;
+ char* epgimagedir;
+ int withutf8;
+ cCondVar waitCondition;
+ cMutex mutex;
+ int fullreload;
+ char imageExtension[3+TB];
+
+ cMutex timerMutex;
+ int dbReconnectTriggered;
+ int timerJobsUpdateTriggered;
+ int timerTableUpdateTriggered;
+ int manualTrigger;
+ int recordingStateChangedTrigger;
+ int recordingFullReloadTrigger;
+ int storeAllRecordingInfoFilesTrigger;
+ int updateRecFolderOptionTrigger;
+ cList<cRunningRecording> runningRecordings;
+ cMutex runningRecMutex;
+
+ Es::State epgdState;
+ int epgdBusy;
+ int eventsPending;
+ int mainActPending;
+ const char* videoBasePath;
+ int timersTableMaxUpdsp;
+
+ cDbTable* eventsDb;
+ cDbTable* useeventsDb;
+ cDbTable* fileDb;
+ cDbTable* imageDb;
+ cDbTable* imageRefDb;
+ cDbTable* episodeDb;
+ cDbTable* mapDb;
+ cDbTable* timerDb;
+ cDbTable* timerDoneDb;
+ cDbTable* vdrDb;
+ cDbTable* compDb;
+ cDbTable* recordingDirDb;
+ cDbTable* recordingListDb;
+
+ cDbStatement* selectMasterVdr;
+ cDbStatement* selectAllImages;
+ cDbStatement* selectUpdEvents;
+ cDbStatement* selectEventById;
+ cDbStatement* selectAllChannels;
+ cDbStatement* selectChannelById;
+ cDbStatement* markUnknownChannel;
+ cDbStatement* selectComponentsOf;
+ cDbStatement* deleteTimer;
+ cDbStatement* selectMyTimer;
+ cDbStatement* selectRecordings;
+ cDbStatement* selectRecForInfoUpdate;
+ cDbStatement* selectPendingTimerActions;
+ cDbStatement* selectTimerByEvent;
+ cDbStatement* selectTimerById;
+ cDbStatement* selectTimerByDoneId;
+ cDbStatement* selectMaxUpdSp;
+
+ cDbValue vdrEvtId;
+ cDbValue extEvtId;
+ cDbValue vdrStartTime;
+ cDbValue extChannelId;
+ cDbValue imageUpdSp;
+ cDbValue imageSize;
+ cDbValue masterId;
+
+ cDbValue* dvbDescription;
+
+ std::queue<std::string> pendingNewRecordings; // recordings to store details
+ std::vector<TimerId> deletedTimers;
+};
+
+//***************************************************************************
+#endif //__UPDATE_H