summaryrefslogtreecommitdiff
path: root/muggle-plugin
diff options
context:
space:
mode:
Diffstat (limited to 'muggle-plugin')
-rw-r--r--muggle-plugin/COPYING340
-rw-r--r--muggle-plugin/HISTORY186
-rw-r--r--muggle-plugin/Makefile128
-rw-r--r--muggle-plugin/README385
-rw-r--r--muggle-plugin/TODO237
-rw-r--r--muggle-plugin/i18n.c1257
-rw-r--r--muggle-plugin/i18n.h16
-rw-r--r--muggle-plugin/menu.txt73
-rw-r--r--muggle-plugin/mg_content.c264
-rw-r--r--muggle-plugin/mg_content.h115
-rw-r--r--muggle-plugin/mg_mysql.c586
-rw-r--r--muggle-plugin/mg_mysql.h76
-rw-r--r--muggle-plugin/mg_order.c1090
-rw-r--r--muggle-plugin/mg_order.h192
-rw-r--r--muggle-plugin/mg_selection.c1149
-rw-r--r--muggle-plugin/mg_selection.h468
-rw-r--r--muggle-plugin/mg_setup.c37
-rw-r--r--muggle-plugin/mg_setup.h63
-rw-r--r--muggle-plugin/mg_sync.c305
-rw-r--r--muggle-plugin/mg_sync.h73
-rw-r--r--muggle-plugin/mg_thread_sync.c63
-rw-r--r--muggle-plugin/mg_thread_sync.h39
-rw-r--r--muggle-plugin/mg_tools.c149
-rw-r--r--muggle-plugin/mg_tools.h89
-rw-r--r--muggle-plugin/mg_valmap.c70
-rw-r--r--muggle-plugin/mg_valmap.h52
-rw-r--r--muggle-plugin/muggle.c283
-rw-r--r--muggle-plugin/muggle.doxygen1078
-rw-r--r--muggle-plugin/muggle.h73
-rwxr-xr-xmuggle-plugin/mugglei.c180
-rw-r--r--muggle-plugin/mugglei.h0
-rw-r--r--muggle-plugin/scripts/COPYRIGHT17
-rwxr-xr-xmuggle-plugin/scripts/createdb.mysql2
-rwxr-xr-xmuggle-plugin/scripts/createtables.mysql2
-rwxr-xr-xmuggle-plugin/scripts/genres.txt274
-rwxr-xr-xmuggle-plugin/scripts/gentables47
-rwxr-xr-xmuggle-plugin/scripts/iso639tab.py81
-rw-r--r--muggle-plugin/scripts/iso_639.xml2072
-rw-r--r--muggle-plugin/scripts/languages.txt467
-rwxr-xr-xmuggle-plugin/scripts/make-empty-db4
-rwxr-xr-xmuggle-plugin/scripts/musictypes.txt4
-rwxr-xr-xmuggle-plugin/scripts/sources.txt6
-rw-r--r--muggle-plugin/stylesheet.css115
-rw-r--r--muggle-plugin/vdr_actions.c1414
-rw-r--r--muggle-plugin/vdr_actions.h188
-rw-r--r--muggle-plugin/vdr_config.h118
-rw-r--r--muggle-plugin/vdr_decoder.c217
-rw-r--r--muggle-plugin/vdr_decoder.h170
-rw-r--r--muggle-plugin/vdr_decoder_flac.c363
-rw-r--r--muggle-plugin/vdr_decoder_flac.h80
-rw-r--r--muggle-plugin/vdr_decoder_mp3.c464
-rw-r--r--muggle-plugin/vdr_decoder_mp3.h114
-rw-r--r--muggle-plugin/vdr_decoder_ogg.c461
-rw-r--r--muggle-plugin/vdr_decoder_ogg.h63
-rw-r--r--muggle-plugin/vdr_menu.c1053
-rw-r--r--muggle-plugin/vdr_menu.h413
-rw-r--r--muggle-plugin/vdr_network.h47
-rw-r--r--muggle-plugin/vdr_player.c1808
-rw-r--r--muggle-plugin/vdr_player.h149
-rw-r--r--muggle-plugin/vdr_setup.c87
-rw-r--r--muggle-plugin/vdr_setup.h38
-rw-r--r--muggle-plugin/vdr_sound.c753
-rw-r--r--muggle-plugin/vdr_stream.c351
-rw-r--r--muggle-plugin/vdr_stream.h60
64 files changed, 20618 insertions, 0 deletions
diff --git a/muggle-plugin/COPYING b/muggle-plugin/COPYING
new file mode 100644
index 0000000..5b6e7c6
--- /dev/null
+++ b/muggle-plugin/COPYING
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <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 Library General
+Public License instead of this License.
diff --git a/muggle-plugin/HISTORY b/muggle-plugin/HISTORY
new file mode 100644
index 0000000..83071c1
--- /dev/null
+++ b/muggle-plugin/HISTORY
@@ -0,0 +1,186 @@
+VDR Plugin 'muggle' Revision History
+------------------------------------
+
+2004-08-31: Version 0.0.1-ALPHA
+- An initial revision given to a few people.
+
+2004-09-05: Version 0.0.2-ALPHA
+- Added an Ogg Vorbis decoder
+
+XXXXXXXXXX: Version 0.0.5-ALPHA
+- Support für g++ 2.95.4
+- Support für Sockets (statt TCP)
+- Kleinere Fehlerchen beseitigt
+
+XXXXXXXXXX: Version 0.0.7-ALPHA
+- Doppelter Import von Files bei erneutem Aufruf von mugglei
+ beseitigt, bei erneutem Aufruf wird stattdessen die DB
+ aus den Tags upgedated.
+- Compilerwarnings beseitigt
+- Anzeige auf dem gLCD
+- Menüanzeige bei aktiviertem Progressdisplay funktioniert
+- Verbessertes Progress Display beim Abspielen (Track/Playlist, Progress/Detail), nur für 1.3.12
+- Import von genre-Tags
+- Instant play funktioniert
+- Starten einer Playlist von irgendwo mittels Ok
+
+XXXXXXXXXX: Version 0.0.8-ALPHA
+- Beim import werden bei bereits vorhandene Files nur die DB-Einträge erneuert,
+ keine Duplikate mehr.
+- mugglei mit der Option -z löscht Datenbankeinträge, bei denen die verwiesene
+ Datei nicht existiert.
+- Bug in mugglei entfernt, der verhinderte, dass neue Dateien korrekt
+ eingetragen werden.
+- Ein Bug beim Skippen der Tracks am Ende der Playliste entfernt, der letzte
+ Track wurde außerdem für immer wiederholt.
+- Französische Übersetzung. Merci a Patrice!
+
+2005-01-07: Version 0.1.0-BETA
+- der Begriff Playlist ist weggefallen. Neu gibt es Sammlungen.
+ Wichtig ist vor allem die Sammlung "spielen". Was man an diese
+ Sammlung anhängt, wird eines nach dem anderen gespielt.
+- Wenn man etwas an eine Sammlung anhängt, muss man vorher sagen,
+ welche das sein soll. Beim ersten Start von muggle ist das "spielen".
+ Man kann die "Zielsammlung" ändern, indem man in der Sammlungsliste
+ die richtige auswahlt, dann den blauen Knopf "Befehle" nimmt und
+ dann "Ziel auf Sammlung .... setzen" macht.
+- man kann direkt in der Sammlungsliste auch neue anlegen.
+- die gelbe Taste schaltet zwischen Sammlungen und Suche um. Die
+ Befehle sind an beiden Stellen etwa dieselben, man muss also
+ z.B. nicht extra eine Sammlung anlegen, um eine Playlist (*.mru) zu exportieren.
+- Taste OK auf einem Track spielt ihn sofort. Wenn er zu Ende ist oder
+ man mit Stop abbricht, und wenn vorher etwasaus "spielen" lief,
+ wird dort weitergemacht. Ein zweiter Druck auf Taste Stop beendet
+ auch das Abspielen von "spielen".
+- Der Befehl "sofort spielen" macht dasselbe wie OK auf einem Track,
+ aber für alle Tracks, die hinter dem gewähltenListeneintrag stecken,
+ z.B. alles von Abba.
+- nachdem man Musik gestartet hat, bleibt das muggle - Menu stehen.
+ Damit und mit dem OK - Anspielen kann man sehr schnell alle Tracks
+ kurz anspielen.
+- auch während das muggle - Menu sichtbar ist, funktionieren nun die
+ Tasten Stop, Play, Pause.
+- wenn man muggle verlässt (am besten mit der Menu - Taste), wird der
+ Status gespeichert. Wenn man muggle wieder aufruft, ist man am gleichen Ort.
+- beim Start von muggle kommt man direkt in das aktuelle Suchschema.
+ Dieses kann man neu ändern, indem man Taste "Befehle" nimmt, dann
+ Menu "Suchschema".
+- Wenn man irgendwo mitten im Suchbaum ist und das Suchschema wechselt,
+ verwendet muggle die schon bekannten Schlüsselfelder, um wieder
+ möglichst weit in den Suchbaum hineinzugehen.
+- Filter gibt es erstmal nicht mehr, da wird aber sicher wieder etwas kommen.
+- Man kann die Tasten rot, grün, gelb frei belegen, indem man unter
+ "Befehle" auf einen Befehl geht und dann die gewünschte Farbtaste drückt.
+ Das funktioniert auch für extern definierte Befehle.
+- Datenbankabfragen sind z.T. deutlich schneller
+
+2005-01-23: Version 0.1.1-BETA
+- FLAC decoder added
+- Compiles with VDR 1.3.18 (mileage may vary)
+- Works with VDR 1.2.6
+- Selections can now be chosen when executing add/remove
+- GD compatibility added
+- Many bugfixes and usability improvements
+
+- Die Organisation der Dateien kann nun vom Benutzer verändert werden.
+ Zudem können neue Bäume erstellt werden (zB mag ich
+ Decade > Genre > Track sehr gern).
+- m3u - Dateien werden nun immer in /tmp mit relativen Dateinamen erstellt.
+ Externe Befehle werden im top level directory der tracks aufgerufen
+ (vorangehendes chdir).
+- m3u - Dateien enthalten zusätzlich eine Kennung #MUGGLE:XXX
+ wobei XXX die tracks.id des Stücks ist. Somit können Kommandos auch
+ Befehle auf der Datenbank durchführen (zB Löschen eines Tracks).
+ Muggle stellt die OSD-Ansicht danach neu dar, um Änderungen anzuzeigen.
+- Blättern in Genre-Hierarchien ist neu. Das Feld Genre nutzt wie
+ bisher eine flache Genre-Liste. Die neuen Felder Genre1, Genre2, Genre3
+ definieren Ebenen im Suchbaum aus der Genre-Hierarchie.
+- Die Sprache wird aus id3v2-Tags importiert (für mp3 und flac)
+- Musikstücke können nach Sprache gebrowsed werden.
+- Hat ein Track 2 Genres (in den Feldern genre1 und genre2), so
+ erscheint es in Kategorien für beide Genres. Allerdings wird das zweite
+ Genre derzeit beim Import nicht berücksichtigt.
+- Wichtige Meldungen erscheinen nun auch im OSD (nicht mehr nur im Syslog)
+- Läuft mit allen Versionen inkl. 1.3.20
+- Player schaltet nach Ende der Playlist stumm (kein TV-Gedröhne mehr)
+- Decoder für Ogg und FLAC können nach Defines in make.config gebaut werden
+- Bugfixes und sonstige Verbesserungen
+
+2005-02-20: Version 0.1.3-BETA
+- das deutsche VDR - Wiki enthält zu muggle einen Abschnitt "Bedienung".
+ Vielleicht findet sich ja jemand, der da etwas zu schreibt?
+ Ich stehe gerne bei Fragen zur Verfügung.
+- Man kann nun nach Ordnern/Verzeichnissen sortieren. Bis zu 4 Stufen
+ sind möglich. Man muss alle Tracks mit mugglei neu importieren, damit
+ das geht. Wenn mugglei nicht die Berechtigung hat, neue Felder in der
+ Tabelle tracks anzulegen, bleibt alles wie bisher. In diesem Fall
+ müsste man entweder für die nötigen Rechte sorgen oder mit den Scripts
+ die ganze Datenbank neu anlegen.
+- Hinter den Listeneinträgen steht nun, wieviele Tracks das jeweilen sind.
+ Dank geht an jarny für seine Hilfe zu SQL.
+- Die Sprachcodes werden nun vom Standard ISO 639-2/B (bibliographic)
+ genommen, wie in den id3v2 Tags. Das betrifft nur den Import, die
+ Kompatibilität zu GiantDisc bleibt.
+- Die Setup - Einstellungen loop mode und shuffle mode werden nun
+ berücksichtigt.
+- mugglei erklärt jetzt besser, warum er etwas nicht importieren kann.
+- wenn die Datei muggle.state nicht schreibbar ist, warnt muggle einmal.
+ (Die Datei muggle.state speichert den Status (z.B. Sortierungen,
+ Position, Farbknopfbelegung)
+- Einige Fehler korrigiert, vor allem Memory leaks (die meisten mit
+ valgrind gefunden). Sollte nun auch (wieder) mit g++ 2.95 compilieren.
+- Wer eine ältere Version von mysql benutzt, z.B. 3.23, wird mugglei nicht
+ kompilieren können. Die Fixes sollten einfach sein, evtl reicht es,
+ die Aufrufe mysql_server_init/end zu entfernen. Das README hat schon
+ immer mindestens 4.0.18 empfohlen.
+
+2005-03-06: Version 0.1.4-BETA
+- embedded mysql server as default. If you want to use an external server
+ as before, read the README file
+- If the plugin finds the database missing, it offers to create
+ it. It will then also import all from the top level music directory
+ (as indicated with option -t)
+- new option -v for the plugin and mugglei sets the output debug level.
+- rewrote mugglei. Mainly it now recursively imports directories making
+ it much faster. This example will create a new database and import all
+ files from /Musik : mugglei -t /Musik -v 4 -c .
+- removed the mugglei -f option. All arguments after the options will be
+ imported.
+- removed the mugglei -a option. It should now automatically do the right
+ thing
+- orders can now be displayed in the default order or descending by their
+ counts
+- orders can now be a combination of order by collection and other key
+ fields. Known bug: If the collection is not the first key field like
+ in Genre:Collection:Album:Track, the counts are wrong in the top list
+- make additions to playlists multi user safe
+- rename Search to Browse/Navigieren
+- when creating the data base, all ISO 639-2/B codes will be imported,
+ updated the list.
+- the language names can appear in the local language if the translations
+ exist (debian: Package iso-codes)
+- add all genres listed by id3 -L. Fix spellings.
+- lots of bug fixes, as usual
+
+2005-03-11: Version 0.1.5-BETA
+- add include files like stdio.h, needed in some environments
+- fix genre import
+- if you have mysql embedded 4.1.11 or better, you can access embedded
+ and external data bases with the same binary. If you omit the -h
+ parameter, embedded is used. Without embedded support compiled in,
+ the default for -h is still localhost
+- renamed the Makefile conditional HAVE_SERVER to HAVE_ONLY_SERVER. This
+ better reflects the new functionality.
+- if you want to connect to the local server using sockets, you now
+ do that with -h localhost. Up to now you had to specify -s. This is
+ no longer needed. This better reflects the mysql C API. As a con-
+ sequence, up to now not giving any argument to muggle called the
+ server on localhost using TCP, now it uses the faster sockets. You
+ can still request TCP by using -h 127.0.0.1
+
+2005-03-21: Version 0.1.6-BETA
+- killing vdr could still result in an empty muggle.state. Fixed.
+- new sorting fields: Only by the first character of artist or title
+- import now runs as a separate thread and no longer blocks user
+ input and VDR is no longer killed by the watchdog during import
+
diff --git a/muggle-plugin/Makefile b/muggle-plugin/Makefile
new file mode 100644
index 0000000..f1a691a
--- /dev/null
+++ b/muggle-plugin/Makefile
@@ -0,0 +1,128 @@
+#
+# Makefile for a Video Disk Recorder plugin
+#
+# $Id$
+
+# The official name of this plugin.
+# This name will be used in the '-P...' option of VDR to load the plugin.
+# By default the main source file also carries this name.
+#
+PLUGIN = muggle
+
+#if you want ogg / flac support, define HAVE_VORBISFILE and/or HAVE_FLAC
+#in $VDRDIR/Make.config like this:
+# HAVE_VORBISFILE=1
+# HAVE_FLAC=1
+
+#if you do not want to compile in code for embedded sql,
+#define this in $VDRDIR/Make.config:
+# HAVE_ONLY_SERVER=1
+
+### The version number of this plugin (taken from the main source file):
+
+VERSION = $(shell grep 'static const char \*VERSION *=' $(PLUGIN).c | awk '{ print $$6 }' | sed -e 's/[";]//g')
+
+### The C++ compiler and options:
+
+CXX ?= g++-3.3
+CXXFLAGS ?= -fPIC -O0 -Wall -Woverloaded-virtual -Wno-deprecated -g
+
+### The directory environment:
+
+DVBDIR = ../../../../DVB
+VDRDIR = ../../../
+# /usr/local/src/VDR
+LIBDIR = ../../lib
+TMPDIR = /tmp
+
+### Allow user defined options to overwrite defaults:
+
+-include $(VDRDIR)/Make.config
+
+### The version number of VDR (taken from VDR's "config.h"):
+
+VDRVERSION = $(shell grep 'define VDRVERSION ' $(VDRDIR)/config.h | awk '{ print $$3 }' | sed -e 's/"//g')
+
+### The name of the distribution archive:
+
+ARCHIVE = $(PLUGIN)-$(VERSION)
+PACKAGE = vdr-$(ARCHIVE)
+
+### Includes and Defines (add further entries here):
+
+INCLUDES += -I$(VDRDIR) -I$(VDRDIR)/include -I$(DVBDIR)/include \
+ $(shell mysql_config --cflags) $(shell taglib-config --cflags)
+
+DEFINES += -D_GNU_SOURCE -DPLUGIN_NAME_I18N='"$(PLUGIN)"' -DMYSQLCLIENTVERSION='"$(shell mysql_config --version)"'
+
+### The object files (add further files here):
+
+OBJS = $(PLUGIN).o i18n.o mg_valmap.o mg_mysql.o mg_sync.o mg_thread_sync.o mg_order.o mg_content.o mg_selection.o vdr_actions.o vdr_menu.o mg_tools.o \
+ vdr_decoder_mp3.o vdr_stream.o vdr_decoder.o vdr_player.o \
+ vdr_setup.o mg_setup.o
+
+LIBS = -lmad $(shell taglib-config --libs)
+MILIBS = $(shell taglib-config --libs)
+
+ifdef HAVE_ONLY_SERVER
+SQLLIBS = $(shell mysql_config --libs)
+DEFINES += -DHAVE_ONLY_SERVER
+else
+SQLLIBS = $(shell mysql_config --libmysqld-libs) -L/lib
+endif
+
+ifdef HAVE_VORBISFILE
+DEFINES += -DHAVE_VORBISFILE
+OBJS += vdr_decoder_ogg.o
+LIBS += -lvorbisfile -lvorbis
+endif
+ifdef HAVE_FLAC
+DEFINES += -DHAVE_FLAC
+OBJS += vdr_decoder_flac.o
+LIBS += -lFLAC++
+endif
+
+### Targets:
+
+all: libvdr-$(PLUGIN).so mugglei
+
+# Dependencies:
+
+MAKEDEP = $(CXX) -MM -MG
+DEPFILE = .dependencies
+$(DEPFILE): Makefile
+ @$(MAKEDEP) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@
+
+-include $(DEPFILE)
+
+### Implicit rules:
+
+%.o: %.c %.h
+ $(CXX) $(CXXFLAGS) $(DEFINES) $(INCLUDES) -c $<
+
+mg_tables.h: scripts/genres.txt scripts/iso_639.xml scripts/musictypes.txt scripts/sources.txt
+ scripts/gentables
+
+libvdr-$(PLUGIN).so: $(OBJS)
+ $(CXX) $(CXXFLAGS) -shared $(OBJS) $(LIBS) $(SQLLIBS) -o $@
+ @cp $@ $(LIBDIR)/$@.$(VDRVERSION)
+
+mugglei: mg_tools.o mugglei.o mg_sync.o mg_mysql.o mg_setup.o
+ $(CXX) $(CXXFLAGS) $^ $(MILIBS) $(SQLLIBS) -o $@
+
+install:
+ @cp ../../lib/libvdr-muggle*.so.* /usr/lib/vdr/
+ @cp mugglei /usr/local/bin/
+# @install -m 755 mugglei /usr/local/bin/
+
+dist: clean mg_tables.h
+ @-rm -rf $(TMPDIR)/$(ARCHIVE)
+ @mkdir $(TMPDIR)/$(ARCHIVE)
+ @cp -a * $(TMPDIR)/$(ARCHIVE)
+ @tar czf $(PACKAGE).tgz --exclude=.svn/* -C $(TMPDIR) $(ARCHIVE)
+ @-rm -rf $(TMPDIR)/$(ARCHIVE)
+ @echo Distribution package created as $(PACKAGE).tgz
+
+clean:
+ @-rm -f $(OBJS) $(BINOBJS) $(DEPFILE) *.so *.tgz core* *~ mugglei.o mugglei
+
diff --git a/muggle-plugin/README b/muggle-plugin/README
new file mode 100644
index 0000000..67004ec
--- /dev/null
+++ b/muggle-plugin/README
@@ -0,0 +1,385 @@
+/*! \mainpage Muggle: Media Juggler for VDR
+
+This is a plugin for the Video Disk Recorder (VDR).
+
+Written by: Andi Kellner,
+ Lars von Wedel <vonwedel@web.de>,
+ Ralf Klueber <r@lf-klueber.de>,
+ Wolfgang Rohdewald <wolfgang@rohdewald.de>
+
+Project's homepage: http://www.htpc-tech.de/htpc/muggle.htm
+
+Latest version available at: http://www.htpc-tech.de/htpc/muggle-dev.htm
+
+See the file COPYING for license information.
+
+\section foreword PLEASE!
+
+This is a difficult plugin. It's nice but difficult.
+With difficult I mean, that due to the underlying
+database, many more sources of error can occur as
+opposed to other plugins.
+
+Take some time to carefully read these instructions.
+Please provide feedback to the authors whenever you
+think, these instructions are not appropriate, wrong,
+or useless in any other sense.
+
+\section desc DESCRIPTION
+
+The muggle plugin provides a database link for VDR so that selection of media becomes more flexible.
+Prerequisites are describedin Section 2, Notes on Compilation are in Section 3. Before using the plugin,
+you need to import your media into the database (cf. Section 4). The configuration of VDR and startup
+parameters are descibed in Section 5.
+
+\section prereq PREREQUISITES
+
+The plugin currently runs on versions 1.3.17- of VDR (including 1.2.6). It also compiles on 1.3.18
+but your mileage may vary. In addition, the following pieces of software are required:
+
+ - mySQL client libraries
+ (Debian package libmysqlclient-dev or
+ http://www.mysql.org)
+ - mySQL server (tested with 4.0.18) (Debian packages mysql-server, mysql-client)
+ only needed, if you want to run the database server on a remote machine
+ - libmad (for mp3 decoding)
+ (Debian package libmad0-dev or
+ http://www.underbit.com/products/mad/)
+ - libtag (for ID3 tag reading/writing)
+ (Debian package libtag1-dev or
+ http://developer.kde.org/~wheeler/taglib.html)
+ - optionally libvorbis and libvorbisfile to replay OGG Vorbis files
+ (Debian packages libvorbis-dev or
+ http://www.xiph.org/ogg/vorbis/)
+ - optionally libFLAC++ to replay FLAC files
+ (Debian package libflac++-dev or sources from flac.sourceforge.net)
+The developer versions are needed because their headers are required for compilation.
+The server need not be on the same machine as the VDR. Also, music tracks can reside somewhere else,
+if they are available through a remote filesystem (NFS, Samba). However, in this case you should
+know what you are doing in terms of networking and security issues.
+
+\section install INSTALLING
+
+Unpack the sources in PLUGINS/src below your VDR directory (i.e. where all your other plugins are.
+For example (paths and version numbers may vary)
+
+\verbatim
+ cd /usr/local/src/VDR/PLUGINS/src
+ tar xvjf muggle-0.1.1.tgz
+\endverbatim
+
+Establish a symlink as you would for other plugins:
+
+\verbatim
+ ln -s muggle-0.1.1 muggle
+\endverbatim
+
+Adapt the Makefile to your system. Define HAVE_VORBIS and/or HAVE_FLAC and adapt
+the LIBS variable accordingly.
+
+NOTE: By default, muggle will be built using the embedded mysql library so that it is
+not required to install further packages or run additional services. If you want to use
+the remote server as in previous versions, you have to define the variable HAVE_SERVER
+in the Makefile by uncommenting the corresponding line.
+
+NOTE: If you have not installed the mysql server package on your machine the files
+containing error messages for MySQL you will see an error message like this:
+
+050306 9:29:14 Can't find messagefile '/usr/share/mysql/english/errmsg.sys'
+050306 9:29:14 Aborting
+
+In this case you need to obtain these files and put them there.
+
+Then, within the VDR main directory (e.g. /usr/local/src/VDR) issue
+
+\verbatim
+ make plugins
+\endverbatim
+
+This should build all relevant stuff. If you have difficulties, check that required libraries are
+in the library directories stated in the muggle Makefile.
+
+Note: On my Debian sarge system, I had difficulties because a proper symlink for libwrap.so was
+missing. Check this in case the compiler complains about a missing -lwrap.
+
+\section SET UP MUGGLE WITH EMBEDDED MYSQL
+
+The step of setting up the database and importing music has been simplified a lot
+for Muggle using the embedded MySQL server. When starting up muggle the first time
+it will determine that the database does not exist and will ask, whether to
+create the database. Confirm this with Ok.
+
+After successfully creating the database, muggle will query whether to import music.
+Confirm this question with Ok, too. Muggle will now recursively descend into the
+directories below the music directory specified with the -t option on the command line.
+Once muggle is running, you can import new tracks and read updated tags by a command
+in the setup menu. The use of mugglei is still possible, but only while VDR is not
+running (because muggle will then block the use of the database).
+
+NOTE: The embedded MySQL server cannot be used by other programs. The use of the
+GiantDisc web interface for example is not possible.
+
+\section SET UP MUGGLE WITH REMOTE MYSQL
+
+If you already have a MySQL server running in your network (e.g. as a basis
+for a webserver) or want to access the music database with other programs
+(e.g. the GiantDisc web interface) you may be interested in using
+
+This step can be done on the database server or on some other client machine.
+The scripts in the directory scripts is no longer needed. Instead, mugglei can
+now also create new databases and provide basic information about existing languages,
+genres etc. A small utility called 'mugglei' to administer the database has been
+created along with the plugin. Run
+
+\verbatim
+ mugglei -c
+\endverbatim
+
+to create a database and initialize the tables. This replaces the series of commands
+needed in former commands. If you want to change the server or database name look at
+the command line arguments (execute mugglei without arguments to see a list).
+
+\subsection importremote Import for Muggle with remote MySQL
+
+The next step is to feed all music information into the database. To accomplish this, mugglei
+connects to the database, evaluates ID3 tags from a file, and writes the tags into the
+database. Since release 0.1.4 mugglei recursively descends a file system hierarchy so that
+the use of find is no longer needed.
+
+For this step, it is helpful, that all music files are somehow gathered under a toplevel directory.
+It does not matter whether there are further subdirectories which organize files into genres, artists,
+album or whatever. If this is not the case, you may want to take some time to do this. Read on before
+you start. Executing
+
+\verbatim
+ mugglei *
+\endverbatim
+
+will import all music files (*.mp3., *.ogg, *.flac) below the current directory. Obviously,
+you may need additional options for the database host, user, etc.
+It is important that you perform various import steps from the same location so the
+filenames are relative to exactly the same directory (e.g. /home/music in the example case).
+
+NOTE: The options -f and -a are no longer needed. mugglei should now automatically do the right thing.
+
+If a track has no ID3 tags, the following defaults will be applied:
+
+- Title: Filename will be used
+- Artist: "Unknown"
+- Album: "Unassigned"
+- Track: 0
+- Year: 0
+
+\section config MUGGLE CONFIGURATION
+
+In case you use mugglei with the embedded MySQL server the most important
+options are -d DIR (controls where the database file is created) and
+-t DIR (controls where the music resides).
+
+When using the remote MySQL server, muggle uses a small set of command line
+parameters in order to control the interaction with the mySQL server. Let's
+look at an example:
+
+\verbatim
+ -P'muggle -h localhost -u vdr -n GiantDisc -t/home/music'
+\endverbatim
+
+The -h parameter specifies the database host, -u specifies the user,
+-n is the database name. The scripts mentioned above do not make use
+of passwords, but restrict database acccess on a server basis.
+
+The -t argument specifies the top level directory of the music files.
+On a local installation, this is the directory in which you executed the
+import steps (Chapter 4.2).
+
+In case you want to use Muggle with the embedded MySQL server, specify the
+directory to place the database into with the option -d DIR.
+
+\section quickuse QUICK INTRO
+
+Quick version: select Muggle on the OSD, browse titles (using up/down and Ok),
+add them using the red button. Music will start instantly while you can continue
+to browse and add tracks.
+
+During playback, Up/Down jumps forth and back in the current playlist. Yellow
+toggles play/pause mode and Ok toggles a display of the replay process. Using
+Green, the display can be switched between playlist and single display mode,
+Red toggles info and progress view. For VDR 1.3.6- the progress display is
+"quite simple", unfortunately.
+
+\section use DETAILED USER'S GUIDE
+
+The core concept of the Muggle user interface is called a *selection*. That is,
+as the name suggests, a selection of music tracks. Note, that a selection can be
+as small as a single track (a very simple selection, indeed) or as large as the
+whole music library.
+
+Selections are used to structure all tracks (the music library) into sets (e.g.
+a selection of all tracks by an author) and subsets (e.g. the tracks of an author
+on a certain album). Such selections are built by means of keys (e.g. author
+or album) defined in the database and are displayed in the *music browser*. The
+current selection in the *music browser* contains all tracks defined by the line
+the cursor is on. So if you place the cursor on the line "Pop", all tracks with
+Genre Pop are selected. If you then enter Pop and go to the line "Beatles", you
+narrow your selection to pop songs from the beatles.
+
+A collection is a special selection. Collections can be defined by the user, and
+he can add or remove any selection to / from a collection. A collection has only
+one order: a number which is incremented for every added track. Otherwise, since
+a collection is also a selection, everything that is valid for selections also
+holds for collections.
+
+Collections can be defined by the user in the sense of a playlist. This is done by
+adding/removing selections to/from the *default collection*.
+
+Changing the contents of a collection changes them directly in the data base. Saving
+or loading collections is not needed.
+
+An important term while working with Muggle is the *default collection*. This
+is a special collection which is the target of commands working on collections.
+Whenever you add selections to somewhere, they will be added to the default
+collection. The same happens when you remove selections.
+
+Another important collection is the 'play' collection. This is a temporary collection.
+Whatever is added to it will be played in that order. If you add something while muggle
+is not playing anything, this collection will first be emptied. However 'temporary' does
+not mean that its content is not saved to the data base.
+
+Starting from release 0.1.1 Muggle can be also used without default playlists. There are
+new menu entries "Add X to collection" and "Remove X from collection" which show a list
+of all collections to choose from. The concept of a default collection still exists and
+both approaches can be used in common. However, you can spceify which commands to use for
+the special keys Red/Green/Blueas you like.
+
+\subsection general General remarks
+
+There are two main views in Muggle, the *Music browser* view and the *Collection browser*
+view. You can toggle between them using the Yellow key by default, however the key binding
+can be changed.
+
+Each of the two views has associated commands. To show a summary of the commands available
+for the current view press the blue key. Note, that the red, green and yellow keys do not
+have a fixed meaning. Rather, while the commands for a certain view are displayed, you can
+press red/green/yellow to make the respective key execute the command currently selected
+(highlighted) by the cursor. The commands you choose for red/green/yellow will be saved for
+the next time you start muggle. You can define different commands in both view *Music browser*
+and *Collection browser*.
+
+\subsection browse Music browser
+
+By default, Muggle starts in the *Music browser* display at the place where you left it
+last time. This browser displays the music library according to a search order, e.g.
+according to artists / albums / tracks or genre / year / track. These search orders are
+currently fixed in the code, but the objective is to make them editable by the user on the
+OSD. Browsing these search orders is done using Up/Down/Left/Right keys. To display the
+contents of a currently selected selection, press Ok. To return to the parent selection
+press Back.
+
+A set of commands can be displayed with the Blue key on the remote control. A new menu
+will open and show the commands explained below. Remember that pressing Red, Green or
+Yellow will make these keys execute the command currently highlighted by the cursor
+from now on.
+
+Those commands are currently available in the *music browser*:
+
+- Instant Play: instantly play the current selection. This does not enter any collection.
+
+- Add to 'play': add the current selection to the default collection. After the first
+start of Muggle, the default collection is 'play'
+
+- Remove from 'play': remove the current selection from the default collection. If
+there are more than one instances of a specific track in the collection, they are all
+removed.
+
+- Collections: switch to the collection view
+
+- Select an order: select another search order, edit existing ones, or create new ones (see below)
+
+- Export tracklist: generate a file X.m3u containing all tracks from the current selection
+
+- External commands: whatever you define
+
+By default, the red key adds the currently selected collection to the default collection.
+The green key instantly plays the currently selected collection. The yellow key toggles
+between the *Music browser* and the *Collection browser*. Thus, if you want to play an
+album, browse to it and press green. Remember that you can redefine commands executed by
+Red, Green and Yellow by pressing them while displaying the command list.
+
+Muggle comes with a few default browsing orders (like artist / album /track). Since release 0.1.2
+it is possible for the user to change these or create now ones without going into the code.
+In the music browser submenu (enter with blue while in the music browser) enter "Select an order".
+Existing search orders will be shown. Move the cursor to any of those and press the Red button to edit
+it. Each key of the current search order will be shown on a line. Move the cursor to a line and
+change the search key using Left/Right buttons. Note, that the number of key depends on what is
+currently selected. So keys may appear/vanish as you cycle through the choices. This is intented
+and not a bug. Play around with this a while to see, why this is necessary. Press Ok to make your
+choices persistent, use back to leave the search order editor without making any changes.
+
+In addition, you can create new search orders using the Green key and delete orders no longer
+needed using Yellow. As an exercise, try to e.g. create orders like "Decade > Genre > Track" or
+"Year > Album > Track".
+
+\subsection collections Collection browser
+
+The *Collection browser* displays a list of available collections. Browse the list with
+Up/Down and display the collection contents with Ok. Returning to the collection list
+is done by pressing Back. One of the collections (the one called "play" when you start
+up Muggle for the first time) is marked with a "->" in front of the name, meaning that
+it is the default collection. Whenever you add or remove selections, this default
+collection is the current target, meaning that selections will be added/removed
+to/from this collection.
+
+At the bottom of the list, the entry "Create collection" is displayed. Entering it with
+the right key will make the editor appear on the second half of the line and using the
+keys Up/Down/Left/Right you can enter the name of the new collection. Pressing Ok will
+terminate the editing process and add the new collection to the list.
+
+Just like with the *music browser*, a set of commands can be displayed with the Blue
+key on the remote control.
+
+Those commands are currently available in the list of collections. Depending on the
+current selection, not all of them are available:
+
+- Instant play: See *music browser*
+
+- Add to 'play': See *music browser*
+
+- Remove from 'play': See *music browser*. Not available when the cursor is on the default collection.
+
+- Remove all entries from 'play': Only available when the cursor is on the default collection.
+
+- Search: switch to the *music browser*
+
+- Set default collection to 'X': as it says.
+
+- Delete collection: Not available for the default collection and for the 'play' collection.
+
+- Export track list: See *music browser*
+
+- External commands: whatever you define
+
+Note that you cannot only add to/remove from collections in the *music browser*.
+Rather, also collections can be added/removed. The reason is that - as explained
+above - a collection is also a selection. So everything that can be done with
+selections can also be done with collections. An example: if you want to give a
+party, you could create a new collection "Party". Now, steer your cursor to the
+collection entitled "Lounge music" and select add. Then go to "Pop 80s" and add
+again. Finally, go to "Dance classics" and add. Now you have created a collection
+"Party" from three already existing collections. To continue this example, let us
+assume that one of your guests has a personal dislike against "Modern Talking".
+Switch to the browser view, go to the artist selection of "Modern Talking" and
+select "Remove". Now all tracks written by Modern Talking will be removed from
+your "Party" collection.
+
+Please note that "Remove" means removing from the default collection. "Delete" will
+delete a collection.
+
+It is possible that a collection holds the same track several times if you add it
+several times. However when you remove that track, all of its occurrences will be removed.
+
+The remote buttons Play, Pause, Stop are also supported while muggle displays its
+OSD. If Stop is pressed, muggle first stops playing what was started by Instant
+Play. Muggle will then continue playing the 'play' collection. A second Stop will
+stop playing the 'play' collection.
+
+*/
diff --git a/muggle-plugin/TODO b/muggle-plugin/TODO
new file mode 100644
index 0000000..6594daf
--- /dev/null
+++ b/muggle-plugin/TODO
@@ -0,0 +1,237 @@
+/*! \page issues Muggle Issue List
+
+ The page lists a number of open issues and possible ideas for improvement.
+ It can be seen as a notepad for the developers or as an entry point for
+ volunteers. There is no real order among those things and even the occurrence
+ of an issue does not mean that it will be implemented/resolved at some time.
+
+ If you feel, something is really urgent, go ahead. We'll help you.
+
+ \section urgent Urgent/Short-term issues
+
+ \subsection bugs Bugs and testing needed
+
+ \subsection urgentosd OSD-related Issues
+
+ \subsection urgentcode Code polishing
+
+ - Clean up coding style and documentation in general
+ - Logging
+ - extend mgLog with static logging methods
+ - in DEBUG mode, issue logs, warnings, errors to stderr
+ - otherwise issue errors only to syslog
+ - Check for unnecessary log commands
+ - Generate HTML documentation using doxygen,
+ - use dotty/gv for state machines of player
+ - make available online
+ - Clean up mugglei (abstract code where possible)
+ - extend mgSelection with all SQL code needed by mugglei.c, maybe something like mgSelection.SyncWithPath(const char *path, options)
+ - then remove all SQL code from mugglei.c
+ - Check for memory leaks
+ - Check for (reasonably) consistent usage of char pointers and strings
+ - mgPlayer used what for?
+ - Could save IP/host name and associate last playlist/index loaded
+
+ \subsection urgentcontent Content handling
+ - Save on exit
+ - Think, whether type (mp3, ogg, flac) should be stored in database
+ - could be used in searching/structuring as well
+ - Party mode (see iTunes)
+ - initialization
+ - find 15 titles according to the scheme below
+ - playing
+ - before entering next title perform track selection
+ - do not increment the playcount
+ - if more than 5 titles are found, make sure the same title is not directly repeated
+ - track selection
+ - generate a random uid
+ - if file exists:
+ - determine maximum playcount of all tracks
+ - generate a random number n
+ - if n < playcount / max. playcount
+ - add the file to the end of the list
+
+ \subsection urgentplayer Player extensions
+ - Possible to resume play instead of restarting list from the beginning?
+ - Display covers
+ - Import filename
+ - Show image during replay
+ - Add FLAC decoder
+ - Determine max. framecount (needed for rewinding?)
+ - Init scale/level/normalize?
+ - The max. level should be recognized during play
+ - Store max. level in the database
+ - Display covers
+
+ \subsection deploy Deployment
+
+ - Script to publish a version
+ - make dist
+ - copy .tgz, README, CHANGES, HISTORY into web directory
+ - generate documentation
+ - copy into web directory
+ - sync with web
+ - How to track bugs and feature requests?
+
+ \verbatim
+ # $1: version name (e.g. 0.0.5-BETA)
+ # how to determine current path?
+ svn copy ... http://.../svn/muggle/tags/$1
+
+ make dist
+ # obtain name from output? or copy commands and make correctly
+ mv vdr-muggle-0.0.1.tgz ~/Web/current/htpc/muggle/vdr-muggle-$1.tgz
+
+ cp README ...
+ cp TODO ...
+ cp CHANGES ...
+
+ doxygen muggle.doxygen
+ cp -R doc ~/Web/current/htpc/muggle/
+
+ sitecopy --update htpctech
+ \endverbatim
+
+ \section mid ToDo items of moderate importance
+
+ \subsection midimport Import stuff
+
+ - Handle updates in both directions
+ - Check modification date in DB/fstat
+ - if file is newer: update tags into db
+ - if DB is newer: update db into tags
+ - Second pass: check for all tracks, whether files do exist
+ - delete DB entry if not
+
+ - Album
+ - Cover images (based on filename or tag)
+ - Genre
+ - Modified
+ - Cover text
+
+ - Tracks
+ - Language (?) - encoded by what standard?
+ - Rating?
+ - Modified, created
+ - Lyrics
+
+ - Import playlist from m3u
+ - Run import/update from within OSD?
+
+ \subsection midcode Code issues
+
+ \subsection midosd OSD-related issues
+ - can mgMenu and mgMainMenu be combined into one class?
+ - can mgActions inherit from cOsdItem?
+ - Incremental search
+ - Type numbers to enter characters and jump to first title accordingly
+ - Check whether submenus (as implemented in VDR) are more suitable
+ - do not permit jumping to arbitrary menus though
+
+ \subsection midcontent filter issues
+
+ - new OSD list for filters. Only ONE filter can be active at any time
+ - filters can be defined recursively, different filters can share subfilters
+ - Save/load filter set
+ - table filters and filterrules
+ - filters: id, name, created, author. Uses id 0 from filterrules.
+ - filterrules: PRIMARY(filterid, id), negate, operator, op1, op2,
+ - if operator is AND or OR, op1 and op2 are filterrule ids
+ - filters are always applied, even to "instant play" and "now playing"
+
+ \subsection midcontent Content issues
+
+ - Handle ratings (increase/decrease during replay)
+ - Keys to directly increase
+ - handle a playcounter
+ - when playfrequency reaches lower level x from above: decrease rating
+ - when playfrequency reaches lower level x from below: increase rating
+ - when playfrequency reaches upper level y from above: decrease rating
+ - when playfrequency reaches upper level y from below: increase rating
+
+ \subsection midplayer Player issues
+
+ - Use single CD files with cuesheets in metadata for FLAC
+ - Handle recoding samplerate, limiter etc correctly
+
+ \section vision Long term ideas and visions
+
+ - daapd integration?
+ - netjuke integration?
+ - Display arbitrary images while playing music
+
+ - handle off-line media (CDs, DVDs, recordings)
+ - handle streams (live TV with channel list, MP3 radio,..., EPG)
+
+ - handle images (possibly in sync with music/radio)
+ - muggle content syndication (e.g. via DAAPD)
+ - access media on someone elses computer
+ - provide a stream (e.g. icecast) of the currently played music?
+ - allow remote stations to attach to this
+
+ \section done Done
+
+ - BUG: Check play speed (was XINE related)
+ - BUG: Playlists starts with 2nd song (DONE)
+ - Export playlists
+ - Delete selected item
+ - Add command line option for top level directory
+ - Prepend top level dir to filename in non-GD-mode
+ - Edit playlist (move tracks like channels in VDR channel list)
+ (OK in playlist view)
+ - Instant play = empty current playlist, append tracks of current node and play
+ (easy, in submenu of browser)
+ - Clear playlist (submenu action)
+ - Find files from database entry based on GD compatibility flag
+ - Handle Next/PrevFile in mgPlaylist (vdr_player.c)
+ - Add plugin parameters for database name/host/user/pass
+ - Add plugin parameter for GD filename compatibility
+ - handle filters:
+ - create tracklist from filter
+ - create tree from filter
+ - i18n (english and german)
+ - Album import
+ - Various artists import (assorted)
+ - Ogg/Vorbis decoder integration
+ - cOggFile kept
+ - cOggInfo dismissed in favor of obtaining info from DB
+ - coding conventions adapted
+ - Schema extended to keep audio properties
+ - Import (mugglei) extended to store audio properties in DB
+ (most notably samplerate, no. channels)
+ - Extended mgContentItem with audio properties
+ - Extended mgGdTrack with audio properties (bitrate, samplerate, channels)
+ - in mgPCMPlayer/vdr_player.c:
+ - pass m_playing to mp3/ogg decoder (instead of filename)
+ - mgOggDecoder: obtain audio properties from DB (channels, sampling rate via mgContentItem)
+ - mgPCMPlayer::getSourceFile moved to abstract data layer (mgContentItem)
+ and made concrete in subclasses (mgGdTrack)
+ - mgDecoders::findDecoder: extend decoder detection
+ - BUG: Check compatibility for 1.3.12 (DONE)
+ - BUG: Plugin crashes when deleting playlist while playing
+ - should stop playing immediately or not permit this
+ - BUG: Check deletion of entries while playing
+ - only allowed, if item is not currently played
+ - adapt index in playlist
+ - BUG: Playlist indexing not correct
+ - Player jumps e.g. from track 1 to track 3
+ - Make sure jumping beyond the end of the list is not permitted
+ - BUG: Plugin crashes when selecting entries with special characters
+ - Escape query strings correctly
+ - should go into gd_content_interface.c (row[] in lines 1175,1179,1882)?
+ - Simple progress indicator for 1.3.12
+ - Attach to graphlcd plugin via replay string
+ - Test Instant play from browser view
+ - Displaying the menu while progress display is shown makes VDR crash (DONE)
+ - Check int/unsigned stuff in mg_playlist
+ - mgPCMPlayer::GetIndex: obtain track length from database (DONE)
+ - Import (mugglei) now checks for duplicate entries based on filenames
+ - Playlist view: start at selected on Ok
+ - Toggle Track view/playlist view (red)
+ - For playlist view show playlist name, total time
+ - For track view show name, artist, time
+ - Toggle detail/progress view (green)
+ - Track view: all metadata
+ - Playlist view: all tracks (past three, upcoming ones)
+
+*/
diff --git a/muggle-plugin/i18n.c b/muggle-plugin/i18n.c
new file mode 100644
index 0000000..891bdfd
--- /dev/null
+++ b/muggle-plugin/i18n.c
@@ -0,0 +1,1257 @@
+/*
+ * i18n.c: Internationalization
+ *
+ * See the README file for copyright information and how to reach the author.
+ * Traduction en Français Patrice Staudt
+ *
+ * $Id$
+ */
+
+#include "i18n.h"
+
+const tI18nPhrase Phrases[] =
+{
+
+ {
+ "Sort by count",
+ "Nach Häufigkeit sortieren",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Sort by count", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Key %d",
+ "Schlüsselfeld %d",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Key %d", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Create",
+ "Neu",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Nouveau", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Browse",
+ "Navigieren",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Naviguer", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Order",
+ "Sortierung",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Ordre", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Collections",
+ "Sammlungen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Collections", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Clear the collection?",
+ "Sammlung leeren?",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Vider la collection?", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Delete the collection?",
+ "Sammlung löschen?",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacer la collection?", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Create order",
+ "Sortierung neu anlegen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Créer un ordre nouveaux", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Create collection",
+ "Sammlung neu anlegen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Créer une nouvelle collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Delete the collection",
+ "Sammlung löschen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacer la collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Delete collection '%s'",
+ "Sammlung '%s' löschen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacer la collection '%s'", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Collections",
+ "Sammlungen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Collections", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Commands",
+ "Befehle",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Commandes", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Commands:%s",
+ "Befehle:%s",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Commandes:%s", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Collection",
+ "Sammlung",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "List",
+ "Liste",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Liste", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Export",
+ "Exportieren",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Exporter", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Export track list",
+ "Stückliste exportieren",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Exporter la liste", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "External playlist commands",
+ "Externe Playlist-Kommandos",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "commande externe playlist", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Loop mode off",
+ "Endlosmodus aus",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Déclancher le mode répétition", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Loop mode single",
+ "Endlosmodus Einzeltitel",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Mode répétition titre seul", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Loop mode full",
+ "Endlosmodus alle",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Mode répétition playlist", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Shuffle mode off",
+ "Zufallsmodus aus",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "mode allèatoire déclenché", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Shuffle mode normal",
+ "Zufallsmodus normal",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Mode allèatoire normal", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Artist",
+ "Interpret",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Interprète",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "ArtistABC",
+ "InterpretABC",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "InterprèteABC",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Play all",
+ "Spiele alles",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Jouer tout",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Set",
+ "Setzen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Définir",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Instant play",
+ "Sofort spielen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Jouer en direct",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Instant play '%s'",
+ "'%s' sofort spielen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Jouer '%s' en direct",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Shuffle mode party",
+ "Zufallsmodus Party",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Mode allèatoire fêtes", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Default",
+ "Ziel",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Destinataire", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "'%s' to collection",
+ "'%s' zu Sammlung",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Ajoute '%s' à une collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Set default to collection '%s'",
+ "Setze Ziel auf Sammlung '%s'",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Changer destination à la collection '%s'", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Default collection now is '%s'",
+ "Zielsammlung ist nun '%s'",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "La collection destinataire est maintenant '%s'", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Add",
+ "Hinzu",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Ajouter", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Add to a collection",
+ "Zu einer Sammlung hinzufügen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Ajouter à une collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Add to '%s'",
+ "Zu '%s' hinzufügen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Ajouter à '%s'", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Remove",
+ "Weg",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacer", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Clear",
+ "Leeren",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Vider", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Clear the collection",
+ "Sammlung leeren",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Vider la collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Remove from a collection",
+ "Aus einer Sammlung entfernen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacer d'une collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "New collection",
+ "Neue Sammlung anlegen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Ajouter une collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Remove this collection",
+ "Diese Sammlung entfernen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacer cette collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Remove entry from this collection",
+ "Eintrag aus dieser Sammlung entfernen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacer de cette collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Added %s entries",
+ "%s Einträge hinzugefügt",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Ajouté %s pièces", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Removed %s entries",
+ "%s Einträge entfernt",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacé %s pièces", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Removed all entries",
+ "Alle Einträge entfernt",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Effacé toutes les pièces", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Now playing",
+ "Jetzt wird gespielt",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "En jouant", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Rating",
+ "Bewertung",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Decade",
+ "Dekade",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Décade", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Year",
+ "Jahr",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Année", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Album",
+ "Album",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Album", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Folder1",
+ "Ordner1",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Folder1", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Folder2",
+ "Ordner2",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Folder2", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Folder3",
+ "Ordner3",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Folder3", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Folder4",
+ "Ordner4",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Folder4", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Genre 2",
+ "Genre 2",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Genre 2", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Title",
+ "Titel",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Titre", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "TitleABC",
+ "TitelABC",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "TitreABC", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Track",
+ "Track",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Pièce", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "play",
+ "spielen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "jouer", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Collection item",
+ "Sammlungseintrag",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Pièce de collection", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Collection '%s' NOT deleted",
+ "Sammlung '%s' NICHT gelöscht",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Collection '%s' PAS effacée", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Collection '%s' deleted",
+ "Sammlung '%s' gelöscht",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Collection '%s' effacée", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Select an order",
+ "Sortierung wählen",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Choisir un ordre", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Synchronize database",
+ "Datenbank synchronisieren",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Synchroniser la base des données", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Create database %s?",
+ "Datenbank %s anlegen?",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Générer la base des données %s?", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Synchronize database with track files?",
+ "Datenbank mit Trackdateien synchronisieren?",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Synchroniser la base des données avec les tracks?", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Import tracks?",
+ "Tracks importieren?",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Importer les tracks?", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Imported %d tracks...",
+ "%d Tracks importiert...",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "%d tracks importés...", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {
+ "Import done:imported %d tracks",
+ "Import fertig:%d Tracks importiert",
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "Import finis:%d tracks importés...", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ "", // TODO
+ },
+ {NULL}
+};
diff --git a/muggle-plugin/i18n.h b/muggle-plugin/i18n.h
new file mode 100644
index 0000000..884adc2
--- /dev/null
+++ b/muggle-plugin/i18n.h
@@ -0,0 +1,16 @@
+/*
+ * i18n.h: Internationalization
+ *
+ * See the README file for copyright information and how to reach the author.
+ *
+ * $Id$
+ */
+
+#ifndef _I18N__H
+#define _I18N__H
+
+// #include <vdr/i18n.h>
+#include <i18n.h>
+
+extern const tI18nPhrase Phrases[];
+#endif //_I18N__H
diff --git a/muggle-plugin/menu.txt b/muggle-plugin/menu.txt
new file mode 100644
index 0000000..57f0049
--- /dev/null
+++ b/muggle-plugin/menu.txt
@@ -0,0 +1,73 @@
+TREE
+---------
+OK Leaf: ==>DisplayTrackInfo( leaf ) // TODO
+ Node: ==> DisplayTree( child ); // expand
+BACK ==> DisplayTree( parent ); // collapse
+ ==> on root: return to VDR main menu
+
+UP select previous item (VDR-mechanism)
+DOWN select next item (VDR-mechanism)
+LEFT selection pg-up (VDR-mechanism)
+RIGHT selection pg-down (VDR-mechanism)
+
+RED add all tracks under currently highlighted node to playlist
+GREEN ==>Switch to next tree view(TODO?)
+YELLOW ==>Cycle to playlist view
+BLUE ==>Enter tree view submenu
+
+PLAYLIST
+---------
+OK -- // should start playing in the future
+BACK --
+
+UP select previous item (VDR-mechanism)
+DOWN select next item (VDR-mechanism)
+LEFT selection pg-up (VDR-mechanism)
+RIGHT selection pg-down (VDR-mechanism)
+
+RED ==>Edit playlist (mark/move, VDR mechanism) TODO
+GREEN ==>Playlist -> Track info -> Album info -> Playlist
+YELLOW ==>Cycle to filter view
+BLUE ==>Playlist submenu
+
+TRACKINFO (TODO)
+---------
+OK PlayItem (jump/skip to item? useful choice?)
+BACK go to previous view (?)
+
+RED ??
+GREEN ==>Album info (TODO)
+YELLOW ==>Cycle to filter view
+BLUE ==>Playlist submenu
+
+ALBUMINFO
+---------
+OK PlayItem (track? album?)
+BACK ==>DisplayPlaylist(); (?)
+
+RED ??
+GREEN ==>Cycle to playlist view
+YELLOW ==>Cycle to filter view
+BLUE ==>Playlist submenu
+
+FILTER
+------
+OK Confirm entry (really? VDR mechanism)
+BACK ?
+
+UP Previous filter entry (VDR mechanism)
+DOWN Next filter entry (VDR mechanism)
+
+RED Perform query, display according to view type entry (TODO)
+GREEN ==>Load other filter
+YELLOW ==>Cycle to tree view
+BLUE ==>Filter submenu
+
+/************************************************************
+ *
+ * $Log: menu.txt,v $
+ * Revision 1.3 2004/02/08 10:48:44 LarsAC
+ * Made major revisions in OSD behavior
+ *
+ *
+ ************************************************************/
diff --git a/muggle-plugin/mg_content.c b/muggle-plugin/mg_content.c
new file mode 100644
index 0000000..74b9fce
--- /dev/null
+++ b/muggle-plugin/mg_content.c
@@ -0,0 +1,264 @@
+/*!
+ * \file mg_selection.c
+ * \brief A general interface to data items, currently only GiantDisc
+ *
+ * \version $Revision: 1.0 $
+ * \date $Date: 2004-12-07 10:10:35 +0200 (Tue, 07 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr $
+ *
+ */
+
+#include <stdio.h>
+#include "i18n.h"
+#include "mg_selection.h"
+#include "mg_setup.h"
+#include "mg_tools.h"
+
+
+mgSelItem*
+mgContentItem::getKeyItem(mgKeyTypes kt)
+{
+ string val;
+ string id;
+ if (m_trackid>=0)
+ {
+ switch (kt) {
+ case keyGenres:
+ case keyGenre1:
+ case keyGenre2:
+ case keyGenre3: val = getGenre();id=m_genre1_id;break;
+ case keyArtist: val = id = getArtist();break;
+ case keyArtistABC: val = id = getArtist()[0];break;
+ case keyAlbum: val = id = getAlbum();break;
+ case keyYear: val = id = string(ltos(getYear()));break;
+ case keyDecade: val = id = string(ltos(int((getYear() % 100) / 10) * 10));break;
+ case keyTitle: val = id = getTitle();break;
+ case keyTitleABC: val = id = getTitle()[0];break;
+ case keyTrack: val = id = getTitle();break;
+ case keyLanguage: val = getLanguage();id=m_language_id ; break;
+ case keyRating: val = id = getRating();break;
+ case keyFolder1:
+ case keyFolder2:
+ case keyFolder3:
+ case keyFolder4:
+ {
+ char *folders[4];
+ char *fbuf=SeparateFolders(m_mp3file.c_str(),folders,4);
+ val = id = folders[int(kt)-int(keyFolder1)];
+ free(fbuf);
+ break;
+ }
+ default: return new mgSelItem;
+ }
+ }
+ return new mgSelItem(val,id);
+}
+
+
+string mgContentItem::getGenre () const
+{
+ string result="";
+ if (m_genre1!="NULL")
+ result = m_genre1;
+ if (m_genre2!="NULL")
+ {
+ if (!result.empty())
+ result += "/";
+ result += m_genre2;
+ }
+ return result;
+}
+
+
+string mgContentItem::getLanguage() const
+{
+ return m_language;
+}
+
+string mgContentItem::getBitrate () const
+{
+ return m_bitrate;
+}
+
+
+string mgContentItem::getImageFile () const
+{
+ return "Name of Imagefile";
+}
+
+
+string mgContentItem::getAlbum () const
+{
+ return m_albumtitle;
+}
+
+
+int mgContentItem::getYear () const
+{
+ return m_year;
+}
+
+
+int mgContentItem::getRating () const
+{
+ return m_rating;
+}
+
+
+int mgContentItem::getDuration () const
+{
+ return m_duration;
+}
+
+
+int mgContentItem::getSampleRate () const
+{
+ return m_samplerate;
+}
+
+
+int mgContentItem::getChannels () const
+{
+ return m_channels;
+}
+
+mgContentItem::mgContentItem ()
+{
+ m_trackid = -1;
+}
+
+mgContentItem::mgContentItem (const mgContentItem* c)
+{
+ m_trackid = c->m_trackid;
+ m_title = c->m_title;
+ m_mp3file = c->m_mp3file;
+ m_artist = c->m_artist;
+ m_albumtitle = c->m_albumtitle;
+ m_genre1_id = c->m_genre1_id;
+ m_genre2_id = c->m_genre2_id;
+ m_genre1 = c->m_genre1;
+ m_genre2 = c->m_genre2;
+ m_language = c->m_language;
+ m_language_id = c->m_language_id;
+ m_bitrate = c->m_bitrate;
+ m_year = c->m_year;
+ m_rating = c->m_rating;
+ m_duration = c->m_duration;
+ m_samplerate = c->m_samplerate;
+ m_channels = c->m_channels;
+}
+
+static char *mg_readline(FILE *f)
+{
+ static char buffer[10000];
+ if (fgets(buffer, sizeof(buffer), f) > 0) {
+ int l = strlen(buffer) - 1;
+ if (l >= 0 && buffer[l] == '\n')
+ buffer[l] = 0;
+ return buffer;
+ }
+ return 0;
+}
+
+static const char *FINDCMD = "cd '%s' 2>/dev/null && find -follow -name '%s' -print 2>/dev/null";
+
+static string
+GdFindFile( const char* tld, string mp3file )
+{
+ string result = "";
+ char *cmd = 0;
+ asprintf( &cmd, FINDCMD, tld, mp3file.c_str() );
+ FILE *p = popen( cmd, "r" );
+ if (p)
+ {
+ char *s;
+ if( (s = mg_readline(p) ) != 0)
+ result = string(s);
+ pclose(p);
+ }
+
+ free( cmd );
+
+ return result;
+}
+
+string
+mgContentItem::getSourceFile(bool AbsolutePath) const
+{
+ const char* tld = the_setup.ToplevelDir;
+ string result="";
+ if (AbsolutePath) result = tld;
+ if (the_setup.GdCompatibility)
+ result += GdFindFile(tld,m_mp3file);
+ else
+ result += m_mp3file;
+ return result;
+}
+
+mgContentItem::mgContentItem (const mgSelection* sel,const MYSQL_ROW row)
+{
+ m_trackid = atol (row[0]);
+ if (row[1])
+ m_title = row[1];
+ else
+ m_title = "NULL";
+ if (row[2])
+ m_mp3file = row[2];
+ else
+ m_mp3file = "NULL";
+ if (row[3])
+ m_artist = row[3];
+ else
+ m_artist = "NULL";
+ if (row[4])
+ m_albumtitle = row[4];
+ else
+ m_albumtitle = "NULL";
+ if (row[5])
+ {
+ m_genre1_id = row[5];
+ m_genre1 = sel->value(keyGenres,row[5]);
+ }
+ else
+ m_genre1 = "NULL";
+ if (row[6])
+ {
+ m_genre2_id = row[6];
+ m_genre2 = sel->value(keyGenres,row[6]);
+ }
+ else
+ m_genre2 = "NULL";
+ if (row[7])
+ m_bitrate = row[7];
+ else
+ m_bitrate = "NULL";
+ if (row[8])
+ m_year = atol (row[8]);
+ else
+ m_year = 0;
+ if (row[9])
+ m_rating = atol (row[9]);
+ else
+ m_rating = 0;
+ if (row[10])
+ m_duration = atol (row[10]);
+ else
+ m_duration = 0;
+ if (row[11])
+ m_samplerate = atol (row[11]);
+ else
+ m_samplerate = 0;
+ if (row[12])
+ m_channels = atol (row[12]);
+ else
+ m_channels = 0;
+ if (row[13])
+ {
+ m_language_id = row[13];
+ m_language = sel->value(keyLanguage,row[13]);
+ }
+ else
+ m_language_id = "NULL";
+};
+
diff --git a/muggle-plugin/mg_content.h b/muggle-plugin/mg_content.h
new file mode 100644
index 0000000..ee97745
--- /dev/null
+++ b/muggle-plugin/mg_content.h
@@ -0,0 +1,115 @@
+/*!
+ * \file mg_selection.h
+ * \brief A general interface to data items, currently only GiantDisc
+ *
+ * \version $Revision: 1.0 $
+ * \date $Date: 2004-12-07 10:10:35 +0200 (Tue, 07 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr $
+ *
+ */
+
+#ifndef _MG_CONTENT_H
+#define _MG_CONTENT_H
+#include <stdlib.h>
+#include <mysql/mysql.h>
+#include <string>
+#include <list>
+#include <vector>
+#include <map>
+using namespace std;
+
+#include "mg_tools.h"
+#include "mg_valmap.h"
+#include "mg_order.h"
+
+typedef vector<string> strvector;
+
+
+class mgSelection;
+
+//! \brief represents a content item like an mp3 file.
+class mgContentItem
+{
+ public:
+ mgContentItem ();
+
+ mgSelItem* getKeyItem(mgKeyTypes kt);
+
+ //! \brief copy constructor
+ mgContentItem(const mgContentItem* c);
+
+ //! \brief construct an item from an SQL row
+ mgContentItem (const mgSelection* sel, const MYSQL_ROW row);
+//! \brief returns track id
+ long getTrackid () const
+ {
+ return m_trackid;
+ }
+
+//! \brief returns title
+ string getTitle () const
+ {
+ return m_title;
+ }
+
+//! \brief returns filename
+ string getSourceFile (bool AbsolutePath=true) const;
+
+//! \brief returns artist
+ string getArtist () const
+ {
+ return m_artist;
+ }
+
+//! \brief returns the name of the album
+ string getAlbum () const;
+
+//! \brief returns the name of genre
+ string getGenre () const;
+
+//! \brief returns the name of the language
+ string getLanguage () const;
+
+//! \brief returns the bitrate
+ string getBitrate () const;
+
+//! \brief returns the file name of the album image
+ string getImageFile () const;
+
+//! \brief returns year
+ int getYear () const;
+
+//! \brief returns rating
+ int getRating () const;
+
+//! \brief returns duration
+ int getDuration () const;
+
+//! \brief returns samplerate
+ int getSampleRate () const;
+
+//! \brief returns # of channels
+ int getChannels () const;
+
+ private:
+ long m_trackid;
+ string m_title;
+ string m_mp3file;
+ string m_artist;
+ string m_albumtitle;
+ string m_genre1_id;
+ string m_genre2_id;
+ string m_genre1;
+ string m_genre2;
+ string m_bitrate;
+ string m_language_id;
+ string m_language;
+ int m_year;
+ int m_rating;
+ int m_duration;
+ int m_samplerate;
+ int m_channels;
+};
+
+#endif
diff --git a/muggle-plugin/mg_mysql.c b/muggle-plugin/mg_mysql.c
new file mode 100644
index 0000000..411629c
--- /dev/null
+++ b/muggle-plugin/mg_mysql.c
@@ -0,0 +1,586 @@
+/*! \file mg_mysql.c
+ * \brief A capsule around MySql database access
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date: 2005-02-10 17:42:54 +0100 (Thu, 10 Feb 2005) $
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner, Wolfgang Rohdewald
+ * \author file owner: $Author: LarsAC $
+ */
+
+#include "mg_mysql.h"
+#include "mg_tools.h"
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+
+#include "mg_setup.h"
+
+bool needGenre2;
+static bool needGenre2_set;
+bool NoHost();
+bool UsingEmbedded();
+
+class mysqlhandle_t {
+ public:
+ mysqlhandle_t();
+ ~mysqlhandle_t();
+};
+
+
+static char *datadir;
+
+static char *embedded_args[] =
+{
+ "muggle",
+ "--datadir=/tmp", // stupid default
+ "--key_buffer_size=32M"
+};
+
+#ifndef HAVE_ONLY_SERVER
+static char *embedded_groups[] =
+{
+ "embedded",
+ "server",
+ "muggle_SERVER",
+ 0
+};
+#endif
+
+void
+set_datadir(char *dir)
+{
+ mgDebug(1,"setting datadir to %s",dir);
+ struct stat stbuf;
+ datadir=strdup(dir);
+ asprintf(&embedded_args[1],"--datadir=%s",datadir);
+ if (stat(datadir,&stbuf))
+ mkdir(datadir,0755);
+ if (stat(datadir,&stbuf))
+ {
+ mgError("Cannot access datadir %s: errno=%d",datadir,errno);
+ }
+}
+
+
+mysqlhandle_t::mysqlhandle_t()
+{
+#ifndef HAVE_ONLY_SERVER
+ int argv_size;
+ if (UsingEmbedded())
+ {
+ mgDebug(1,"calling mysql_server_init for embedded");
+ argv_size = sizeof(embedded_args) / sizeof(char *);
+ }
+ else
+ {
+ if (strcmp(MYSQLCLIENTVERSION,"4.1.11")<0)
+ mgError("You have embedded mysql. For accessing external servers "
+ "you need mysql 4.1.11 but you have only %s", MYSQLCLIENTVERSION);
+ mgDebug(1,"calling mysql_server_init for external");
+ argv_size = -1;
+ }
+ if (mysql_server_init(argv_size, embedded_args, embedded_groups))
+ mgDebug(3,"mysql_server_init failed");
+#endif
+}
+
+mysqlhandle_t::~mysqlhandle_t()
+{
+#ifndef HAVE_ONLY_SERVER
+ mgDebug(3,"calling mysql_server_end");
+ mysql_server_end();
+#endif
+}
+
+static mysqlhandle_t* mysqlhandle;
+
+mgmySql::mgmySql()
+{
+ m_database_found=false;
+ m_hasfolderfields=false;
+ if (!mysqlhandle)
+ mysqlhandle = new mysqlhandle_t;
+ m_db = 0;
+ Connect();
+}
+
+mgmySql::~mgmySql()
+{
+ if (m_db)
+ {
+ mgDebug(3,"%X: closing DB connection",this);
+ mysql_close (m_db);
+ m_db = 0;
+ }
+}
+
+static char *db_cmds[] =
+{
+ "drop table if exists album;",
+ "CREATE TABLE album ( "
+ "artist varchar(255) default NULL, "
+ "title varchar(255) default NULL, "
+ "cddbid varchar(20) NOT NULL default '', "
+ "coverimg varchar(255) default NULL, "
+ "covertxt mediumtext, "
+ "modified date default NULL, "
+ "genre varchar(10) default NULL, "
+ "PRIMARY KEY (cddbid), "
+ "KEY artist (artist(10)), "
+ "KEY title (title(10)), "
+ "KEY genre (genre), "
+ "KEY modified (modified)) "
+ "TYPE=MyISAM;",
+ "drop table if exists genre;",
+ "CREATE TABLE genre ("
+ "id varchar(10) NOT NULL default '', "
+ "id3genre smallint(6) default NULL, "
+ "genre varchar(255) default NULL, "
+ "freq int(11) default NULL, "
+ "PRIMARY KEY (id)) "
+ "TYPE=MyISAM;",
+ "drop table if exists language;",
+ "CREATE TABLE language ("
+ "id varchar(4) NOT NULL default '', "
+ "language varchar(40) default NULL, "
+ "freq int(11) default NULL, "
+ "PRIMARY KEY (id)) "
+ "TYPE=MyISAM;",
+ "drop table if exists musictype;",
+ "CREATE TABLE musictype ("
+ "musictype varchar(40) default NULL, "
+ "id tinyint(3) unsigned NOT NULL auto_increment, "
+ "PRIMARY KEY (id)) "
+ "TYPE=MyISAM;",
+ "drop table if exists player;",
+ "CREATE TABLE player ( "
+ "ipaddr varchar(255) NOT NULL default '', "
+ "uichannel varchar(255) NOT NULL default '', "
+ "logtarget int(11) default NULL, "
+ "cdripper varchar(255) default NULL, "
+ "mp3encoder varchar(255) default NULL, "
+ "cdromdev varchar(255) default NULL, "
+ "cdrwdev varchar(255) default NULL, "
+ "id int(11) NOT NULL default '0', "
+ "PRIMARY KEY (id)) "
+ "TYPE=MyISAM;",
+ "drop table if exists playerstate;",
+ "CREATE TABLE playerstate ( "
+ "playerid int(11) NOT NULL default '0', "
+ "playertype int(11) NOT NULL default '0', "
+ "snddevice varchar(255) default NULL, "
+ "playerapp varchar(255) default NULL, "
+ "playerparams varchar(255) default NULL, "
+ "ptlogger varchar(255) default NULL, "
+ "currtracknb int(11) default NULL, "
+ "state varchar(4) default NULL, "
+ "shufflepar varchar(255) default NULL, "
+ "shufflestat varchar(255) default NULL, "
+ "pauseframe int(11) default NULL, "
+ "framesplayed int(11) default NULL, "
+ "framestotal int(11) default NULL, "
+ "anchortime bigint(20) default NULL, "
+ "PRIMARY KEY (playerid,playertype)) "
+ "TYPE=HEAP;",
+ "drop table if exists playlist;",
+ "CREATE TABLE playlist ( "
+ "title varchar(255) default NULL, "
+ "author varchar(255) default NULL, "
+ "note varchar(255) default NULL, "
+ "created timestamp(8) NOT NULL, "
+ "id int(10) unsigned NOT NULL auto_increment, "
+ "PRIMARY KEY (id)) "
+ "TYPE=MyISAM;",
+ "drop table if exists playlistitem;",
+ "CREATE TABLE playlistitem ( "
+ "playlist int(11) NOT NULL default '0', "
+ "tracknumber mediumint(9) NOT NULL default '0', "
+ "trackid int(11) default NULL, "
+ "PRIMARY KEY (playlist,tracknumber)) "
+ "TYPE=MyISAM;",
+ "drop table if exists playlog;",
+ "CREATE TABLE playlog ( "
+ "trackid int(11) default NULL, "
+ "played date default NULL, "
+ "id tinyint(3) unsigned NOT NULL auto_increment, "
+ "PRIMARY KEY (id)) "
+ "TYPE=MyISAM;",
+ "drop table if exists recordingitem;",
+ "CREATE TABLE recordingitem ( "
+ "trackid int(11) default NULL, "
+ "recdate date default NULL, "
+ "rectime time default NULL, "
+ "reclength int(11) default NULL, "
+ "enddate date default NULL, "
+ "endtime time default NULL, "
+ "repeat varchar(10) default NULL, "
+ "initcmd varchar(255) default NULL, "
+ "parameters varchar(255) default NULL, "
+ "atqjob int(11) default NULL, "
+ "id int(11) NOT NULL default '0', "
+ "PRIMARY KEY (id)) "
+ "TYPE=MyISAM;",
+ "drop table if exists source",
+ "CREATE TABLE source ( "
+ "source varchar(40) default NULL, "
+ "id tinyint(3) unsigned NOT NULL auto_increment, "
+ "PRIMARY KEY (id)) "
+ "TYPE=MyISAM;",
+ "drop table if exists tracklistitem;",
+ "CREATE TABLE tracklistitem ( "
+ "playerid int(11) NOT NULL default '0', "
+ "listtype smallint(6) NOT NULL default '0', "
+ "tracknb int(11) NOT NULL default '0', "
+ "trackid int(11) NOT NULL default '0', "
+ "PRIMARY KEY (playerid,listtype,tracknb)) "
+ "TYPE=MyISAM;",
+ "drop table if exists tracks;",
+ "CREATE TABLE tracks ( "
+ "artist varchar(255) default NULL, "
+ "title varchar(255) default NULL, "
+ "genre1 varchar(10) default NULL, "
+ "genre2 varchar(10) default NULL, "
+ "year smallint(5) unsigned default NULL, "
+ "lang varchar(4) default NULL, "
+ "type tinyint(3) unsigned default NULL, "
+ "rating tinyint(3) unsigned default NULL, "
+ "length smallint(5) unsigned default NULL, "
+ "source tinyint(3) unsigned default NULL, "
+ "sourceid varchar(20) default NULL, "
+ "tracknb tinyint(3) unsigned default NULL, "
+ "mp3file varchar(255) default NULL, "
+ "condition tinyint(3) unsigned default NULL, "
+ "voladjust smallint(6) default '0', "
+ "lengthfrm mediumint(9) default '0', "
+ "startfrm mediumint(9) default '0', "
+ "bpm smallint(6) default '0', "
+ "lyrics mediumtext, "
+ "bitrate varchar(10) default NULL, "
+ "created date default NULL, "
+ "modified date default NULL, "
+ "backup tinyint(3) unsigned default NULL, "
+ "samplerate int(7) unsigned default NULL, "
+ "channels tinyint(3) unsigned default NULL, "
+ "id int(11) NOT NULL auto_increment, "
+ "folder1 varchar(255), "
+ "folder2 varchar(255), "
+ "folder3 varchar(255), "
+ "folder4 varchar(255), "
+ "PRIMARY KEY (id), "
+ "KEY title (title(10)), "
+ "KEY mp3file (mp3file(10)), "
+ "KEY genre1 (genre1), "
+ "KEY genre2 (genre2), "
+ "KEY year (year), "
+ "KEY lang (lang), "
+ "KEY type (type), "
+ "KEY rating (rating), "
+ "KEY sourceid (sourceid), "
+ "KEY artist (artist(10))) "
+ "TYPE=MyISAM;"
+};
+
+bool
+mgmySql::sql_query(const char *sql)
+{
+ return mysql_query(m_db,sql);
+}
+
+
+MYSQL_RES*
+mgmySql::exec_sql( string query)
+{
+ if (!m_db || query.empty())
+ return 0;
+ mgDebug(4,"exec_sql(%X,%s)",m_db,query.c_str());
+ if (sql_query (query.c_str ()))
+ {
+ mgError("SQL Error in %s: %s",query.c_str(),mysql_error (m_db));
+ std::cout<<"ERROR in " << query << ":" << mysql_error(m_db)<<std::endl;
+ return 0;
+ }
+ return mysql_store_result (m_db);
+}
+
+string
+mgmySql::get_col0( const string query)
+{
+ MYSQL_RES * sql_result = exec_sql ( query);
+ if (!sql_result)
+ return "NULL";
+ MYSQL_ROW row = mysql_fetch_row (sql_result);
+ string result;
+ if (row == NULL)
+ result = "NULL";
+ else if (row[0] == NULL)
+ result = "NULL";
+ else
+ result = row[0];
+ mysql_free_result (sql_result);
+ return result;
+}
+
+unsigned long
+mgmySql::exec_count( const string query)
+{
+ return atol (get_col0 ( query).c_str ());
+}
+
+struct genres_t {
+ char *id;
+ int id3genre;
+ char *name;
+};
+
+struct lang_t {
+ char *id;
+ char *name;
+};
+
+struct musictypes_t {
+ char *name;
+};
+
+struct sources_t {
+ char *name;
+};
+
+#include "mg_tables.h"
+void mgmySql::FillTables()
+{
+ int len = sizeof( genres ) / sizeof( genres_t );
+ for( int i=0; i < len; i ++ )
+ {
+ char b[600];
+ char id3genre[5];
+ if (genres[i].id3genre>=0)
+ sprintf(id3genre,"%d",genres[i].id3genre);
+ else
+ strcpy(id3genre,"NULL");
+ string genre = sql_string(genres[i].name);
+ sprintf(b,"INSERT INTO genre SET id='%s', id3genre=%s, genre=%s",
+ genres[i].id,id3genre,genre.c_str());
+ exec_sql(b);
+ }
+ len = sizeof( languages ) / sizeof( lang_t );
+ for( int i=0; i < len; i ++ )
+ {
+ char b[600];
+ sprintf(b,"INSERT INTO language SET id='%s', language=",
+ languages[i].id);
+ sql_Cstring(languages[i].name,strchr(b,0));
+ exec_sql(b);
+ }
+ len = sizeof( musictypes ) / sizeof( musictypes_t );
+ for( int i=0; i < len; i ++ )
+ {
+ char b[600];
+ sprintf(b,"INSERT INTO musictype SET musictype='%s'",
+ musictypes[i].name);
+ exec_sql(b);
+ }
+ len = sizeof( sources ) / sizeof( sources_t );
+ for( int i=0; i < len; i ++ )
+ {
+ char b[600];
+ sprintf(b,"INSERT INTO source SET source='%s'",
+ sources[i].name);
+ exec_sql(b);
+ }
+}
+
+time_t createtime;
+
+void mgmySql::Create()
+{
+ createtime=time(0);
+ // create database and tables
+ mgDebug(1,"Dropping and recreating database %s",the_setup.DbName);
+ if (sql_query("DROP DATABASE IF EXISTS GiantDisc;"))
+ {
+ mgWarning("Cannot drop existing database:%s",mysql_error (m_db));
+ return;
+ }
+ if (sql_query("CREATE DATABASE GiantDisc;"))
+ {
+ mgWarning("Cannot create database:%s",mysql_error (m_db));
+ return;
+ }
+
+ if (!UsingEmbedded())
+ sql_query("grant all privileges on GiantDisc.* to vdr@localhost;");
+ // ignore error. If we can create the data base, we can do everything
+ // with it anyway.
+
+ if (mysql_select_db(m_db,the_setup.DbName))
+ mgError("mysql_select_db(%s) failed with %s",mysql_error(m_db));
+
+ int len = sizeof( db_cmds ) / sizeof( char* );
+ for( int i=0; i < len; i ++ )
+ {
+ if (sql_query (db_cmds[i]))
+ {
+ mgWarning("%20s: %s",db_cmds[i],mysql_error (m_db));
+ sql_query("DROP DATABASE IF EXISTS GiantDisc;"); // clean up
+ return;
+ }
+ }
+ m_database_found=true;
+ FillTables();
+}
+
+string
+mgmySql::sql_string( const string s )
+{
+ char *b = sql_Cstring(s);
+ string result = string( b);
+ free( b);
+ return result;
+}
+
+char*
+mgmySql::sql_Cstring( const string s, char *buf )
+{
+ return sql_Cstring(s.c_str(),buf);
+}
+
+char*
+mgmySql::sql_Cstring( const char *s, char *buf)
+{
+ char *b;
+ if (buf)
+ b=buf;
+ else
+ {
+ int buflen;
+ if (!this)
+ buflen=strlen(s)+2;
+ else
+ buflen=2*strlen(s)+3;
+ b = (char *) malloc( buflen);
+ }
+ b[0]='\'';
+ if (!this)
+ strcpy(b+1,s);
+ else
+ mysql_real_escape_string( m_db, b+1, s, strlen(s) );
+ *(strchr(b,0)+1)=0;
+ *(strchr(b,0))='\'';
+ return b;
+}
+
+bool
+mgmySql::ServerConnected () const
+{
+ return m_db;
+}
+
+bool
+mgmySql::Connected () const
+{
+ return m_database_found;
+}
+
+bool
+NoHost()
+{
+ return (!the_setup.DbHost
+ || strlen(the_setup.DbHost)==0);
+}
+
+bool
+UsingEmbedded()
+{
+#ifdef HAVE_ONLY_SERVER
+ return false;
+#else
+ return NoHost();
+#endif
+}
+
+void
+mgmySql::Connect ()
+{
+ assert(!m_db);
+ m_db = mysql_init (0);
+ if (!m_db)
+ return;
+ if (UsingEmbedded())
+ {
+ if (!mysql_real_connect(m_db, 0, 0, 0, 0, 0, 0, 0))
+ mgWarning("Failed to connect to embedded mysql in %s:%s ",datadir,mysql_error(m_db));
+ else
+ mgDebug(1,"Connected to embedded mysql in %s",datadir);
+ }
+ else
+ {
+ if (NoHost() || !strcmp(the_setup.DbHost,"localhost"))
+ mgDebug(1,"Using socket %s for connecting to local system as user %s.",
+ the_setup.DbSocket, the_setup.DbUser);
+ else
+ mgDebug(1,"Using TCP for connecting to server %s as user %s.",
+ the_setup.DbHost, the_setup.DbUser);
+ if (!mysql_real_connect( m_db,
+ the_setup.DbHost, the_setup.DbUser, the_setup.DbPass, 0,
+ the_setup.DbPort, the_setup.DbSocket, 0 ) != 0 )
+ {
+ mgWarning("Failed to connect to server '%s' as User '%s', Password '%s': %s",
+ the_setup.DbHost,the_setup.DbUser,the_setup.DbPass,mysql_error(m_db));
+ mysql_close (m_db);
+ m_db = 0;
+ }
+ }
+ if (m_db)
+ {
+ m_database_found = mysql_select_db(m_db,the_setup.DbName)==0;
+ {
+ if (!Connected())
+ if (!createtime)
+ mgWarning(mysql_error(m_db));
+ }
+ }
+ if (!needGenre2_set && Connected())
+ {
+ needGenre2_set=true;
+ needGenre2=exec_count("SELECT COUNT(DISTINCT genre2) from tracks")>1;
+ }
+ return;
+}
+
+
+void
+mgmySql::CreateFolderFields()
+{
+ if (!Connected())
+ return;
+ if (HasFolderFields())
+ return;
+ sql_query("DESCRIBE tracks folder1");
+ MYSQL_RES *rows = mysql_store_result(m_db);
+ if (rows)
+ {
+ m_hasfolderfields = mysql_num_rows(rows)>0;
+ mysql_free_result(rows);
+ if (!m_hasfolderfields)
+ {
+ m_hasfolderfields = !sql_query(
+ "alter table tracks add column folder1 varchar(255),"
+ "add column folder2 varchar(255),"
+ "add column folder3 varchar(255),"
+ "add column folder4 varchar(255)");
+
+ }
+ }
+}
+
+void
+database_end()
+{
+ delete mysqlhandle;
+ mysqlhandle=0;
+}
diff --git a/muggle-plugin/mg_mysql.h b/muggle-plugin/mg_mysql.h
new file mode 100644
index 0000000..3c3fde1
--- /dev/null
+++ b/muggle-plugin/mg_mysql.h
@@ -0,0 +1,76 @@
+/*!
+ * \file mg_mysql.h
+ * \brief A capsule around MySql database access
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date: 2005-02-10 17:42:54 +0100 (Thu, 10 Feb 2005) $
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner, Wolfgang Rohdewald
+ * \author Responsible author: $Author: LarsAC $
+ */
+
+#ifndef __MG_MYSQL_H
+#define __MG_MYSQL_H
+
+#include <string>
+#include <mysql/mysql.h>
+
+using namespace std;
+
+void database_end(); // must be done explicitly
+void set_datadir(char *datadir);
+
+/*!
+ * \brief an abstract database class
+ *
+ */
+class mgmySql
+{
+ public:
+
+ /*! \brief default constructor
+ */
+ mgmySql( );
+
+ ~mgmySql();
+
+ /*!
+ * \brief helper function to execute queries
+ *
+ */
+ MYSQL_RES* exec_sql( const string query);
+
+ /*!
+ * \brief escape arguments to be contained in a query
+ */
+ string sql_string( string s );
+
+ char* sql_Cstring( const string s,char *buf=0);
+ char* sql_Cstring( const char *s,char *buf=0);
+
+ string get_col0( const string query);
+
+/*! \brief executes a query and returns the integer value from
+ * the first column in the first row. The query shold be a COUNT query
+ * returning only one row.
+ * \param query the SQL query to be executed
+ */
+ unsigned long exec_count (string query);
+
+ long thread_id() { return mysql_thread_id(m_db);}
+ long affected_rows() { return mysql_affected_rows(m_db);}
+ bool ServerConnected() const;
+ bool Connected() const;
+ bool HasFolderFields() const { return m_hasfolderfields;}
+ void Connect();
+ //! \brief create database and tables
+ void Create();
+ void FillTables();
+ void CreateFolderFields();
+ private:
+ MYSQL *m_db;
+ bool m_database_found;
+ bool m_hasfolderfields;
+ bool sql_query(const char *query);
+};
+
+#endif
diff --git a/muggle-plugin/mg_order.c b/muggle-plugin/mg_order.c
new file mode 100644
index 0000000..525469c
--- /dev/null
+++ b/muggle-plugin/mg_order.c
@@ -0,0 +1,1090 @@
+#include "mg_order.h"
+#include "mg_tools.h"
+#include "i18n.h"
+#include <stdio.h>
+#include <assert.h>
+
+mgSelItem zeroitem;
+
+bool iskeyGenre(mgKeyTypes kt)
+{
+ return kt>=keyGenre1 && kt <= keyGenres;
+}
+
+class mgRefParts : public mgParts {
+ public:
+ mgRefParts(const mgReference& r);
+};
+
+
+strlist& operator+=(strlist&a, strlist b)
+{
+ a.insert(a.end(), b.begin(),b.end());
+ return a;
+}
+
+
+/*! \brief if the SQL command works on only 1 table, remove all table
+* qualifiers. Example: SELECT tracks.title FROM tracks becomes SELECT title
+* FROM tracks
+* \param spar the sql command. It will be edited in place
+* \return the new sql command is also returned
+*/
+static string
+optimize (string & spar)
+{
+ string s = spar;
+ string::size_type tmp = s.find (" WHERE");
+ if (tmp != string::npos)
+ s.erase (tmp, 9999);
+ tmp = s.find (" ORDER");
+ if (tmp != string::npos)
+ s.erase (tmp, 9999);
+ string::size_type frompos = s.find (" FROM ") + 6;
+ if (s.substr (frompos).find (",") == string::npos)
+ {
+ string from = s.substr (frompos, 999) + '.';
+ string::size_type track;
+ while ((track = spar.find (from)) != string::npos)
+ {
+ spar.erase (track, from.size ());
+ }
+ }
+ return spar;
+}
+
+string&
+addsep (string & s, string sep, string n)
+{
+ if (!n.empty ())
+ {
+ if (!s.empty ())
+ s.append (sep);
+ s.append (n);
+ }
+ return s;
+}
+
+
+static string
+sql_list (string prefix,strlist v,string sep=",",string postfix="")
+{
+ string result = "";
+ for (list < string >::iterator it = v.begin (); it != v.end (); ++it)
+ {
+ addsep (result, sep, *it);
+ }
+ if (!result.empty())
+ {
+ result.insert(0," "+prefix+" ");
+ result += postfix;
+ }
+ return result;
+}
+
+//! \brief converts long to string
+string
+itos (int i)
+{
+ stringstream s;
+ s << i;
+ return s.str ();
+}
+
+//! \brief convert long to string
+string
+ltos (long l)
+{
+ stringstream s;
+ s << l;
+ return s.str ();
+}
+
+mgSelItem::mgSelItem()
+{
+ m_valid=false;
+ m_count=0;
+}
+
+mgSelItem::mgSelItem(string v,string i,unsigned int c)
+{
+ set(v,i,c);
+}
+
+void
+mgSelItem::set(string v,string i,unsigned int c)
+{
+ m_valid=true;
+ m_value=v;
+ m_id=i;
+ m_count=c;
+}
+
+void
+mgSelItem::operator=(const mgSelItem& from)
+{
+ m_valid=from.m_valid;
+ m_value=from.m_value;
+ m_id=from.m_id;
+ m_count=from.m_count;
+}
+
+void
+mgSelItem::operator=(const mgSelItem* from)
+{
+ m_valid=from->m_valid;
+ m_value=from->m_value;
+ m_id=from->m_id;
+ m_count=from->m_count;
+}
+
+bool
+mgSelItem::operator==(const mgSelItem& other) const
+{
+ return m_value == other.m_value
+ && m_id == other.m_id;
+}
+
+class mgKeyNormal : public mgKey {
+ public:
+ mgKeyNormal(const mgKeyNormal& k);
+ mgKeyNormal(const mgKeyTypes kt, string table, string field);
+ virtual mgParts Parts(mgmySql &db,bool orderby=false) const;
+ string value() const;
+ string id() const;
+ bool valid() const;
+ void set(mgSelItem& item);
+ mgSelItem& get();
+ mgKeyTypes Type() const { return m_kt; }
+ virtual string expr() const { return m_table + "." + m_field; }
+ virtual string table() const { return m_table; }
+ protected:
+ string IdClause(mgmySql &db,string what,string::size_type start=0,string::size_type len=string::npos) const;
+ void AddIdClause(mgmySql &db,mgParts &result,string what) const;
+ mgSelItem m_item;
+ string m_field;
+ private:
+ mgKeyTypes m_kt;
+ string m_table;
+};
+
+class mgKeyABC : public mgKeyNormal {
+ public:
+ mgKeyABC(const mgKeyNormal& k) : mgKeyNormal(k) {}
+ mgKeyABC(const mgKeyTypes kt, string table, string field) : mgKeyNormal(kt,table,field) {}
+ virtual string expr() const { return "substring("+mgKeyNormal::expr()+",1,1)"; }
+ protected:
+ //void AddIdClause(mgmySql &db,mgParts &result,string what) const;
+};
+
+class mgKeyDate : public mgKeyNormal {
+ public:
+ mgKeyDate(mgKeyTypes kt,string table, string field) : mgKeyNormal(kt,table,field) {}
+};
+
+class mgKeyTrack : public mgKeyNormal {
+ public:
+ mgKeyTrack() : mgKeyNormal(keyTrack,"tracks","tracknb") {};
+ mgParts Parts(mgmySql &db,bool orderby=false) const;
+};
+
+class mgKeyAlbum : public mgKeyNormal {
+ public:
+ mgKeyAlbum() : mgKeyNormal(keyAlbum,"album","title") {};
+ mgParts Parts(mgmySql &db,bool orderby=false) const;
+};
+
+mgParts
+mgKeyAlbum::Parts(mgmySql &db,bool orderby) const
+{
+ mgParts result = mgKeyNormal::Parts(db,orderby);
+ result.tables.push_back("tracks");
+ return result;
+}
+
+class mgKeyFolder : public mgKeyNormal {
+ public:
+ mgKeyFolder(mgKeyTypes kt,const char *fname)
+ : mgKeyNormal(kt,"tracks",fname) { m_enabled=-1;};
+ bool Enabled(mgmySql &db);
+ private:
+ int m_enabled;
+};
+
+class mgKeyFolder1 : public mgKeyFolder {
+ public:
+ mgKeyFolder1() : mgKeyFolder(keyFolder1,"folder1") {};
+};
+class mgKeyFolder2 : public mgKeyFolder {
+ public:
+ mgKeyFolder2() : mgKeyFolder(keyFolder2,"folder2") {};
+};
+class mgKeyFolder3 : public mgKeyFolder {
+ public:
+ mgKeyFolder3() : mgKeyFolder(keyFolder3,"folder3") {};
+};
+class mgKeyFolder4 : public mgKeyFolder {
+ public:
+ mgKeyFolder4() : mgKeyFolder(keyFolder4,"folder4") {};
+};
+
+bool
+mgKeyFolder::Enabled(mgmySql &db)
+{
+ if (m_enabled<0)
+ {
+ if (!db.Connected())
+ return false;
+ char *b;
+ asprintf(&b,"DESCRIBE tracks %s",m_field.c_str());
+ MYSQL_RES * rows = db.exec_sql (b);
+ free(b);
+ if (rows)
+ {
+ m_enabled = mysql_num_rows(rows);
+ mysql_free_result (rows);
+ }
+ }
+ return (m_enabled==1);
+}
+
+class mgKeyGenres : public mgKeyNormal {
+ public:
+ mgKeyGenres() : mgKeyNormal(keyGenres,"tracks","genre1") {};
+ mgKeyGenres(mgKeyTypes kt) : mgKeyNormal(kt,"tracks","genre1") {};
+ mgParts Parts(mgmySql &db,bool orderby=false) const;
+ string map_idfield() const { return "id"; }
+ string map_valuefield() const { return "genre"; }
+ string map_valuetable() const { return "genre"; }
+ protected:
+ virtual unsigned int genrelevel() const { return 4; }
+ private:
+ string GenreClauses(mgmySql &db,bool orderby) const;
+};
+
+class mgKeyGenre1 : public mgKeyGenres
+{
+ public:
+ mgKeyGenre1() : mgKeyGenres(keyGenre1) {}
+ unsigned int genrelevel() const { return 1; }
+};
+
+class mgKeyGenre2 : public mgKeyGenres
+{
+ public:
+ mgKeyGenre2() : mgKeyGenres(keyGenre2) {}
+ unsigned int genrelevel() const { return 2; }
+};
+
+class mgKeyGenre3 : public mgKeyGenres
+{
+ public:
+ mgKeyGenre3() : mgKeyGenres(keyGenre3) {}
+ unsigned int genrelevel() const { return 3; }
+};
+
+string
+mgKeyGenres::GenreClauses(mgmySql &db,bool orderby) const
+{
+ strlist g1;
+ strlist g2;
+
+ if (orderby)
+ if (genrelevel()==4)
+ {
+ g1.push_back("tracks.genre1=genre.id");
+ g2.push_back("tracks.genre2=genre.id");
+ }
+ else
+ {
+ g1.push_back("substring(tracks.genre1,1,"+ltos(genrelevel())+")=genre.id");
+ g2.push_back("substring(tracks.genre2,1,"+ltos(genrelevel())+")=genre.id");
+ }
+
+ if (valid())
+ {
+ unsigned int len=genrelevel();
+ if (len==4) len=0;
+ g1.push_back(IdClause(db,"tracks.genre1",0,genrelevel()));
+ g2.push_back(IdClause(db,"tracks.genre2",0,genrelevel()));
+ }
+
+ extern bool needGenre2;
+ if (needGenre2)
+ {
+ string o1=sql_list("(",g1," AND ",")");
+ if (o1.empty())
+ return "";
+ string o2=sql_list("(",g2," AND ",")");
+ return string("(") + o1 + " OR " + o2 + string(")");
+ }
+ else
+ return sql_list("",g1," AND ");
+}
+
+
+mgParts
+mgKeyGenres::Parts(mgmySql &db,bool orderby) const
+{
+ mgParts result;
+ result.clauses.push_back(GenreClauses(db,orderby));
+ result.tables.push_back("tracks");
+ if (orderby)
+ {
+ result.fields.push_back("genre.genre");
+ result.fields.push_back("genre.id");
+ result.tables.push_back("genre");
+ result.orders.push_back("genre.genre");
+ }
+ return result;
+}
+
+
+class mgKeyLanguage : public mgKeyNormal {
+ public:
+ mgKeyLanguage() : mgKeyNormal(keyLanguage,"tracks","lang") {};
+ mgParts Parts(mgmySql &db,bool orderby=false) const;
+ string map_idfield() const { return "id"; }
+ string map_valuefield() const { return "language"; }
+ string map_valuetable() const { return "language"; }
+};
+
+class mgKeyCollection: public mgKeyNormal {
+ public:
+ mgKeyCollection() : mgKeyNormal(keyCollection,"playlist","id") {};
+ mgParts Parts(mgmySql &db,bool orderby=false) const;
+ string map_idfield() const { return "id"; }
+ string map_valuefield() const { return "title"; }
+ string map_valuetable() const { return "playlist"; }
+};
+class mgKeyCollectionItem : public mgKeyNormal {
+ public:
+ mgKeyCollectionItem() : mgKeyNormal(keyCollectionItem,"playlistitem","tracknumber") {};
+ mgParts Parts(mgmySql &db,bool orderby=false) const;
+};
+
+class mgKeyDecade : public mgKeyNormal {
+ public:
+ mgKeyDecade() : mgKeyNormal(keyDecade,"tracks","year") {}
+ string expr() const { return "substring(convert(10 * floor(tracks.year/10), char),3)"; }
+};
+
+string
+mgKeyNormal::id() const
+{
+ return m_item.id();
+}
+
+bool
+mgKeyNormal::valid() const
+{
+ return m_item.valid();
+}
+
+string
+mgKeyNormal::value() const
+{
+ return m_item.value();
+}
+
+
+mgKeyNormal::mgKeyNormal(const mgKeyNormal& k)
+{
+ m_kt = k.m_kt;
+ m_table = k.m_table;
+ m_field = k.m_field;
+ m_item = k.m_item;
+}
+
+mgKeyNormal::mgKeyNormal(const mgKeyTypes kt, string table, string field)
+{
+ m_kt = kt;
+ m_table = table;
+ m_field = field;
+}
+
+void
+mgKeyNormal::set(mgSelItem& item)
+{
+ m_item=item;
+}
+
+mgSelItem&
+mgKeyNormal::get()
+{
+ return m_item;
+}
+
+mgParts::mgParts()
+{
+ m_sql_select="";
+ orderByCount = false;
+}
+
+mgParts::~mgParts()
+{
+}
+
+mgParts
+mgKeyNormal::Parts(mgmySql &db, bool orderby) const
+{
+ mgParts result;
+ result.tables.push_back(table());
+ AddIdClause(db,result,expr());
+ if (orderby)
+ {
+ result.fields.push_back(expr());
+ result.orders.push_back(expr());
+ }
+ return result;
+}
+
+string
+mgKeyNormal::IdClause(mgmySql &db,string what,string::size_type start,string::size_type len) const
+{
+ if (len==0)
+ len=string::npos;
+ if (id() == "'NULL'")
+ return what + " is NULL";
+ else if (len==string::npos)
+ return what + "=" + db.sql_string(id());
+ else
+ {
+ return "substring("+what + ","+ltos(start+1)+","+ltos(len)+")="
+ + db.sql_string(id().substr(start,len));
+ }
+}
+
+void
+mgKeyNormal::AddIdClause(mgmySql &db,mgParts &result,string what) const
+{
+ if (valid())
+ result.clauses.push_back(IdClause(db,what));
+}
+
+mgParts
+mgKeyTrack::Parts(mgmySql &db,bool orderby) const
+{
+ mgParts result;
+ result.tables.push_back("tracks");
+ AddIdClause(db,result,"tracks.title");
+ if (orderby)
+ {
+ // if you change tracks.title, please also
+ // change mgContentItem::getKeyItem()
+ result.fields.push_back("tracks.title");
+ result.orders.push_back("tracks.tracknb");
+ }
+ return result;
+}
+
+mgParts
+mgKeyLanguage::Parts(mgmySql &db,bool orderby) const
+{
+ mgParts result;
+ AddIdClause(db,result,"tracks.lang");
+ result.tables.push_back("tracks");
+ if (orderby)
+ {
+ result.fields.push_back("language.language");
+ result.fields.push_back("tracks.lang");
+ result.tables.push_back("language");
+ result.orders.push_back("language.language");
+ }
+ return result;
+}
+
+mgParts
+mgKeyCollection::Parts(mgmySql &db,bool orderby) const
+{
+ mgParts result;
+ if (orderby)
+ {
+ result.tables.push_back("playlist");
+ AddIdClause(db,result,"playlist.id");
+ result.fields.push_back("playlist.title");
+ result.fields.push_back("playlist.id");
+ result.orders.push_back("playlist.title");
+ }
+ else
+ {
+ result.tables.push_back("playlistitem");
+ AddIdClause(db,result,"playlistitem.playlist");
+ }
+ return result;
+}
+
+mgParts
+mgKeyCollectionItem::Parts(mgmySql &db,bool orderby) const
+{
+ mgParts result;
+ result.tables.push_back("playlistitem");
+ AddIdClause(db,result,"playlistitem.tracknumber");
+ if (orderby)
+ {
+ // tracks nur hier, fuer sql_delete_from_coll wollen wir es nicht
+ result.tables.push_back("tracks");
+ result.fields.push_back("tracks.title");
+ result.fields.push_back("playlistitem.tracknumber");
+ result.orders.push_back("playlistitem.tracknumber");
+ }
+ return result;
+}
+
+mgParts&
+mgParts::operator+=(mgParts a)
+{
+ fields += a.fields;
+ tables += a.tables;
+ clauses += a.clauses;
+ orders += a.orders;
+ return *this;
+}
+
+mgRefParts::mgRefParts(const mgReference& r)
+{
+ tables.push_back(r.t1());
+ tables.push_back(r.t2());
+ clauses.push_back(r.t1() + '.' + r.f1() + '=' + r.t2() + '.' + r.f2());
+}
+
+void
+mgParts::Prepare()
+{
+ tables.sort();
+ tables.unique();
+ strlist::reverse_iterator it;
+ string prevtable = "";
+ for (it = tables.rbegin(); it != tables.rend(); ++it)
+ {
+ if (!prevtable.empty())
+ *this += ref.Connect(prevtable,*it);
+ prevtable = *it;
+ }
+ tables.sort();
+ tables.unique();
+ clauses.sort();
+ clauses.unique();
+ orders.unique();
+}
+
+string
+mgParts::sql_select(bool distinct)
+{
+ if (!m_sql_select.empty())
+ return m_sql_select;
+ Prepare();
+ string result;
+ if (distinct)
+ {
+ fields.push_back("COUNT(*) AS mgcount");
+ result = sql_list("SELECT",fields);
+ fields.pop_back();
+ }
+ else
+ result = sql_list("SELECT",fields);
+ if (result.empty())
+ return result;
+ result += sql_list("FROM",tables);
+ result += sql_list("WHERE",clauses," AND ");
+ if (distinct)
+ {
+ result += sql_list("GROUP BY",fields);
+ if (orderByCount)
+ orders.insert(orders.begin(),"mgcount desc");
+ }
+ result += sql_list("ORDER BY",orders);
+ optimize(result);
+ return result;
+}
+
+string
+mgParts::sql_count()
+{
+ Prepare();
+ string result = sql_list("SELECT COUNT(DISTINCT",fields,",",")");
+ if (result.empty())
+ return result;
+ result += sql_list("FROM",tables);
+ result += sql_list("WHERE",clauses," AND ");
+ optimize(result);
+ return result;
+}
+
+bool
+mgParts::UsesTracks()
+{
+ for (list < string >::iterator it = tables.begin (); it != tables.end (); ++it)
+ if (*it == "tracks") return true;
+ return false;
+}
+
+string
+mgParts::sql_delete_from_collection(string pid)
+{
+ if (pid.empty())
+ return "";
+ Prepare();
+ // del nach vorne, weil DELETE playlistitem die erste Table nimmt,
+ // die passt, egal ob alias oder nicht.
+ tables.push_front("playlistitem as del");
+ clauses.push_back("del.playlist="+pid);
+ // todo geht so nicht fuer andere selections
+ if (UsesTracks())
+ clauses.push_back("del.trackid=tracks.id");
+ else
+ clauses.push_back("del.trackid=playlistitem.trackid");
+ string result = "DELETE playlistitem";
+ result += sql_list(" FROM",tables);
+ result += sql_list(" WHERE",clauses," AND ");
+ optimize(result);
+ return result;
+}
+
+string
+mgParts::sql_update(strlist new_values)
+{
+ Prepare();
+ assert(fields.size()==new_values.size());
+ string result = sql_list("UPDATE",fields);
+ result += sql_list(" FROM",tables);
+ result += sql_list(" WHERE",clauses," AND ");
+ result += sql_list("VALUES(",new_values,",",")");
+ optimize(result);
+ return result;
+}
+
+mgReference::mgReference(string t1,string f1,string t2,string f2)
+{
+ m_t1 = t1;
+ m_f1 = f1;
+ m_t2 = t2;
+ m_f2 = f2;
+}
+
+mgOrder::mgOrder()
+{
+ clear();
+ setKey (keyArtist);
+ setKey (keyAlbum);
+ setKey (keyTrack);
+ m_orderByCount = false;
+}
+
+mgOrder::~mgOrder()
+{
+ truncate(0);
+}
+
+mgKey*
+mgOrder::Key(unsigned int idx) const
+{
+ return Keys[idx];
+}
+
+mgKey*&
+mgOrder::operator[](unsigned int idx)
+{
+ assert(idx<size());
+ return Keys[idx];
+}
+
+bool
+operator==(const mgOrder& a, const mgOrder &b)
+{
+ bool result = a.size()==b.size();
+ if (result)
+ for (unsigned int i=0; i<a.size();i++)
+ {
+ result &= a.Key(i)->Type()==b.Key(i)->Type();
+ if (!result) break;
+ }
+ return result;
+}
+
+const mgOrder&
+mgOrder::operator=(const mgOrder& from)
+{
+ clear();
+ InitFrom(from);
+ return *this;
+}
+
+mgOrder::mgOrder(const mgOrder &from)
+{
+ InitFrom(from);
+}
+
+void
+mgOrder::InitFrom(const mgOrder &from)
+{
+ for (unsigned int i = 0; i < from.size();i++)
+ {
+ mgKey *k = ktGenerate(from.getKeyType(i));
+ k->set(from.getKeyItem(i));
+ Keys.push_back(k);
+ }
+ m_orderByCount=from.m_orderByCount;
+}
+
+string
+mgOrder::Name()
+{
+ string result="";
+ for (unsigned int idx=0;idx<size();idx++)
+ {
+ if (!result.empty()) result += ":";
+ result += ktName(Keys[idx]->Type());
+ }
+ return result;
+}
+
+void
+mgOrder::setKey (const mgKeyTypes kt)
+{
+ mgKey *newkey = ktGenerate(kt);
+ if (newkey)
+ Keys.push_back(newkey);
+}
+
+mgOrder::mgOrder(mgValmap& nv,char *prefix)
+{
+ char *idx;
+ asprintf(&idx,"%s.OrderByCount",prefix);
+ m_orderByCount = nv.getbool(idx);
+ free(idx);
+ clear();
+ for (unsigned int i = 0; i < 999 ; i++)
+ {
+ asprintf(&idx,"%s.Keys.%u.Type",prefix,i);
+ unsigned int v = nv.getuint(idx);
+ free(idx);
+ if (v==0) break;
+ setKey (mgKeyTypes(v) );
+ }
+ if (size()>0)
+ clean();
+}
+
+void
+mgOrder::DumpState(mgValmap& nv, char *prefix) const
+{
+ char n[100];
+ sprintf(n,"%s.OrderByCount",prefix);
+ nv.put(n,m_orderByCount);
+ for (unsigned int i=0;i<size();i++)
+ {
+ sprintf(n,"%s.Keys.%d.Type",prefix,i);
+ nv.put(n,int(Key(i)->Type()));
+ }
+}
+
+mgOrder::mgOrder(vector<mgKeyTypes> kt)
+{
+ m_orderByCount = false;
+ setKeys(kt);
+}
+
+void
+mgOrder::setKeys(vector<mgKeyTypes> kt)
+{
+ clear();
+ for (unsigned int i=0;i<kt.size();i++)
+ setKey(kt[i]);
+ clean();
+}
+
+
+mgKeyTypes
+mgOrder::getKeyType(unsigned int idx) const
+{
+ assert(idx<Keys.size());
+ return Keys[idx]->Type();
+}
+
+mgSelItem&
+mgOrder::getKeyItem(unsigned int idx) const
+{
+ assert(idx<Keys.size());
+ return Keys[idx]->get();
+}
+
+void
+mgOrder::truncate(unsigned int i)
+{
+ while (size()>i)
+ {
+ delete Keys.back();
+ Keys.pop_back();
+ }
+}
+
+void
+mgOrder::clear()
+{
+ truncate(0);
+}
+
+void
+mgOrder::clean()
+{
+ // remove double entries:
+ keyvector::iterator i;
+ keyvector::iterator j;
+ bool collection_found = false;
+ bool collitem_found = false;
+ bool album_found = false;
+ bool tracknb_found = false;
+ bool title_found = false;
+ bool is_unique = false;
+ for (i = Keys.begin () ; i != Keys.end (); ++i)
+ {
+ mgKeyNormal* k = dynamic_cast<mgKeyNormal*>(*i);
+ collection_found |= (k->Type()==keyCollection);
+ collitem_found |= (k->Type()==keyCollectionItem);
+ album_found |= (k->Type()==keyAlbum);
+ tracknb_found |= (k->Type()==keyTrack);
+ title_found |= (k->Type()==keyTitle);
+ is_unique = tracknb_found || (album_found && title_found)
+ || (collection_found && collitem_found);
+ if (is_unique)
+ {
+ for (j = i+1 ; j !=Keys.end(); ++j)
+ delete *j;
+ Keys.erase(i+1,Keys.end ());
+ break;
+ }
+ if (k->Type()==keyYear)
+ {
+ for (j = i+1 ; j != Keys.end(); ++j)
+ if ((*j)->Type() == keyDecade)
+ {
+ delete *j;
+ Keys.erase(j);
+ break;
+ }
+ }
+cleanagain:
+ for (j = i+1 ; j != Keys.end(); ++j)
+ if ((*i)->Type() == (*j)->Type())
+ {
+ delete *j;
+ Keys.erase(j);
+ goto cleanagain;
+ }
+ }
+ if (!is_unique)
+ {
+ if (!album_found)
+ Keys.push_back(ktGenerate(keyAlbum));
+ if (!title_found)
+ Keys.push_back(ktGenerate(keyTitle));
+ }
+}
+
+bool
+mgOrder::isCollectionOrder() const
+{
+ return (size()==2
+ && (Keys[0]->Type()==keyCollection)
+ && (Keys[1]->Type()==keyCollectionItem));
+}
+
+mgParts
+mgOrder::Parts(mgmySql &db,unsigned int level,bool orderby) const
+{
+ mgParts result;
+ result.orderByCount = m_orderByCount;
+ if (level==0 && isCollectionOrder())
+ {
+ // sql command contributed by jarny
+ result.m_sql_select = string("select playlist.title,playlist.id, "
+ "count(*) * (playlistitem.playlist is not null) from playlist "
+ "left join playlistitem on playlist.id = playlistitem.playlist "
+ "group by playlist.title");
+ return result;
+ }
+ for (unsigned int i=0;i<=level;i++)
+ {
+ if (i==Keys.size()) break;
+ mgKeyNormal *k = dynamic_cast<mgKeyNormal*>(Keys[i]);
+ mgKeyTypes kt = k->Type();
+ if (iskeyGenre(kt))
+ {
+ for (unsigned int j=i+1;j<=level;j++)
+ {
+ if (j>=Keys.size())
+ break;
+ mgKeyNormal *kn = dynamic_cast<mgKeyNormal*>(Keys[j]);
+ if (kn)
+ {
+ mgKeyTypes knt = kn->Type();
+ if (iskeyGenre(knt) && knt>kt && !kn->id().empty())
+ goto next;
+ }
+ }
+ }
+ result += k->Parts(db,orderby && (i==level));
+next:
+ continue;
+ }
+ return result;
+}
+
+//! \brief right now thread locking should not be needed here
+mgReferences::mgReferences()
+{
+ push_back(mgReference ("tracks","id","playlistitem","trackid"));
+ push_back(mgReference ("playlist","id","playlistitem","playlist"));
+ push_back(mgReference ("tracks","sourceid","album","cddbid"));
+ push_back(mgReference ("tracks","lang","language","id"));
+}
+
+bool
+mgReferences::Equal(unsigned int i,string table1, string table2) const
+{
+ const mgReference& r = operator[](i);
+ string s1 = r.t1();
+ string s2 = r.t2();
+ return ((s1==table1) && (s2==table2))
+ || ((s1==table2) && (s2==table1));
+}
+
+mgParts
+mgReferences::FindConnectionBetween(string table1, string table2) const
+{
+ for (unsigned int i=0 ; i<size(); i++ )
+ if (Equal(i,table1,table2))
+ return mgRefParts(operator[](i));
+ return mgParts();
+}
+
+mgParts
+mgReferences::ConnectToTracks(string table) const
+{
+ mgParts result;
+ if (table=="tracks")
+ return result;
+ result += FindConnectionBetween(table,"tracks");
+ if (result.empty())
+ {
+ result += FindConnectionBetween(table,"playlistitem");
+ if (!result.empty())
+ {
+ result += FindConnectionBetween("playlistitem","tracks");
+ }
+ else
+ assert(false);
+ }
+ return result;
+}
+
+mgParts
+mgReferences::Connect(string table1, string table2) const
+{
+ mgParts result;
+ // same table?
+ if (table1 == table2) return result;
+ if (table1=="genre") return ConnectToTracks(table2);
+ if (table2=="genre") return ConnectToTracks(table1);
+ // do not connect aliases. See sql_delete_from_collection
+ if (table1.find(" as ")!=string::npos) return result;
+ if (table2.find(" as ")!=string::npos) return result;
+ if (table1.find(" AS ")!=string::npos) return result;
+ if (table2.find(" AS ")!=string::npos) return result;
+ // direct connection?
+ result += FindConnectionBetween(table1,table2);
+ if (result.empty())
+ {
+ // indirect connection? try connecting via tracks
+ result += ConnectToTracks(table1);
+ result += ConnectToTracks(table2);
+ }
+ return result;
+}
+
+
+mgKey*
+ktGenerate(const mgKeyTypes kt)
+{
+ mgKey* result = 0;
+ switch (kt)
+ {
+ case keyGenres: result = new mgKeyGenres;break;
+ case keyGenre1: result = new mgKeyGenre1;break;
+ case keyGenre2: result = new mgKeyGenre2;break;
+ case keyGenre3: result = new mgKeyGenre3;break;
+ case keyFolder1:result = new mgKeyFolder1;break;
+ case keyFolder2:result = new mgKeyFolder2;break;
+ case keyFolder3:result = new mgKeyFolder3;break;
+ case keyFolder4:result = new mgKeyFolder4;break;
+ case keyArtist: result = new mgKeyNormal(kt,"tracks","artist");break;
+ case keyArtistABC: result = new mgKeyABC(kt,"tracks","artist");break;
+ case keyTitle: result = new mgKeyNormal(kt,"tracks","title");break;
+ case keyTitleABC: result = new mgKeyABC(kt,"tracks","title");break;
+ case keyTrack: result = new mgKeyTrack;break;
+ case keyDecade: result = new mgKeyDecade;break;
+ case keyAlbum: result = new mgKeyAlbum;break;
+ case keyCreated: result = new mgKeyDate(kt,"tracks","created");break;
+ case keyModified: result = new mgKeyDate(kt,"tracks","modified");break;
+ case keyCollection: result = new mgKeyCollection;break;
+ case keyCollectionItem: result = new mgKeyCollectionItem;break;
+ case keyLanguage: result = new mgKeyLanguage;break;
+ case keyRating: result = new mgKeyNormal(kt,"tracks","rating");break;
+ case keyYear: result = new mgKeyNormal(kt,"tracks","year");break;
+ }
+ return result;
+}
+
+const char * const
+ktName(const mgKeyTypes kt)
+{
+ const char * result = "";
+ switch (kt)
+ {
+ case keyGenres: result = "Genre";break;
+ case keyGenre1: result = "Genre1";break;
+ case keyGenre2: result = "Genre2";break;
+ case keyGenre3: result = "Genre3";break;
+ case keyFolder1: result = "Folder1";break;
+ case keyFolder2: result = "Folder2";break;
+ case keyFolder3: result = "Folder3";break;
+ case keyFolder4: result = "Folder4";break;
+ case keyArtist: result = "Artist";break;
+ case keyArtistABC: result = "ArtistABC";break;
+ case keyTitle: result = "Title";break;
+ case keyTitleABC: result = "TitleABC";break;
+ case keyTrack: result = "Track";break;
+ case keyDecade: result = "Decade";break;
+ case keyAlbum: result = "Album";break;
+ case keyCreated: result = "Created";break;
+ case keyModified: result = "Modified";break;
+ case keyCollection: result = "Collection";break;
+ case keyCollectionItem: result = "Collection item";break;
+ case keyLanguage: result = "Language";break;
+ case keyRating: result = "Rating";break;
+ case keyYear: result = "Year";break;
+ }
+ return tr(result);
+}
+
+mgKeyTypes
+ktValue(const char * name)
+{
+ for (int kt=int(mgKeyTypesLow);kt<=int(mgKeyTypesHigh);kt++)
+ if (!strcmp(name,ktName(mgKeyTypes(kt))))
+ return mgKeyTypes(kt);
+ mgError("ktValue(%s): unknown name",name);
+ return mgKeyTypes(0);
+}
+
+
+vector<const char*>
+ktNames()
+{
+ static vector<const char*> result;
+ for (unsigned int i = int(mgKeyTypesLow); i <= int(mgKeyTypesHigh); i++)
+ result.push_back(ktName(mgKeyTypes(i)));
+ return result;
+}
+
diff --git a/muggle-plugin/mg_order.h b/muggle-plugin/mg_order.h
new file mode 100644
index 0000000..8bbaaa5
--- /dev/null
+++ b/muggle-plugin/mg_order.h
@@ -0,0 +1,192 @@
+#ifndef _MG_SQL_H
+#define _MG_SQL_H
+#include <stdlib.h>
+#include <typeinfo>
+#include <string>
+#include <list>
+#include <vector>
+#include <sstream>
+#include "mg_valmap.h"
+#include "mg_mysql.h"
+
+using namespace std;
+
+typedef list<string> strlist;
+
+strlist& operator+=(strlist&a, strlist b);
+
+//! \brief adds string n to string s, using string sep to separate them
+string& addsep (string & s, string sep, string n);
+
+enum mgKeyTypes {
+ keyGenre1=1, // the genre types must have exactly this order!
+ keyGenre2,
+ keyGenre3,
+ keyGenres,
+ keyDecade,
+ keyYear,
+ keyArtist,
+ keyAlbum,
+ keyTitle,
+ keyTrack,
+ keyLanguage,
+ keyRating,
+ keyFolder1,
+ keyFolder2,
+ keyFolder3,
+ keyFolder4,
+ keyCreated,
+ keyModified,
+ keyArtistABC,
+ keyTitleABC,
+ keyCollection,
+ keyCollectionItem,
+};
+const mgKeyTypes mgKeyTypesLow = keyGenre1;
+const mgKeyTypes mgKeyTypesHigh = keyCollectionItem;
+const unsigned int mgKeyTypesNr = keyCollectionItem;
+
+bool iskeyGenre(mgKeyTypes kt);
+
+class mgParts;
+
+class mgReference {
+ public:
+ mgReference(string t1,string f1,string t2,string f2);
+ string t1() const { return m_t1; }
+ string t2() const { return m_t2; }
+ string f1() const { return m_f1; }
+ string f2() const { return m_f2; }
+ private:
+ string m_t1;
+ string m_t2;
+ string m_f1;
+ string m_f2;
+};
+
+class mgReferences : public vector<mgReference> {
+public:
+ // \todo memory leak for vector ref?
+ mgReferences();
+ mgParts Connect(string c1, string c2) const;
+private:
+ bool Equal(unsigned int i,string table1, string table2) const;
+ mgParts FindConnectionBetween(string table1, string table2) const;
+ mgParts ConnectToTracks(string table) const;
+};
+
+class mgSelItem
+{
+ public:
+ mgSelItem();
+ mgSelItem(string v,string i,unsigned int c=0);
+ void set(string v,string i,unsigned int c=0);
+ void operator=(const mgSelItem& from);
+ void operator=(const mgSelItem* from);
+ bool operator==(const mgSelItem& other) const;
+ string value() const { return m_value; }
+ string id() const { return m_id; }
+ unsigned int count() const { return m_count; }
+ bool valid() const { return m_valid; }
+ private:
+ bool m_valid;
+ string m_value;
+ string m_id;
+ unsigned int m_count;
+};
+
+class mgKey {
+ public:
+ virtual ~mgKey() {};
+ virtual mgParts Parts(mgmySql &db,bool orderby=false) const = 0;
+ virtual string id() const = 0;
+ virtual bool valid() const = 0;
+ virtual string value () const = 0;
+ //!\brief translate field into user friendly string
+ virtual void set(mgSelItem& item) = 0;
+ virtual mgSelItem& get() = 0;
+ virtual mgKeyTypes Type() const = 0;
+ virtual string map_idfield() const { return ""; }
+ virtual string map_valuefield() const { return ""; }
+ virtual string map_valuetable() const { return ""; }
+ virtual bool Enabled(mgmySql &db) { return true; }
+};
+
+
+mgKey*
+ktGenerate(const mgKeyTypes kt);
+
+const char * const ktName(const mgKeyTypes kt);
+mgKeyTypes ktValue(const char * name);
+vector < const char*> ktNames();
+
+typedef vector<mgKey*> keyvector;
+
+class mgParts {
+public:
+ mgParts();
+ ~mgParts();
+ strlist fields;
+ strlist tables;
+ strlist clauses;
+ strlist groupby;
+ strlist orders;
+ mgParts& operator+=(mgParts a);
+ void Prepare();
+ string sql_count();
+ string sql_select(bool distinct=true);
+ string sql_delete_from_collection(string pid);
+ string sql_update(strlist new_values);
+ bool empty() const { return tables.size()==0;}
+ string m_sql_select;
+ bool orderByCount;
+private:
+ bool UsesTracks();
+ mgReferences ref;
+};
+
+//! \brief converts long to string
+string itos (int i);
+
+//! \brief convert long to string
+string ltos (long l);
+
+
+const unsigned int MaxKeys = 20;
+
+class mgOrder {
+public:
+ mgOrder();
+ mgOrder(const mgOrder &from);
+ mgOrder(mgValmap& nv, char *prefix);
+ mgOrder(vector<mgKeyTypes> kt);
+ ~mgOrder();
+ void InitFrom(const mgOrder &from);
+ void DumpState(mgValmap& nv, char *prefix) const;
+ mgParts Parts(mgmySql &db,const unsigned int level,bool orderby=true) const;
+ const mgOrder& operator=(const mgOrder& from);
+ mgKey*& operator[](unsigned int idx);
+ unsigned int size() const { return Keys.size(); }
+ void truncate(unsigned int i);
+ bool empty() const { return Keys.empty(); }
+ void clear();
+ mgKey* Key(unsigned int idx) const;
+ mgKeyTypes getKeyType(unsigned int idx) const;
+ mgSelItem& getKeyItem(unsigned int idx) const;
+ void setKeys(vector<mgKeyTypes> kt);
+ string Name();
+ void setOrderByCount(bool orderbycount) { m_orderByCount = orderbycount;}
+ bool getOrderByCount() { return m_orderByCount; }
+private:
+ bool m_orderByCount;
+ bool isCollectionOrder() const;
+ keyvector Keys;
+ void setKey ( const mgKeyTypes kt);
+ void clean();
+};
+
+bool operator==(const mgOrder& a,const mgOrder&b); //! \brief compares only the order, not the current key values
+
+extern mgSelItem zeroitem;
+
+#endif // _MG_SQL_H
diff --git a/muggle-plugin/mg_selection.c b/muggle-plugin/mg_selection.c
new file mode 100644
index 0000000..464bddb
--- /dev/null
+++ b/muggle-plugin/mg_selection.c
@@ -0,0 +1,1149 @@
+/*!
+ * \file mg_selection.c
+ * \brief A general interface to data items, currently only GiantDisc
+ *
+ * \version $Revision: 1.0 $
+ * \date $Date: 2004-12-07 10:10:35 +0200 (Tue, 07 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr $
+ *
+ */
+
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/stat.h>
+#include <stdio.h>
+#include <fts.h>
+#include <assert.h>
+
+#include "i18n.h"
+#include "mg_selection.h"
+#include "vdr_setup.h"
+#include "mg_tools.h"
+#include "mg_thread_sync.h"
+
+#include <mpegfile.h>
+#include <flacfile.h>
+#include <id3v2tag.h>
+#include <fileref.h>
+
+#if VDRVERSNUM >= 10307
+#include <vdr/interface.h>
+#include <vdr/skins.h>
+#endif
+
+//! \brief adds string n to string s, using a comma to separate them
+static string comma (string & s, string n);
+
+/*! \brief returns a random integer within some range
+ */
+unsigned int
+randrange (const unsigned int high)
+{
+ unsigned int result=0;
+ result = random () % high;
+ return result;
+}
+
+
+static string
+comma (string & s, string n)
+{
+ return addsep (s, ",", n);
+}
+
+
+void
+mgSelection::mgSelItems::clear()
+{
+ m_items.clear();
+}
+
+bool
+mgSelection::mgSelItems::operator==(const mgSelItems&x) const
+{
+ bool result = m_items.size()==x.m_items.size();
+ if (result)
+ for (unsigned int i=0;i<size();i++)
+ result &= m_items[i]==x.m_items[i];
+ return result;
+}
+
+size_t
+mgSelection::mgSelItems::size() const
+{
+ if (!m_sel)
+ mgError("mgSelItems: m_sel is 0");
+ m_sel->refreshValues();
+ return m_items.size();
+}
+
+mgSelItem&
+mgSelection::mgSelItems::operator[](unsigned int idx)
+{
+ if (!m_sel)
+ mgError("mgSelItems: m_sel is 0");
+ m_sel->refreshValues();
+ if (idx>=size()) return zeroitem;
+ return m_items[idx];
+}
+
+void
+mgSelection::mgSelItems::setOwner(mgSelection* sel)
+{
+ m_sel = sel;
+}
+
+unsigned int
+mgSelection::mgSelItems::valindex (const string v) const
+{
+ return index(v,true);
+}
+
+unsigned int
+mgSelection::mgSelItems::idindex (const string i) const
+{
+ return index(i,true);
+}
+
+unsigned int
+mgSelection::mgSelItems::index (const string s,bool val,bool second_try) const
+{
+ if (!m_sel)
+ mgError("mgSelItems::index(%s): m_sel is 0",s.c_str());
+ m_sel->refreshValues();
+ for (unsigned int i = 0; i < size (); i++)
+ {
+ if (val)
+ {
+ if (m_items[i].value() == s)
+ return i;
+ }
+ else
+ {
+ if (m_items[i].id() == s)
+ return i;
+ }
+ }
+ // nochmal mit neuen Werten:
+ if (second_try) {
+ esyslog("index: Gibt es nicht:%s",s.c_str());
+ return 0;
+ }
+ else
+ {
+ m_sel->clearCache();
+ return index(s,val,true);
+ }
+}
+
+void
+mgSelection::clearCache() const
+{
+ m_current_values = "";
+ m_current_tracks = "";
+}
+
+string
+mgSelection::getCurrentValue()
+{
+ return items[gotoPosition()].value();
+}
+
+mgSelItem&
+mgSelection::getKeyItem(const unsigned int level) const
+{
+ return order.getKeyItem(level);
+}
+
+
+mgKeyTypes
+mgSelection::getKeyType (const unsigned int level) const
+{
+ return order.getKeyType(level);
+}
+
+mgContentItem *
+mgSelection::getTrack (unsigned int position)
+{
+ if (position >= getNumTracks ())
+ return 0;
+ return &(m_tracks[position]);
+}
+
+
+mgSelection::ShuffleMode mgSelection::toggleShuffleMode ()
+{
+ setShuffleMode((m_shuffle_mode == SM_PARTY) ? SM_NONE : ShuffleMode (m_shuffle_mode + 1));
+ Shuffle();
+ return getShuffleMode();
+}
+
+void
+mgSelection::setShuffleMode (mgSelection::ShuffleMode mode)
+{
+ m_shuffle_mode = mode;
+}
+
+void
+mgSelection::Shuffle() const
+{
+ unsigned int tracksize = getNumTracks();
+ if (tracksize==0) return;
+ switch (m_shuffle_mode)
+ {
+ case SM_NONE:
+ {
+ long trackid = m_tracks[getTrackPosition()].getTrackid ();
+ m_current_tracks = ""; // force a reload
+ tracksize = getNumTracks(); // getNumTracks also reloads
+ for (unsigned int i = 0; i < tracksize; i++)
+ if (m_tracks[i].getTrackid () == trackid)
+ {
+ setTrackPosition(i);
+ break;
+ }
+ }
+ break;
+ case SM_PARTY:
+ case SM_NORMAL:
+ {
+ // play all, beginning with current track:
+ mgContentItem tmp = m_tracks[getTrackPosition()];
+ m_tracks[getTrackPosition()]=m_tracks[0];
+ m_tracks[0]=tmp;
+ setTrackPosition(0);
+ // randomize all other tracks
+ for (unsigned int i = 1; i < tracksize; i++)
+ {
+ unsigned int j = 1+randrange (tracksize-1);
+ tmp = m_tracks[i];
+ m_tracks[i] = m_tracks[j];
+ m_tracks[j] = tmp;
+ }
+ } break;
+/*
+ * das kapiere ich nicht... (wolfgang)
+ - Party mode (see iTunes)
+ - initialization
+ - find 15 titles according to the scheme below
+ - playing
+ - before entering next title perform track selection
+ - track selection
+ - generate a random uid
+ - if file exists:
+ - determine maximum playcount of all tracks
+- generate a random number n
+- if n < playcount / max. playcount
+- add the file to the end of the list
+*/
+ }
+}
+
+
+mgSelection::LoopMode mgSelection::toggleLoopMode ()
+{
+ m_loop_mode = (m_loop_mode == LM_FULL) ? LM_NONE : LoopMode (m_loop_mode + 1);
+ return m_loop_mode;
+}
+
+
+unsigned int
+mgSelection::AddToCollection (const string Name)
+{
+ if (!m_db.Connected()) return 0;
+ CreateCollection(Name);
+ string listid = m_db.sql_string (m_db.get_col0
+ ("SELECT id FROM playlist WHERE title=" + m_db.sql_string (Name)));
+ unsigned int tracksize = getNumTracks ();
+ if (tracksize==0)
+ return 0;
+
+ // this code is rather complicated but works in a multi user
+ // environment:
+
+ // insert a unique trackid:
+ string trackid = ltos(m_db.thread_id()+1000000);
+ m_db.exec_sql("INSERT INTO playlistitem SELECT "+listid+","
+ "MAX(tracknumber)+"+ltos(tracksize)+","+trackid+
+ " FROM playlistitem WHERE playlist="+listid);
+
+ // find tracknumber of the trackid we just inserted:
+ string sql = string("SELECT tracknumber FROM playlistitem WHERE "
+ "playlist=")+listid+" AND trackid="+trackid;
+ long first = atol(m_db.get_col0(sql).c_str()) - tracksize + 1;
+
+ // replace the place holder trackid by the correct value:
+ m_db.exec_sql("UPDATE playlistitem SET trackid="+ltos(m_tracks[tracksize-1].getTrackid())+
+ " WHERE playlist="+listid+" AND trackid="+trackid);
+
+ // insert all other tracks:
+ const char *sql_prefix = "INSERT INTO playlistitem VALUES ";
+ sql = "";
+ for (unsigned int i = 0; i < tracksize-1; i++)
+ {
+ string item = "(" + listid + "," + ltos (first + i) + "," +
+ ltos (m_tracks[i].getTrackid ()) + ")";
+ comma(sql, item);
+ if ((i%100)==99)
+ {
+ m_db.exec_sql (sql_prefix+sql);
+ sql = "";
+ }
+ }
+ if (!sql.empty()) m_db.exec_sql (sql_prefix+sql);
+ if (inCollection(Name)) clearCache ();
+ return tracksize;
+}
+
+
+unsigned int
+mgSelection::RemoveFromCollection (const string Name)
+{
+ if (!m_db.Connected()) return 0;
+ mgParts p = order.Parts(m_db,m_level,false);
+ string sql = p.sql_delete_from_collection(id(keyCollection,Name));
+ m_db.exec_sql (sql);
+ unsigned int removed = m_db.affected_rows ();
+ if (inCollection(Name)) clearCache ();
+ return removed;
+}
+
+
+bool mgSelection::DeleteCollection (const string Name)
+{
+ if (!m_db.Connected()) return false;
+ ClearCollection(Name);
+ m_db.exec_sql ("DELETE FROM playlist WHERE title=" + m_db.sql_string (Name));
+ if (isCollectionlist()) clearCache ();
+ return (m_db.affected_rows () == 1);
+}
+
+
+void mgSelection::ClearCollection (const string Name)
+{
+ if (!m_db.Connected()) return;
+ string listid = id(keyCollection,Name);
+ m_db.exec_sql ("DELETE FROM playlistitem WHERE playlist="+m_db.sql_string(listid));
+ if (inCollection(Name)) clearCache ();
+}
+
+
+bool mgSelection::CreateCollection(const string Name)
+{
+ if (!m_db.Connected()) return false;
+ string name = m_db.sql_string(Name);
+ if (m_db.exec_count("SELECT count(title) FROM playlist WHERE title = " + name)>0)
+ return false;
+ m_db.exec_sql ("INSERT playlist VALUES(" + name + ",'VDR',NULL,NULL,NULL)");
+ if (isCollectionlist()) clearCache ();
+ return true;
+}
+
+
+string mgSelection::exportM3U ()
+{
+
+// open a file for writing
+ string fn = "/tmp/" + ListFilename () + ".m3u";
+ FILE * listfile = fopen (fn.c_str (), "w");
+ if (!listfile)
+ return "";
+ fprintf (listfile, "#EXTM3U\n");
+ unsigned int tracksize = getNumTracks ();
+ for (unsigned i = 0; i < tracksize; i++)
+ {
+ mgContentItem& t = m_tracks[i];
+ fprintf (listfile, "#EXTINF:%d,%s\n", t.getDuration (),
+ t.getTitle ().c_str ());
+ fprintf (listfile, "#MUGGLE:%ld\n", t.getTrackid());
+ fprintf (listfile, "%s\n", t.getSourceFile (false).c_str ());
+ }
+ fclose (listfile);
+ return fn;
+}
+
+bool
+mgSelection::empty()
+{
+ if (m_level>= order.size ()-1)
+ return ( getNumTracks () == 0);
+ else
+ return ( items.size () == 0);
+}
+
+void
+mgSelection::setPosition (unsigned int position)
+{
+ if (m_level == order.size())
+ setTrackPosition(position);
+ else
+ m_position = position;
+}
+
+void
+mgSelection::setTrackPosition (unsigned int position) const
+{
+ m_tracks_position = position;
+}
+
+unsigned int
+mgSelection::getPosition () const
+{
+ if (m_level == order.size())
+ return getTrackPosition();
+ else
+ return m_position;
+}
+
+unsigned int
+mgSelection::gotoPosition ()
+{
+ if (m_level == order.size ())
+ return gotoTrackPosition();
+ else
+ {
+ unsigned int itemsize = items.size();
+ if (itemsize==0)
+ m_position = 0;
+ else if (m_position >= itemsize)
+ m_position = itemsize -1;
+ return m_position;
+ }
+}
+
+unsigned int
+mgSelection::getTrackPosition() const
+{
+ if (m_tracks_position>=m_tracks.size())
+ if (m_tracks.size()==0)
+ m_tracks_position=0;
+ else
+ m_tracks_position = m_tracks.size()-1;
+ return m_tracks_position;
+}
+
+unsigned int
+mgSelection::gotoTrackPosition()
+{
+ unsigned int tracksize = getNumTracks ();
+ if (tracksize == 0)
+ setTrackPosition(0);
+ else if (m_tracks_position >= tracksize)
+ setTrackPosition(tracksize -1);
+ return m_tracks_position;
+}
+
+bool mgSelection::skipTracks (int steps)
+{
+ unsigned int tracksize = getNumTracks();
+ if (tracksize == 0)
+ return false;
+ if (m_loop_mode == LM_SINGLE)
+ return true;
+ unsigned int old_pos = m_tracks_position;
+ unsigned int new_pos;
+ if (old_pos + steps < 0)
+ {
+ if (m_loop_mode == LM_NONE)
+ return false;
+ new_pos = tracksize - 1;
+ }
+ else
+ new_pos = old_pos + steps;
+ if (new_pos >= tracksize)
+ {
+ if (m_loop_mode == LM_NONE)
+ return false;
+ new_pos = 0;
+ }
+ setTrackPosition (new_pos);
+ return (new_pos == gotoTrackPosition());
+}
+
+
+unsigned long
+mgSelection::getLength ()
+{
+ unsigned long result = 0;
+ unsigned int tracksize = getNumTracks ();
+ for (unsigned int i = 0; i < tracksize; i++)
+ result += m_tracks[i].getDuration ();
+ return result;
+}
+
+
+unsigned long
+mgSelection::getCompletedLength () const
+{
+ unsigned long result = 0;
+ tracks (); // make sure they are loaded
+ for (unsigned int i = 0; i < getTrackPosition(); i++)
+ result += m_tracks[i].getDuration ();
+ return result;
+}
+
+
+
+string mgSelection::getListname () const
+{
+ list<string> st;
+ for (unsigned int i = 0; i < m_level; i++)
+ st.push_back(order.getKeyItem(i).value());
+ st.unique();
+ string result="";
+ for (list < string >::iterator it = st.begin (); it != st.end (); ++it)
+ {
+ addsep (result, ":", *it);
+ }
+ if (result.empty ())
+ if (order.size()>0)
+ result = string(ktName(order.getKeyType(0)));
+ return result;
+}
+
+string mgSelection::ListFilename ()
+{
+ string res = getListname ();
+ // convert char set ?
+ string::iterator it;
+ for (it=res.begin();it!=res.end();it++)
+ {
+ char& c = *it;
+ switch (c)
+ {
+ case '\'':
+ case '/':
+ case '\\':
+ case ' ':
+ case ')':
+ case '(': c = '_';break;
+ }
+ }
+ return res;
+}
+
+const vector < mgContentItem > &
+mgSelection::tracks () const
+{
+ if (!m_db.Connected()) return m_tracks;
+ if (!m_current_tracks.empty()) return m_tracks;
+ mgParts p = order.Parts(m_db,m_level);
+ p.fields.clear();
+ p.fields.push_back("tracks.id");
+ p.fields.push_back("tracks.title");
+ p.fields.push_back("tracks.mp3file");
+ p.fields.push_back("tracks.artist");
+ p.fields.push_back("album.title");
+ p.fields.push_back("tracks.genre1");
+ p.fields.push_back("tracks.genre2");
+ p.fields.push_back("tracks.bitrate");
+ p.fields.push_back("tracks.year");
+ p.fields.push_back("tracks.rating");
+ p.fields.push_back("tracks.length");
+ p.fields.push_back("tracks.samplerate");
+ p.fields.push_back("tracks.channels");
+ p.fields.push_back("tracks.lang");
+ p.tables.push_back("tracks");
+ p.tables.push_back("album");
+ for (unsigned int i = m_level; i<order.size(); i++)
+ p += order.Key(i)->Parts(m_db,true);
+ m_current_tracks = p.sql_select(false);
+ m_tracks.clear ();
+ MYSQL_RES *rows = m_db.exec_sql (m_current_tracks);
+ if (rows)
+ {
+ MYSQL_ROW row;
+ while ((row = mysql_fetch_row (rows)) != 0)
+ m_tracks.push_back (mgContentItem (this,row));
+ mysql_free_result (rows);
+ }
+ if (m_shuffle_mode!=SM_NONE)
+ Shuffle();
+ return m_tracks;
+}
+
+
+void mgSelection::InitSelection() {
+ m_Directory=".";
+ m_level = 0;
+ m_position = 0;
+ m_tracks_position = 0;
+ m_trackid = -1;
+ if (the_setup.InitShuffleMode)
+ m_shuffle_mode = SM_NORMAL;
+ else
+ m_shuffle_mode = SM_NONE;
+ if (the_setup.InitLoopMode)
+ m_loop_mode = LM_FULL;
+ else
+ m_loop_mode = LM_NONE;
+ clearCache();
+ items.setOwner(this);
+}
+
+
+mgSelection::mgSelection (const bool fall_through)
+{
+ InitSelection ();
+ m_fall_through = fall_through;
+}
+
+mgSelection::mgSelection (const mgSelection &s)
+{
+ InitFrom(&s);
+}
+
+mgSelection::mgSelection (const mgSelection* s)
+{
+ InitFrom(s);
+}
+
+mgSelection::mgSelection (mgValmap& nv)
+{
+ InitFrom(nv);
+}
+
+void
+mgSelection::setOrder(mgOrder* o)
+{
+ if (o)
+ {
+ order = *o;
+ }
+ else
+ mgWarning("mgSelection::setOrder(0)");
+}
+
+
+void
+mgSelection::InitFrom(mgValmap& nv)
+{
+ extern time_t createtime;
+ if (m_db.ServerConnected() && !m_db.Connected()
+ && (time(0)>createtime+10))
+ {
+ char *b;
+ asprintf(&b,tr("Create database %s?"),the_setup.DbName);
+ if (Interface->Confirm(b))
+ {
+ char *argv[2];
+ argv[0]=".";
+ argv[1]=0;
+ m_db.Create();
+ if (Interface->Confirm(tr("Import tracks?")))
+ {
+ mgThreadSync *s = mgThreadSync::get_instance();
+ if (s)
+ {
+ extern char *sync_args[];
+ s->Sync(sync_args,the_setup.DeleteStaleReferences);
+ }
+ }
+ }
+ free(b);
+ }
+ InitSelection();
+ m_fall_through = nv.getbool("FallThrough");
+ m_Directory = nv.getstr("Directory");
+ while (m_level < nv.getuint("Level"))
+ {
+ char *idx;
+ asprintf(&idx,"order.Keys.%u.Position",m_level);
+ string newval = nv.getstr(idx);
+ free(idx);
+ if (!enter (newval))
+ if (!select (newval)) break;
+ }
+ m_trackid = nv.getlong("TrackId");
+ setPosition(nv.getstr("Position"));
+ if (m_level>=order.size()-1)
+ setTrackPosition(nv.getlong("TrackPosition"));
+}
+
+
+mgSelection::~mgSelection ()
+{
+}
+
+void mgSelection::InitFrom(const mgSelection* s)
+{
+ InitSelection();
+ m_fall_through = s->m_fall_through;
+ m_Directory = s->m_Directory;
+ map_values = s->map_values;
+ map_ids = s->map_ids;
+ order = s->order;
+ m_level = s->m_level;
+ m_position = s->m_position;
+ m_trackid = s->m_trackid;
+ m_tracks_position = s->m_tracks_position;
+ setShuffleMode (s->getShuffleMode ());
+ setLoopMode (s->getLoopMode ());
+}
+
+unsigned int
+mgSelection::ordersize ()
+{
+ return order.size ();
+}
+
+unsigned int
+mgSelection::valcount (string value)
+{
+ return items[items.valindex(value)].count();
+}
+
+void
+mgSelection::refreshValues () const
+{
+ assert(this);
+ if (!m_db.Connected())
+ return;
+ if (m_current_values.empty())
+ {
+ mgParts p = order.Parts(m_db,m_level);
+ m_current_values = p.sql_select();
+ items.clear ();
+ MYSQL_RES *rows = m_db.exec_sql (m_current_values);
+ if (rows)
+ {
+ unsigned int num_fields = mysql_num_fields(rows);
+ MYSQL_ROW row;
+ while ((row = mysql_fetch_row (rows)) != 0)
+ {
+ if (!row[0]) continue;
+ string r0 = row[0];
+ if (!strcmp(row[0],"NULL")) // there is a genre NULL!
+ continue;
+ mgSelItem n;
+ if (num_fields==3)
+ {
+ if (!row[1]) continue;
+ n.set(row[0],row[1],atol(row[2]));
+ }
+ else
+ n.set(value(order.Key(m_level),r0),r0,atol(row[1]));
+ items.push_back(n);
+ }
+ mysql_free_result (rows);
+ }
+ }
+}
+
+unsigned int
+mgSelection::count () const
+{
+ return items.size ();
+}
+
+
+bool mgSelection::enter (unsigned int position)
+{
+ if (order.empty())
+ esyslog("mgSelection::enter(%u): order is empty", position);
+ if (empty())
+ return false;
+ setPosition (position);
+ position = gotoPosition(); // reload adjusted position
+ if (items.size()==0)
+ return false;
+ mgSelItem item = items[position];
+ mgSelItems prev;
+ if (m_fall_through && items.size()<10)
+ prev=items;
+ while (1)
+ {
+ if (m_level >= order.size () - 1)
+ return false;
+ order[m_level++]->set (item);
+ clearCache();
+ if (m_level >= order.size())
+ mgError("mgSelection::enter(%u): level greater than order.size() %u",
+ m_level,order.size());
+ m_position = 0;
+ refreshValues();
+ if (count()==0)
+ break;
+ item=items[0];
+ if (!m_fall_through)
+ break;
+ if (m_level==order.size()-1)
+ break;
+ if (count () > 1 && !(prev==items))
+ break;
+ }
+ return true;
+}
+
+
+bool mgSelection::select (unsigned int position)
+{
+ if (m_level == order.size () - 1)
+ {
+ if (getNumTracks () <= position)
+ return false;
+ order[m_level]->set (items[position]);
+ m_level++;
+ m_trackid = m_tracks[position].getTrackid ();
+
+ clearCache();
+ return true;
+ }
+ else
+ return enter (position);
+}
+
+bool
+mgSelection::leave ()
+{
+ string prevvalue;
+ if (order.empty())
+ {
+ mgWarning("mgSelection::leave(): order is empty");
+ return false;
+ }
+ if (m_level == order.size ())
+ {
+ m_level--;
+ prevvalue=order.getKeyItem(m_level).value();
+ order[m_level]->set(zeroitem);
+ m_trackid = -1;
+ clearCache();
+ setPosition(prevvalue);
+ return true;
+ }
+ mgSelItems prev;
+ if (m_fall_through && items.size()<10)
+ prev=items;
+ while (1)
+ {
+ if (m_level < 1)
+ return false;
+ if (m_level<order.size()) order[m_level]->set (zeroitem);
+ m_level--;
+ prevvalue=order.getKeyItem(m_level).value();
+ if (m_level<order.size()) order[m_level]->set (zeroitem);
+ clearCache();
+ if (!m_fall_through)
+ break;
+ if (count () > 1 && !(prev==items))
+ break;
+ }
+ setPosition(prevvalue);
+ return true;
+}
+
+void
+mgSelection::leave_all ()
+{
+ m_level=0;
+ for (unsigned int i=0;i<ordersize();i++)
+ order[i]->set (zeroitem);
+ clearCache();
+}
+
+void
+mgSelection::selectfrom(mgOrder& oldorder,mgContentItem* o)
+{
+ leave_all();
+ mgSelItem selitem;
+ assert(m_level==0);
+ for (unsigned int idx = 0; idx < ordersize(); idx++)
+ {
+ selitem = zeroitem;
+ mgKeyTypes new_kt = getKeyType(idx);
+ for (unsigned int i=0;i<oldorder.size();i++)
+ {
+ mgKeyTypes old_kt = oldorder.getKeyType(i);
+ if (old_kt==new_kt)
+ selitem = oldorder.getKeyItem(i);
+ else if (old_kt>new_kt
+ && iskeyGenre(old_kt)
+ && iskeyGenre(new_kt))
+ {
+ string selid=id(new_kt,value(new_kt,oldorder.getKeyItem(i).id()));
+ selitem=mgSelItem (value(new_kt,selid),selid);
+ }
+ if (selitem.valid()) break;
+ }
+ if (!selitem.valid() && o && o->getTrackid()>=0)
+ selitem = o->getKeyItem(new_kt);
+ if (!selitem.valid())
+ break;
+ if (m_level<ordersize()-1)
+ {
+ order[m_level++]->set (selitem);
+ }
+ else
+ {
+ setPosition(selitem.value());
+ return;
+ }
+ }
+ if (m_level>0)
+ {
+ m_level--;
+ selitem = order.getKeyItem(m_level);
+ order[m_level]->set(zeroitem);
+ setPosition(selitem.value());
+ order[m_level+1]->set(zeroitem);
+ }
+ assert(m_level<ordersize());
+}
+
+string
+mgSelection::value(mgKeyTypes kt, string idstr) const
+{
+ if (loadvalues (kt))
+ {
+ map<string,string>& valmap = map_values[kt];
+ map<string,string>::iterator it;
+ it = valmap.find(idstr);
+ if (it!=valmap.end())
+ {
+ string r = it->second;
+ if (!r.empty())
+ return r;
+ }
+ map_ids[kt].clear();
+ loadvalues(kt);
+ it = valmap.find(idstr);
+ if (it!=valmap.end())
+ return valmap[idstr];
+ }
+ return idstr;
+}
+
+string
+mgSelection::value(mgKey* k, string idstr) const
+{
+ return value(k->Type(),idstr);
+}
+
+string
+mgSelection::value(mgKey* k) const
+{
+ return value(k,k->id());
+}
+
+string
+mgSelection::id(mgKeyTypes kt, string val) const
+{
+ if (loadvalues (kt))
+ {
+ map<string,string>& idmap = map_ids[kt];
+ string v = idmap[val];
+ if (kt==keyGenre1) v=v.substr(0,1);
+ if (kt==keyGenre2) v=v.substr(0,2);
+ if (kt==keyGenre3) v=v.substr(0,3);
+ return v;
+ }
+ else
+ return val;
+}
+
+string
+mgSelection::id(mgKey* k, string val) const
+{
+ return id(k->Type(),val);
+}
+
+string
+mgSelection::id(mgKey* k) const
+{
+ return k->id();
+}
+
+bool
+mgSelection::UsedBefore(mgOrder *o,const mgKeyTypes kt,unsigned int level) const
+{
+ if (level>=o->size())
+ level = o->size() -1;
+ for (unsigned int lx = 0; lx < level; lx++)
+ if (o->getKeyType(lx)==kt)
+ return true;
+ return false;
+}
+
+bool mgSelection::isLanguagelist() const
+{
+ return (order.getKeyType(0) == keyLanguage);
+}
+
+bool mgSelection::isCollectionlist () const
+{
+ if (order.size()==0) return false;
+ return (order.getKeyType(0) == keyCollection && m_level == 0);
+}
+
+bool
+mgSelection::inCollection(const string Name) const
+{
+ if (order.size()==0) return false;
+ bool result = (order.getKeyType(0) == keyCollection && m_level == 1);
+ if (result)
+ if (order.getKeyType(1) != keyCollectionItem)
+ mgError("inCollection: key[1] is not keyCollectionItem");
+ if (!Name.empty())
+ result &= (order.getKeyItem(0).value() == Name);
+ return result;
+}
+
+
+void mgSelection::DumpState(mgValmap& nv) const
+{
+ nv.put("FallThrough",m_fall_through);
+ nv.put("Directory",m_Directory);
+ nv.put("Level",int(m_level));
+ for (unsigned int i=0;i<order.size();i++)
+ {
+ if (i<m_level) {
+ char *n;
+ asprintf(&n,"order.Keys.%u.Position",i);
+ nv.put(n,getKeyItem(i).value());
+ free(n);
+ }
+ }
+ nv.put("TrackId",m_trackid);
+ nv.put("Position",items[m_position].value());
+ if (m_level>=order.size()-1)
+ nv.put("TrackPosition",getTrackPosition());
+}
+
+map <mgKeyTypes, string>
+mgSelection::UsedKeyValues()
+{
+ map <mgKeyTypes, string> result;
+ for (unsigned int idx = 0 ; idx < level() ; idx++)
+ {
+ result[order.getKeyType(idx)] = order.getKeyItem(idx).value();
+ }
+ if (level() < order.size()-1)
+ {
+ mgKeyTypes ch = order.getKeyType(level());
+ result[ch] = getCurrentValue();
+ }
+ return result;
+}
+
+bool
+mgSelection::loadvalues (mgKeyTypes kt) const
+{
+ if (map_ids.count(kt)>0)
+ return true;
+ map<string,string>& idmap = map_ids[kt];
+ mgKey* k = ktGenerate(kt);
+ if (k->map_idfield().empty())
+ {
+ delete k;
+ return false;
+ }
+ map<string,string>& valmap = map_values[kt];
+ char *b;
+ asprintf(&b,"select %s,%s from %s;",k->map_idfield().c_str(),k->map_valuefield().c_str(),k->map_valuetable().c_str());
+ MYSQL_RES *rows = m_db.exec_sql (string(b));
+ free(b);
+ if (rows)
+ {
+ MYSQL_ROW row;
+ while ((row = mysql_fetch_row (rows)) != 0)
+ {
+ if (row[0] && row[1])
+ {
+ valmap[row[0]] = row[1];
+ idmap[row[1]] = row[0];
+ }
+ }
+ mysql_free_result (rows);
+ }
+ delete k;
+ return true;
+}
+
+static vector<int> keycounts;
+
+unsigned int
+mgSelection::keycount(mgKeyTypes kt)
+{
+ if (keycounts.size()==0)
+ {
+ for (unsigned int ki=int(mgKeyTypesLow);ki<=int(mgKeyTypesHigh);ki++)
+ {
+ keycounts.push_back(-1);
+ }
+ }
+ int& count = keycounts[int(kt-mgKeyTypesLow)];
+ if (count==-1)
+ {
+ mgKey* k = ktGenerate(kt);
+ if (k->Enabled(m_db))
+ count = m_db.exec_count(k->Parts(m_db,true).sql_count());
+ else
+ count = 0;
+ delete k;
+ }
+ return count;
+}
+
+
+vector <const char *>
+mgSelection::choices(mgOrder *o,unsigned int level, unsigned int *current)
+{
+ vector<const char*> result;
+ if (level>o->size())
+ {
+ *current = 0;
+ return result;
+ }
+ for (unsigned int ki=int(mgKeyTypesLow);ki<=int(mgKeyTypesHigh);ki++)
+ {
+ mgKeyTypes kt = mgKeyTypes(ki);
+ if (kt==o->getKeyType(level))
+ {
+ *current = result.size();
+ result.push_back(ktName(kt));
+ continue;
+ }
+ if (UsedBefore(o,kt,level))
+ continue;
+ if (kt==keyDecade && UsedBefore(o,keyYear,level))
+ continue;
+ if (kt==keyGenre1)
+ {
+ if (UsedBefore(o,keyGenre2,level)) continue;
+ if (UsedBefore(o,keyGenre3,level)) continue;
+ if (UsedBefore(o,keyGenres,level)) continue;
+ }
+ if (kt==keyGenre2)
+ {
+ if (UsedBefore(o,keyGenre3,level)) continue;
+ if (UsedBefore(o,keyGenres,level)) continue;
+ }
+ if (kt==keyGenre3)
+ {
+ if (UsedBefore(o,keyGenres,level)) continue;
+ }
+ if (kt==keyFolder1)
+ {
+ if (UsedBefore(o,keyFolder2,level)) continue;
+ if (UsedBefore(o,keyFolder3,level)) continue;
+ if (UsedBefore(o,keyFolder4,level)) continue;
+ }
+ if (kt==keyFolder2)
+ {
+ if (UsedBefore(o,keyFolder3,level)) continue;
+ if (UsedBefore(o,keyFolder4,level)) continue;
+ }
+ if (kt==keyFolder3)
+ {
+ if (UsedBefore(o,keyFolder4,level)) continue;
+ }
+ if (kt==keyCollection || kt==keyCollectionItem)
+ result.push_back(ktName(kt));
+ else if (keycount(kt)>1)
+ result.push_back(ktName(kt));
+ }
+ return result;
+}
+
diff --git a/muggle-plugin/mg_selection.h b/muggle-plugin/mg_selection.h
new file mode 100644
index 0000000..f9fa455
--- /dev/null
+++ b/muggle-plugin/mg_selection.h
@@ -0,0 +1,468 @@
+/*!
+ * \file mg_selection.h
+ * \brief A general interface to data items, currently only GiantDisc
+ *
+ * \version $Revision: 1.0 $
+ * \date $Date: 2004-12-07 10:10:35 +0200 (Tue, 07 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr $
+ *
+ */
+
+#ifndef _MG_SELECTION_H
+#define _MG_SELECTION_H
+#include <stdlib.h>
+#include <string>
+#include <list>
+#include <vector>
+#include <map>
+using namespace std;
+
+#include "mg_tools.h"
+#include "mg_valmap.h"
+#include "mg_order.h"
+#include "mg_content.h"
+
+typedef vector<string> strvector;
+
+/*!
+ * \brief the only interface to the database.
+ * Some member functions are declared const although they can modify the inner state of mgSelection.
+ * But they only modify variables used for caching. With const, we want to express
+ * the logical constness. E.g. the selected tracks can change without breaking constness:
+ * The selection never defines concrete tracks but only how to choose them.
+ */
+class mgSelection
+{
+ class mgSelItems
+ {
+ public:
+ mgSelItems() { m_sel=0; }
+ void setOwner(mgSelection* sel);
+ mgSelItem& operator[](unsigned int idx);
+ string& id(unsigned int);
+ unsigned int count(unsigned int);
+ bool operator==(const mgSelItems&x) const;
+ size_t size() const;
+ unsigned int valindex (const string v) const;
+ unsigned int idindex (const string i) const;
+ void clear();
+ void push_back(mgSelItem& item) { m_items.push_back(item); }
+ private:
+ unsigned int index (const string s,bool val,bool second_try=false) const;
+ vector<mgSelItem> m_items;
+ mgSelection* m_sel;
+ };
+ public:
+//! \brief defines an order to be used
+ void setOrder(mgOrder *o);
+
+ mgOrder& getOrder() { return order; }
+
+/*! \brief define various ways to play music in random order
+ * \todo Party mode is not implemented, does same as SM_NORMAL
+ */
+ enum ShuffleMode
+ {
+ SM_NONE, //!< \brief play normal sequence
+ SM_NORMAL, //!< \brief a shuffle with a fair distribution
+ SM_PARTY //!< \brief select the next few songs randomly, continue forever
+ };
+
+//! \brief define various ways to play music in a neverending loop
+ enum LoopMode
+ {
+ LM_NONE, //!< \brief do not loop
+ LM_SINGLE, //!< \brief loop a single track
+ LM_FULL //!< \brief loop the whole track list
+ };
+
+/*! \brief the main constructor
+ * \param fall_through if TRUE: If enter() returns a choice
+ * containing only one item, that item is automatically entered.
+ * The analog happens with leave()
+ */
+ mgSelection ( const bool fall_through = false);
+
+/*! \brief a copy constructor. Does a deep copy.
+ * Some of the data base content will only be retrieved by the
+ * new mgSelection as needed, so some data base
+ * overhead is involved
+ */
+ mgSelection (const mgSelection& s);
+/*! \brief a copy constructor. Does a deep copy.
+ * Some of the data base content will only be retrieved by the
+ * new mgSelection as needed, so some data base
+ * overhead is involved
+ */
+ mgSelection (const mgSelection* s);
+
+
+//! \brief initializes from a map.
+ void InitFrom(mgValmap& nv);
+
+//! \brief the normal destructor
+ ~mgSelection ();
+
+/*! \brief represents all items for the current level. The result
+ * is cached, subsequent accesses to values only incur a
+ * small overhead for building the SQL WHERE command. The items will
+ * be reloaded when the SQL command changes
+ */
+ mutable mgSelItems items;
+
+/*! \brief returns the name of a key
+ */
+ mgKeyTypes getKeyType (const unsigned int level) const;
+
+//! \brief return the current value of this key
+ mgSelItem& getKeyItem (const unsigned int level) const;
+
+/*! \brief returns the current item from the value() list
+ */
+ string getCurrentValue();
+
+//! \brief returns a map (new allocated) for all used key fields and their values
+ map<mgKeyTypes,string> UsedKeyValues();
+
+//! \brief the number of key fields used for the query
+ unsigned int ordersize ();
+
+//! \brief the number of music items currently selected
+ unsigned int count () const;
+
+//! \brief the current position
+ unsigned int getPosition ()const;
+
+ //! \brief go to the current position. If it does not exist,
+ // go to the nearest.
+ unsigned int gotoPosition ();
+
+
+//! \brief the current position in the tracks list
+ unsigned int getTrackPosition () const;
+
+ //! \brief go to the current track position. If it does not exist,
+ // go to the nearest.
+ unsigned int gotoTrackPosition ();
+
+/*! \brief enter the the next higher level, go one up in the tree.
+ * If fall_through (see constructor) is set to true, and the
+ * level entered by enter() contains only one item, automatically
+ * goes down further until a level with more than one item is reached.
+ * \param position is the position in the current level that is to be expanded
+ * \return returns false if there is no further level
+ */
+ bool enter (unsigned int position);
+
+ /*! \brief like enter but if we are at the leaf level simply select
+ * the entry at position
+ */
+ bool select (unsigned int position);
+
+/*! \brief enter the next higher level, expanding the current position.
+ * See also enter(unsigned int position)
+ */
+ bool enter ()
+ {
+ return enter (gotoPosition ());
+ }
+
+ /*! \brief like enter but if we are at the leaf level simply select
+ * the current entry
+ */
+ bool select ()
+ {
+ return select (gotoPosition ());
+ }
+
+/*! \brief enter the next higher level, expanding the position holding a certain value
+ * \param value the position holding value will be expanded.
+ */
+ bool enter (const string value)
+ {
+ return enter (items.valindex (value));
+ }
+
+ /*! \brief like enter but if we are at the leaf level simply select
+ * the current entry
+ */
+ bool select (const string value)
+ {
+ return select (items.valindex(value));
+ }
+
+ bool selectid (const string i)
+ {
+ return select(items.idindex(i));
+ }
+
+ void selectfrom(mgOrder& oldorder,mgContentItem* o);
+
+/*! \brief leave the current level, go one up in the tree.
+ * If fall_through (see constructor) is set to true, and the
+ * level entered by leave() contains only one item, automatically
+ * goes up further until a level with more than one item is reached.
+ * \return returns false if there is no further upper level
+ */
+ bool leave ();
+
+/*! \brief leave the current level, go up in the tree until
+ * target level is reached.
+ * If fall_through (see constructor) is set to true, and the
+ * level entered by leave() contains only one item, automatically
+ * goes up further until a level with more than one item is reached.
+ * \return returns false if there is no further upper level
+ */
+ void leave_all ();
+
+//! \brief the current level in the tree
+ unsigned int level () const
+ {
+ return m_level;
+ }
+
+ //! \brief true if the selection holds no items
+ bool empty();
+
+/*! \brief returns detailed info about all selected tracks.
+ * The ordering is done only by the keyfield of the current level.
+ * This might have to be changed - suborder by keyfields of detail
+ * levels. This list is cached so several consequent calls mean no
+ * loss of performance. See value(), the same warning applies.
+ * \todo call this more seldom. See getNumTracks()
+ */
+ const vector < mgContentItem > &tracks () const;
+
+/*! \brief returns an item from the tracks() list
+ * \param position the position in the tracks() list
+ * \return returns NULL if position is out of range
+ */
+ mgContentItem* getTrack (unsigned int position);
+
+/*! \brief returns the current item from the tracks() list
+ */
+ mgContentItem* getCurrentTrack ()
+ {
+ return getTrack (gotoTrackPosition());
+ }
+
+/*! \brief toggles the shuffle mode thru all possible values.
+ * When a shuffle modus SM_NORMAL or SM_PARTY is selected, the
+ * order of the tracks in the track list will be randomly changed.
+ */
+ ShuffleMode toggleShuffleMode ();
+
+//! \brief toggles the loop mode thru all possible values
+ LoopMode toggleLoopMode ();
+
+//! \brief returns the current shuffle mode
+ ShuffleMode getShuffleMode () const
+ {
+ return m_shuffle_mode;
+ }
+
+//! \brief sets the current shuffle mode
+ void setShuffleMode (const ShuffleMode shuffle_mode);
+
+//! \brief returns the current loop mode
+ LoopMode getLoopMode () const
+ {
+ return m_loop_mode;
+ }
+
+//! \brief sets the current loop mode
+ void setLoopMode (const LoopMode loop_mode)
+ {
+ m_loop_mode = loop_mode;
+ }
+
+/*! \brief adds the whole current track list to a collection
+ * \param Name the name of the collection. If it does not yet exist,
+ * it will be created.
+ */
+ unsigned int AddToCollection (const string Name);
+
+/*! \brief removes the whole current track from a the collection
+ * Remember - this selection can be configured to hold exactly
+ * one list, so this command can be used to clear a selected list.
+ * \param Name the name of the collection
+ */
+ unsigned int RemoveFromCollection (const string Name);
+//! \brief delete a collection
+ bool DeleteCollection (const string Name);
+/*! \brief create a collection only if it does not yet exist.
+ * \return true only if it has been created. false if it already existed.
+ */
+ bool CreateCollection(const string Name);
+
+//! \brief remove all items from the collection
+ void ClearCollection (const string Name);
+
+/*! generates an m3u file containing all tracks. The directory
+ * can be indicated by SetDirectory().
+ * The file name will be built from the list name, slashes
+ * and spaces converted
+ */
+ string exportM3U ();
+
+
+/*! \brief go to a position in the current level. If we are at the
+ * most detailled level this also sets the track position since
+ * they are identical.
+ * \param position the wanted position. If it is too big, go to the
+ * last existing position
+ * \return only if no position exists, false will be returned
+ */
+ void setPosition (unsigned int position);
+
+/*! \brief go to the position with value in the current level
+ * \param value the value of the wanted position
+ */
+ void setPosition (const string value)
+ {
+ setPosition (items.valindex (value));
+ }
+
+/*! \brief go to a position in the track list
+ * \param position the wanted position. If it is too big, go to the
+ * last existing position
+ * \return only if no position exists, false will be returned
+ */
+ void setTrackPosition (unsigned int position) const;
+
+/*! \brief skip some tracks in the track list
+ * \return false if new position does not exist
+ */
+ bool skipTracks (int step=1);
+
+/*! \brief skip forward by 1 in the track list
+ * \return false if new position does not exist
+ */
+ bool skipFwd ()
+ {
+ return skipTracks (+1);
+ }
+
+/*! \brief skip back by 1 in the track list
+ * \return false if new position does not exist
+ */
+ bool skipBack ()
+ {
+ return skipTracks (-1);
+ }
+
+//! \brief returns the sum of the durations of all tracks
+ unsigned long getLength ();
+
+/*! \brief returns the sum of the durations of completed tracks
+ * those are tracks before the current track position
+ */
+ unsigned long getCompletedLength () const;
+
+/*! returns the number of tracks in the track list
+ * \todo should not call tracks () which loads all track info.
+ * instead, only count the tracks. If the size differs from
+ * m_tracks.size(), invalidate m_tracks
+ */
+ unsigned int getNumTracks () const
+ {
+ return tracks ().size ();
+ }
+
+//! sets the directory for the storage of m3u file
+ void SetDirectory (const string directory)
+ {
+ m_Directory = directory;
+ }
+
+/*! returns the name of the current play list. If no play list is active,
+ * the name is built from the name of the key fields.
+ */
+ string getListname () const;
+
+/*! \brief true if this selection currently selects a list of collections
+ */
+ bool isCollectionlist () const;
+
+/*! \brief true if this selection currently selects a list of languages
+ */
+ bool isLanguagelist () const;
+
+ //! \brief true if we have entered a collection
+ bool inCollection(const string Name="") const;
+
+ /*! \brief dumps the entire state of this selection into a map,
+ * \param nv the values will be entered into this map
+ */
+ void DumpState(mgValmap& nv) const;
+
+ /*! \brief creates a new selection using saved definitions
+ * \param nv this map contains the saved definitions
+ */
+ mgSelection(mgValmap& nv);
+
+ //! \brief clear the cache, next access will reload from data base
+ void clearCache() const;
+
+ void refreshValues() const;
+
+ //! \brief true if values and tracks need to be reloaded
+ bool cacheIsEmpty() const
+ {
+ return (m_current_values=="" && m_current_tracks=="");
+ }
+ string value(mgKeyTypes kt, string idstr) const;
+ string value(mgKey* k, string idstr) const;
+ string value(mgKey* k) const;
+ string id(mgKeyTypes kt, string val) const;
+ string id(mgKey* k, string val) const;
+ string id(mgKey* k) const;
+ unsigned int keycount(mgKeyTypes kt);
+ vector <const char *> choices(mgOrder *o,unsigned int level, unsigned int *current);
+ unsigned int valcount (string val);
+ bool Connected() { return m_db.Connected(); }
+
+ private:
+ mutable map <mgKeyTypes, map<string,string> > map_values;
+ mutable map <mgKeyTypes, map<string,string> > map_ids;
+ mutable string m_current_values;
+ mutable string m_current_tracks;
+//! \brief be careful when accessing this, see mgSelection::tracks()
+ mutable vector < mgContentItem > m_tracks;
+ //! \brief initializes maps for id/value mapping in both direction
+ bool loadvalues (mgKeyTypes kt) const;
+ bool m_fall_through;
+ unsigned int m_position;
+ mutable unsigned int m_tracks_position;
+ ShuffleMode m_shuffle_mode;
+ void Shuffle() const;
+ LoopMode m_loop_mode;
+ mutable mgmySql m_db;
+ unsigned int m_level;
+ long m_trackid;
+
+ mgOrder order;
+ bool UsedBefore (mgOrder *o,const mgKeyTypes kt, unsigned int level) const;
+ void InitSelection ();
+ /*! \brief returns the SQL command for getting all values.
+ * For the leaf level, all values are returned. For upper
+ * levels, every distinct value is returned only once.
+ * This must be so for the leaf level because otherwise
+ * the value() entries do not correspond to the track()
+ * entries and the wrong tracks might be played.
+ */
+ string sql_values ();
+ string ListFilename ();
+ string m_Directory;
+ void loadgenres ();
+
+ void InitFrom(const mgSelection* s);
+
+};
+
+
+unsigned int randrange (const unsigned int high);
+
+
+#endif // _DB_H
diff --git a/muggle-plugin/mg_setup.c b/muggle-plugin/mg_setup.c
new file mode 100644
index 0000000..c649536
--- /dev/null
+++ b/muggle-plugin/mg_setup.c
@@ -0,0 +1,37 @@
+/*!
+ * \file vdr_setup.c
+ * \brief A setup class for a VDR media plugin (muggle)
+ *
+ * \version $Revision: 1.3 $
+ * \date $Date: 2005-01-24 15:45:30 +0100 (Mon, 24 Jan 2005) $
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author: wr61 $
+ *
+ * $Id: vdr_setup.c 399 2005-01-24 14:45:30Z wr61 $
+ *
+ * Partially adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+
+#include "mg_setup.h"
+
+mgSetup the_setup;
+
+
+mgSetup::mgSetup ()
+{
+ InitLoopMode = 0;
+ InitShuffleMode = 0;
+ AudioMode = 1;
+ DisplayMode = 3;
+ BackgrMode = 1;
+ TargetLevel = DEFAULT_TARGET_LEVEL;
+ LimiterLevel = DEFAULT_LIMITER_LEVEL;
+ Only48kHz = 0;
+ ToplevelDir = "/mnt/music/";
+ DbHost = "localhost";
+ DbName = "GiantDisc";
+ DeleteStaleReferences = false;
+}
diff --git a/muggle-plugin/mg_setup.h b/muggle-plugin/mg_setup.h
new file mode 100644
index 0000000..7423d4d
--- /dev/null
+++ b/muggle-plugin/mg_setup.h
@@ -0,0 +1,63 @@
+/*!
+ * \file vdr_setup.h
+ * \brief A setup class for a VDR media plugin (muggle)
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date: 2005-01-24 15:45:30 +0100 (Mon, 24 Jan 2005) $
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author: wr61 $
+ *
+ * $Id: vdr_setup.h 399 2005-01-24 14:45:30Z wr61 $
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___SETUP_MG_H
+#define ___SETUP_MG_H
+
+#define MAX_STRING_LEN 128
+
+#define DEFAULT_TARGET_LEVEL 25
+#define MAX_TARGET_LEVEL 50
+#define DEFAULT_LIMITER_LEVEL 70
+#define MIN_LIMITER_LEVEL 25
+
+/*!
+ * \brief storage for setup data
+ */
+class mgSetup
+{
+ public:
+ int InitLoopMode;
+ int InitShuffleMode;
+ int AudioMode;
+ int DisplayMode;
+ int BackgrMode;
+ int MenuMode;
+ int TargetLevel;
+ int LimiterLevel;
+ int Only48kHz;
+
+ char *DbHost;
+ char *DbSocket;
+ char *DbName;
+ char *DbUser;
+ char *DbPass;
+ int DbPort;
+ bool GdCompatibility;
+ char *ToplevelDir;
+
+ char PathPrefix[MAX_STRING_LEN];
+
+ int DeleteStaleReferences;
+
+ public:
+ mgSetup (void);
+
+};
+
+extern mgSetup the_setup;
+
+#endif
diff --git a/muggle-plugin/mg_sync.c b/muggle-plugin/mg_sync.c
new file mode 100644
index 0000000..677c3f8
--- /dev/null
+++ b/muggle-plugin/mg_sync.c
@@ -0,0 +1,305 @@
+/*!
+ * \file mg_sync.c
+ * \brief synchronization between SQL and filesystem
+ *
+ * \version $Revision: 1.0 $
+ * \date $Date: 2004-12-07 10:10:35 +0200 (Tue, 07 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr $
+ *
+ */
+
+#include "mg_sync.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <fts.h>
+#include <sys/time.h>
+#include <time.h>
+
+#include <mpegfile.h>
+#include <flacfile.h>
+#include <id3v2tag.h>
+#include <fileref.h>
+
+#include "mg_tools.h"
+#include "mg_mysql.h"
+#include "mg_setup.h"
+
+char *
+mgSync::sql_Cstring(TagLib::String s,char *buf)
+{
+ return m_db.sql_Cstring(s.toCString(),buf);
+}
+
+char *
+mgSync::lower(char *s)
+{
+ char *p=s;
+ while (*p)
+ {
+ int i=(int)(*p);
+ (*p)=(char)tolower(i);
+ p++;
+ }
+ return s;
+}
+
+TagLib::String
+mgSync::getlanguage(const char *filename)
+{
+ TagLib::String result = "";
+ TagLib::ID3v2::Tag * id3v2tag=0;
+ if (!strcmp(c_extension,"flac"))
+ {
+ TagLib::FLAC::File f(filename);
+ id3v2tag = f.ID3v2Tag();
+ if (id3v2tag)
+ {
+ TagLib::ID3v2::FrameList l = id3v2tag->frameListMap()["TLAN"];
+ if (!l.isEmpty())
+ result = l.front()->toString();
+ }
+ }
+ else if (!strcmp(c_extension,"mp3"))
+ {
+ TagLib::MPEG::File f(filename);
+ id3v2tag = f.ID3v2Tag();
+ if (id3v2tag)
+ {
+ TagLib::ID3v2::FrameList l = id3v2tag->frameListMap()["TLAN"];
+ if (!l.isEmpty())
+ result = l.front()->toString();
+ }
+ }
+ return result;
+}
+
+char *
+mgSync::getAlbum(const char *c_album,const char *c_artist,const char *c_directory)
+{
+ char * result;
+ char *b;
+ asprintf(&b,"SELECT cddbid FROM album"
+ " WHERE title=%s AND artist=%s",c_album,c_artist);
+ result=m_db.sql_Cstring(m_db.get_col0(b));
+ free(b);
+ if (!strcmp(result,"'NULL'"))
+ {
+ const char *directory="substring(tracks.mp3file,1,length(tracks.mp3file)"
+ "-instr(reverse(tracks.mp3file),'/'))";
+ char *where;
+ asprintf(&where,"WHERE tracks.sourceid=album.cddbid "
+ "AND %s=%s "
+ "AND album.title=%s",
+ directory,c_directory,
+ c_album);
+
+ // how many artists will the album have after adding this one?
+ asprintf(&b,"SELECT distinct album.artist FROM tracks, album %s "
+ " union select %s",where,c_artist);
+ MYSQL_RES *rows = m_db.exec_sql (b);
+ free(b);
+ long new_album_artists = m_db.affected_rows();
+ mysql_free_result(rows);
+ if (new_album_artists>1) // is the album multi artist?
+ {
+ asprintf(&b,"SELECT album.cddbid FROM tracks, album %s",where);
+ free(result);
+ result=m_db.sql_Cstring(m_db.get_col0(b));
+ free(b);
+ asprintf(&b,"UPDATE album SET artist='Various Artists' WHERE cddbid=%s",result);
+ m_db.exec_sql(b);
+ // here we could change all tracks.sourceid to result and delete
+ // the other album entries for this album, but that should only
+ // be needed if a pre 0.1.4 import has been done incorrectly, so we
+ // don't bother
+ }
+ else
+ { // no usable album found
+ free(result);
+ asprintf(&result,"'%ld-%9s",random(),c_artist+1);
+ char *p=strchr(result,0)-1;
+ if (*p!='\'')
+ *p='\'';
+ asprintf(&b,"INSERT INTO album SET title=%s,artist=%s,cddbid=%s",
+ c_album,c_artist,result);
+ m_db.exec_sql(b);
+ free(b);
+ }
+ free(where);
+ }
+ return result;
+}
+
+mgSync::mgSync()
+{
+ m_genre_rows=0;
+ if (!m_db.Connected())
+ return;
+ m_genre_rows = m_db.exec_sql ("SELECT id,genre from genre");
+ MYSQL_ROW rx;
+ while ((rx = mysql_fetch_row (m_genre_rows)) != 0)
+ m_Genres[rx[1]]=rx[0];
+ // init random number generator
+ struct timeval tv;
+ struct timezone tz;
+ gettimeofday( &tv, &tz );
+ srandom( tv.tv_usec );
+}
+
+mgSync::~mgSync()
+{
+ if (m_genre_rows) mysql_free_result(m_genre_rows);
+}
+
+void
+mgSync::UpdateTrack(long trackid)
+{
+ char sql[7000];
+ char *c_cddbid=getAlbum(c_album,c_artist,c_directory);
+ sprintf(sql,"UPDATE tracks SET artist=%s, title=%s,year=%d,sourceid=%s,"
+ "tracknb=%d,length=%d,bitrate=%d,samplerate=%d,"
+ "channels=%d,genre1=%s,lang=%s WHERE id=%ld",
+ c_artist,c_title,year,c_cddbid,
+ trackno,len,bitrate,sample,
+ channels,c_genre1,c_lang,trackid);
+ free(c_cddbid);
+ m_db.exec_sql(sql);
+}
+
+void
+mgSync::AddTrack()
+{
+ char sql[7000];
+ char *c_cddbid=getAlbum(c_album,c_artist,c_directory);
+ sprintf(sql,"INSERT INTO tracks SET artist=%s,title=%s,year=%u,sourceid=%s,"
+ "tracknb=%u,mp3file=%s,length=%d,bitrate=%d,samplerate=%d,"
+ "channels=%d,genre1=%s,genre2='',lang=%s,"
+ "folder1=%s,folder2=%s,folder3=%s,folder4=%s",
+ c_artist,c_title,year,c_cddbid,
+ trackno,c_mp3file,len,bitrate,sample,
+ channels,c_genre1,c_lang,
+ c_folder1,c_folder2,c_folder3,c_folder4);
+ free(c_cddbid);
+ m_db.exec_sql(sql);
+}
+
+bool
+mgSync::GetFileInfo(const char *filename)
+{
+ TagLib::FileRef f( filename) ;
+ if (f.isNull())
+ return false;
+ TagLib::Tag *tag = f.tag();
+ if (!f.tag())
+ return false;
+ if (tag->album()=="")
+ strcpy(c_album,"'Unassigned'");
+ else
+ sql_Cstring(tag->album(),c_album);
+ sql_Cstring(tag->artist(),c_artist);
+ sql_Cstring(tag->title(),c_title);
+ sql_Cstring(filename,c_directory);
+ char *slash=strrchr(c_directory,'/');
+ if (slash)
+ {
+ *slash='\'';
+ *(slash+1)=0;
+ }
+ TagLib::String sgenre1=tag->genre();
+ const char *genrename=sgenre1.toCString();
+ const char *genreid=m_Genres[genrename].c_str();
+ sql_Cstring(genreid,c_genre1);
+ sql_Cstring(getlanguage(filename),c_lang);
+ trackno=tag->track();
+ year=tag->year();
+ TagLib::AudioProperties *ap = f.audioProperties();
+ len = ap->length(); // tracks.length
+ bitrate = ap->bitrate(); // tracks.bitrate
+ sample = ap->sampleRate(); //tracks.samplerate
+ channels = ap->channels(); //tracks.channels
+ if (m_db.HasFolderFields())
+ {
+ char *folders[4];
+ char *fbuf=SeparateFolders(filename,folders,4);
+ sql_Cstring(folders[0],c_folder1);
+ sql_Cstring(folders[1],c_folder2);
+ sql_Cstring(folders[2],c_folder3);
+ sql_Cstring(folders[3],c_folder4);
+ free(fbuf);
+ }
+ return true;
+}
+
+void
+mgSync::SyncFile(const char *filename)
+{
+ if (!strncmp(filename,"./",2)) // strip leading ./
+ filename += 2;
+ if (strlen(filename)>255)
+ {
+ mgWarning("Length of file exceeds database field capacity: %s", filename);
+ return;
+ }
+ mgDebug(3,"Importing %s",filename);
+ sql_Cstring(filename,c_mp3file);
+ char sql[600];
+ sprintf(sql,"SELECT id from tracks WHERE mp3file=%s",c_mp3file);
+ string s = m_db.get_col0(sql);
+ if (s!="NULL")
+ {
+ if (GetFileInfo(filename))
+ UpdateTrack(atol(s.c_str()));
+ }
+ else
+ {
+ if (GetFileInfo(filename))
+ AddTrack();
+ }
+}
+
+void
+mgSync::Sync(char * const * path_argv, bool delete_missing)
+{
+ extern void showimportcount(unsigned int,bool final=false);
+ if (!m_db.Connected())
+ return;
+
+ unsigned int count=0;
+ m_db.CreateFolderFields();
+ chdir(the_setup.ToplevelDir);
+ FTS *fts;
+ FTSENT *ftsent;
+ fts = fts_open( path_argv, FTS_LOGICAL, 0);
+ if (fts)
+ {
+ while ( (ftsent = fts_read(fts)) != NULL)
+ {
+ if (!((ftsent->fts_statp->st_mode)||S_IFREG))
+ continue;
+ char *extension = strrchr(ftsent->fts_path,'.');
+ if (!extension)
+ continue;
+ strcpy(c_extension,extension+1);
+ lower(c_extension);
+ if (!strcmp(c_extension,"flac") || !strcmp(c_extension,"ogg") || !strcmp(c_extension,"mp3"))
+ {
+ SyncFile(ftsent->fts_path);
+ count++;
+ if (count%1000==0)
+ showimportcount(count);
+ }
+ }
+ fts_close(fts);
+ }
+ showimportcount(count,true);
+}
+
+void
+mgSync::Create()
+{
+ m_db.Create();
+}
diff --git a/muggle-plugin/mg_sync.h b/muggle-plugin/mg_sync.h
new file mode 100644
index 0000000..6374d59
--- /dev/null
+++ b/muggle-plugin/mg_sync.h
@@ -0,0 +1,73 @@
+/*!
+ * \file mg_sync.h
+ * \brief synchronization between SQL and filesystem
+ *
+ * \version $Revision: 1.0 $
+ * \date $Date: 2004-12-07 10:10:35 +0200 (Tue, 07 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr $
+ *
+ */
+
+#ifndef _MG_SYNC_H
+#define _MG_SYNC_H
+
+#include <map>
+#include <string.h>
+#include <tag.h>
+
+#include "mg_mysql.h"
+
+class mgSync
+{
+ public:
+ mgSync();
+ ~mgSync();
+
+ //! \brief drop and create the data base GiantDisc
+ void Create();
+
+ /*! \brief import/export tags like
+ * \par path can be a file or a directory. If directory,
+ * sync all files within
+ * \par assorted see mugglei -h
+ * \par delete_missing if the file does not exist, delete the
+ * data base entry. If the file is unreadable, do not delete.
+ */
+ void Sync(char * const * path_argv, bool delete_missing = false);
+
+ private:
+ mgmySql m_db;
+ char *sql_Cstring(TagLib::String s,char *buf=0);
+ char *lower(char *s);
+ TagLib::String getlanguage(const char *filename);
+ char * getAlbum(const char *c_album,const char *c_artist,const char *c_directory);
+ bool GetFileInfo(const char *filename);
+ void AddTrack();
+ void UpdateTrack(long trackid);
+ void SyncFile(const char *filename);
+ map<string,string> m_Genres;
+ MYSQL_RES* m_genre_rows;
+
+
+ char c_album[520]; // at least 256 * 2 + 2 for VARCHAR(255), see sql_string()
+ char c_artist[520];
+ char c_title[520];
+ char c_directory[520];
+ char c_mp3file[520];
+ char c_genre1[520];
+ char c_lang[520];
+ char c_folder1[520];
+ char c_folder2[520];
+ char c_folder3[520];
+ char c_folder4[520];
+ char c_extension[300];
+ unsigned int trackno;
+ unsigned int year;
+ int len;
+ int bitrate;
+ int sample;
+ int channels;
+};
+
+#endif
diff --git a/muggle-plugin/mg_thread_sync.c b/muggle-plugin/mg_thread_sync.c
new file mode 100644
index 0000000..85b55a6
--- /dev/null
+++ b/muggle-plugin/mg_thread_sync.c
@@ -0,0 +1,63 @@
+
+#include <mysql/mysql.h>
+
+#include "mg_thread_sync.h"
+#include "mg_sync.h"
+
+static mgThreadSync* the_instance = NULL;
+
+mgThreadSync* mgThreadSync::get_instance()
+{
+ if( !the_instance )
+ {
+ the_instance = new mgThreadSync();
+ }
+
+
+ if( the_instance->Active() )
+ {
+ return NULL;
+ }
+ else
+ {
+ return the_instance;
+ }
+}
+
+void mgThreadSync::SetArguments( char * const * path_argv, bool delete_missing )
+{
+ m_path = path_argv;
+ m_delete = delete_missing;
+}
+
+bool mgThreadSync::Sync(char * const * path_argv, bool delete_missing )
+{
+ mgThreadSync *s = mgThreadSync::get_instance();
+ if( s )
+ {
+ s->SetArguments( path_argv, delete_missing );
+ s->Start();
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+void
+mgThreadSync::Action()
+{
+ mysql_thread_init();
+
+ if( m_path )
+ {
+ mgSync s;
+ s.Sync( m_path, m_delete );
+ }
+
+ mysql_thread_end();
+}
+
+
diff --git a/muggle-plugin/mg_thread_sync.h b/muggle-plugin/mg_thread_sync.h
new file mode 100644
index 0000000..d9a2e3f
--- /dev/null
+++ b/muggle-plugin/mg_thread_sync.h
@@ -0,0 +1,39 @@
+/*!
+ * \file mg_sync.h
+ * \brief synchronization between SQL and filesystem
+ *
+ * \version $Revision: 1.0 $
+ * \date $Date: 2004-12-07 10:10:35 +0200 (Tue, 07 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr $
+ *
+ */
+
+#ifndef _MG_THREADSYNC_H
+#define _MG_THREADSYNC_H
+
+#include <thread.h>
+
+class mgThreadSync : public cThread
+{
+ public:
+
+ static mgThreadSync* get_instance();
+
+ bool Sync(char * const * path_argv, bool delete_missing );
+
+ protected:
+ /*! \brief Runs the import routine as a separate thread
+ */
+ virtual void Action();
+
+ private:
+
+ void SetArguments( char * const * path_argv, bool delete_missing );
+
+ char * const *m_path;
+ bool m_delete;
+
+};
+
+#endif
diff --git a/muggle-plugin/mg_tools.c b/muggle-plugin/mg_tools.c
new file mode 100644
index 0000000..ce09ab6
--- /dev/null
+++ b/muggle-plugin/mg_tools.c
@@ -0,0 +1,149 @@
+/*!
+ * \file mg_tools.c
+ * \brief A few util functions for standalone and plugin messaging for the vdr muggle plugindatabase
+ *
+ * \version $Revision: 1.4 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author file owner: $Author$
+ */
+
+#include "mg_tools.h"
+
+/*extern "C"
+{*/
+#include <stdarg.h>
+#include <stdio.h>
+/*}
+ */
+#include <stdlib.h>
+
+//! \brief buffer for messages
+#define MAX_BUFLEN 2048
+
+static char buffer[MAX_BUFLEN];
+
+static int DEBUG_LEVEL = 3;
+
+void
+mgSetDebugLevel (int new_level)
+{
+ DEBUG_LEVEL = new_level;
+}
+
+
+void
+mgDebug (int level, const char *fmt, ...)
+{
+
+ va_list ap;
+ if (level <= DEBUG_LEVEL)
+ {
+ va_start (ap, fmt);
+
+ vsnprintf (buffer, MAX_BUFLEN - 1, fmt, ap);
+ if (STANDALONE)
+ {
+ fprintf (stderr, "dbg %d: %s\n", level, buffer);
+ }
+ else
+ {
+#if !STANDALONE
+ isyslog ("%s\n", buffer);
+#endif
+ }
+ }
+ va_end (ap);
+}
+
+
+void
+mgDebug (const char *fmt, ...)
+{
+ va_list ap;
+ va_start (ap, fmt);
+ mgDebug (1, fmt, ap);
+}
+
+
+void
+mgWarning (const char *fmt, ...)
+{
+
+ va_list ap;
+ va_start (ap, fmt);
+ vsnprintf (buffer, MAX_BUFLEN - 1, fmt, ap);
+
+ if (STANDALONE)
+ {
+ fprintf (stderr, "warning: %s\n", buffer);
+ }
+ else
+ {
+#if !STANDALONE
+ isyslog ("Warning: %s\n", buffer);
+#endif
+ }
+ extern void showmessage(const char*,int duration=0);
+ showmessage(buffer);
+ va_end (ap);
+}
+
+
+void
+mgError (const char *fmt, ...)
+{
+
+ va_list ap;
+ va_start (ap, fmt);
+ vsnprintf (buffer, MAX_BUFLEN - 1, fmt, ap);
+
+ if (STANDALONE)
+ {
+ fprintf (stderr, "Error: %s\n", buffer);
+ exit (1);
+ }
+ else
+ {
+#if !STANDALONE
+ isyslog ("Error in Muggle: %s\n", buffer);
+#endif
+ }
+
+ va_end (ap);
+}
+
+
+std::string trim(std::string const& source, char const* delims ) {
+ std::string result(source);
+ std::string::size_type index = result.find_last_not_of(delims);
+ if(index != std::string::npos)
+ result.erase(++index);
+ index = result.find_first_not_of(delims);
+ if(index != std::string::npos)
+ result.erase(0, index);
+ else
+ result.erase();
+ return result;
+}
+
+
+char *
+SeparateFolders(const char *filename, char * folders[],unsigned int fcount)
+{
+ for (unsigned int i=0;i<fcount;i++)
+ folders[i]="";
+ char *fbuf=strdup(filename);
+ char *slash=fbuf-1;
+ for (unsigned int i=0;i<fcount;i++)
+ {
+ char *p=slash+1;
+ slash=strchr(p,'/');
+ if (!slash)
+ break;
+ folders[i]=p;
+ *slash=0;
+ }
+ return fbuf;
+}
+
diff --git a/muggle-plugin/mg_tools.h b/muggle-plugin/mg_tools.h
new file mode 100644
index 0000000..65ba262
--- /dev/null
+++ b/muggle-plugin/mg_tools.h
@@ -0,0 +1,89 @@
+/*! \file mg_tools.h
+ * \ingroup muggle
+ * \brief A few utility functions for standalone and plugin messaging for the vdr muggle plugindatabase
+ *
+ * \version $Revision: 1.4 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author file owner: $Author$
+ *
+ */
+
+/* makes sure we don't use the same declarations twice */
+#ifndef _MUGGLE_TOOLS_H
+#define _MUGGLE_TOOLS_H
+
+#include <iostream>
+#include <string>
+#include <mysql.h>
+
+#define STANDALONE 1 // what's this?
+
+/*!
+ * \brief Logging utilities
+ *
+ * \todo these could be static members in the mgLog class
+ * \todo code of these functions should be compiled conditionally
+ */
+//@{
+void mgSetDebugLevel (int new_level);
+void mgDebug (int level, const char *fmt, ...);
+void mgDebug (const char *fmt, ...);
+void mgWarning (const char *fmt, ...);
+//! \todo mgError should display the message on the OSD. How?
+void mgError (const char *fmt, ...);
+//@}
+
+#ifdef DEBUG
+#define MGLOG(x) mgLog __thelog(x)
+#else
+#define MGLOG(x) {}
+#endif
+
+#ifdef DEBUG
+#define MGLOGSTREAM __thelog.getStream()
+#else
+#define MGLOGSTREAM __thelog.getStream()
+#endif
+
+/*! \brief simplified logging class
+ * \ingroup muggle
+ *
+ * Create a local instance at the beginning of the method
+ * and entering/leaving the function will be logged
+ * as constructors/destructors are called.
+ */
+class mgLog
+{
+ public:
+ enum
+ {
+ LOG, WARNING, ERROR, FATAL
+ } mgLogLevel;
+
+ std::ostream & getStream ()
+ {
+ return std::cout;
+ }
+
+ mgLog (std::string methodname):m_methodname (methodname)
+ {
+ getStream () << m_methodname << " entered" << std::endl;
+ };
+
+ ~mgLog ()
+ {
+ getStream () << m_methodname << " terminated" << std::endl;
+ }
+
+ private:
+
+ std::string m_methodname;
+
+};
+
+std::string trim(std::string const& source, char const* delims = " \t\r\n");
+
+char *SeparateFolders(const char *filename, char * folders[],unsigned int fcount);
+
+#endif /* _MUGGLE_TOOLS_H */
diff --git a/muggle-plugin/mg_valmap.c b/muggle-plugin/mg_valmap.c
new file mode 100644
index 0000000..2361b00
--- /dev/null
+++ b/muggle-plugin/mg_valmap.c
@@ -0,0 +1,70 @@
+#include "mg_valmap.h"
+#include "mg_order.h"
+
+mgValmap::mgValmap(const char *key) {
+ m_key = key;
+}
+
+void mgValmap::Read(FILE *f) {
+ char *line=(char*)malloc(1000);
+ char *prefix=(char*)malloc(strlen(m_key)+2);
+ strcpy(prefix,m_key);
+ strcat(prefix,".");
+ rewind(f);
+ while (fgets(line,1000,f)) {
+ if (strncmp(line,prefix,strlen(prefix))) continue;
+ if (line[strlen(line)-1]=='\n')
+ line[strlen(line)-1]=0;
+ char *name = line + strlen(prefix);
+ char *eq = strchr(name,'=');
+ if (!eq) continue;
+ *(eq-1)=0;
+ char *value = eq + 2;
+ (*this)[string(name)]=string(value);
+ }
+ free(prefix);
+ free(line);
+}
+
+void mgValmap::Write(FILE *f) {
+ for (mgValmap::const_iterator it=begin();it!=end();++it) {
+ char b[1000];
+ sprintf(b,"%s.%s = %s\n",
+ m_key,it->first.c_str(),
+ it->second.c_str());
+ fputs(b,f);
+ }
+}
+
+void mgValmap::put(const char* name, const string value) {
+ if (value.empty()) return;
+ (*this)[string(name)] = value;
+}
+
+void mgValmap::put(const char* name, const char* value) {
+ if (!value || *value==0) return;
+ (*this)[string(name)] = value;
+}
+
+void mgValmap::put(const char* name, const int value) {
+ put(name,ltos(value));
+}
+
+void mgValmap::put(const char* name, const unsigned int value) {
+ put(name,ltos(value));
+}
+
+void mgValmap::put(const char* name, const long value) {
+ put(name,ltos(value));
+}
+
+void mgValmap::put(const char* name, const bool value) {
+ string s;
+ if (value)
+ s = "true";
+ else
+ s = "false";
+ put(name,s);
+}
+
+
diff --git a/muggle-plugin/mg_valmap.h b/muggle-plugin/mg_valmap.h
new file mode 100644
index 0000000..ca39139
--- /dev/null
+++ b/muggle-plugin/mg_valmap.h
@@ -0,0 +1,52 @@
+#ifndef _MG_VALMAP_H
+#define _MG_VALMAP_H
+
+#include <stdio.h>
+#include <string>
+#include <map>
+
+using namespace std;
+
+//! \brief a map for reading / writing configuration data.
+class mgValmap : public map<string,string> {
+ private:
+ const char *m_key;
+ public:
+ /*! \brief constructor
+ * \param key all names will be prefixed with key.
+ */
+ mgValmap(const char *key);
+ //! \brief read from file
+ void Read(FILE *f);
+ //! \brief write to file
+ void Write(FILE *f);
+ //! \brief enter a string value
+ void put(const char*name, string value);
+ //! \brief enter a C string value
+ void put(const char*name, const char* value);
+ //! \brief enter a long value
+ void put(const char*name, long value);
+ //! \brief enter a int value
+ void put(const char*name, int value);
+ //! \brief enter a unsigned int value
+ void put(const char*name, unsigned int value);
+ //! \brief enter a bool value
+ void put(const char*name, bool value);
+ //! \brief return a string
+ string getstr(const char* name) {
+ return (*this)[name];
+ }
+ //! \brief return a C string
+ bool getbool(const char* name) {
+ return (getstr(name)=="true");
+ }
+ //! \brief return a long
+ long getlong(const char* name) {
+ return atol(getstr(name).c_str());
+ }
+ //! \brief return an unsigned int
+ unsigned int getuint(const char* name) {
+ return (unsigned long)getlong(name);
+ }
+};
+#endif
diff --git a/muggle-plugin/muggle.c b/muggle-plugin/muggle.c
new file mode 100644
index 0000000..a249c27
--- /dev/null
+++ b/muggle-plugin/muggle.c
@@ -0,0 +1,283 @@
+/*!
+ * \file muggle.c
+ * \brief Implements a plugin for browsing media libraries within VDR
+ *
+ * \version $Revision: 1.10 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner, Wolfgang Rohdewald
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ */
+
+#include "muggle.h"
+
+#include "vdr_menu.h"
+#include "vdr_setup.h"
+#include "mg_tools.h"
+
+#include "i18n.h"
+#include <getopt.h>
+#include <config.h>
+
+static const char *VERSION = "0.1.6";
+static const char *DESCRIPTION = "Media juggle plugin for VDR";
+static const char *MAINMENUENTRY = "Muggle";
+
+const char *
+mgMuggle::Version (void)
+{
+ return VERSION;
+}
+
+
+const char *
+mgMuggle::Description (void)
+{
+ return DESCRIPTION;
+}
+
+
+const char *
+mgMuggle::MainMenuEntry (void)
+{
+ return MAINMENUENTRY;
+}
+
+
+mgMuggle::mgMuggle (void)
+{
+// defaults for database arguments
+ the_setup.DbHost = 0;
+ the_setup.DbSocket = 0;
+ the_setup.DbPort = 0;
+ the_setup.DbName = strdup ("GiantDisc");
+ the_setup.DbUser = 0;
+ the_setup.DbPass = 0;
+ the_setup.GdCompatibility = false;
+ the_setup.ToplevelDir = strdup ("/mnt/music/");
+#ifndef HAVE_ONLY_SERVER
+ char *buf;
+ asprintf(&buf,"%s/.muggle",getenv("HOME"));
+ set_datadir(buf);
+ free(buf);
+#endif
+}
+
+
+void
+mgMuggle::Stop (void)
+{
+ free(the_setup.DbHost);
+ free(the_setup.DbName);
+ free(the_setup.DbUser);
+ free(the_setup.DbPass);
+ free(the_setup.ToplevelDir);
+}
+
+
+const char *
+mgMuggle::CommandLineHelp (void)
+{
+// Return a string that describes all known command line options.
+ return
+#ifdef HAVE_ONLY_SERVER
+ " -h HHHH, --host=HHHH specify database host (default is localhost)\n"
+#else
+ " -h HHHH, --host=HHHH specify database host (default is mysql embedded)\n"
+#endif
+ " -s SSSS --socket=PATH specify database socket\n"
+ " -n NNNN, --name=NNNN specify database name (overridden by -g)\n"
+ " -p PPPP, --port=PPPP specify port of database server (default is )\n"
+ " -u UUUU, --user=UUUU specify database user (default is )\n"
+ " -w WWWW, --password=WWWW specify database password (default is empty)\n"
+ " -t TTTT, --toplevel=TTTT specify toplevel directory for music (default is /mnt/music)\n"
+#ifndef HAVE_ONLY_SERVER
+ " -d DIRN, --datadir=DIRN specify directory for embedded sql data (default is $HOME/.muggle)\n"
+#endif
+ " -g, --giantdisc enable full Giantdisc compatibility mode\n"
+ " -v, --verbose specify debug level. The higher the more. Default is 1\n"
+ "\n"
+ "if the specified host is localhost, sockets will be used if possible.\n"
+ "Otherwise the -s parameter will be ignored";
+}
+
+
+bool mgMuggle::ProcessArgs (int argc, char *argv[])
+{
+ mgSetDebugLevel (1);
+ char b[1000];
+ sprintf(b,"mgMuggle::ProcessArgs ");
+ for (int i=1;i<argc;i++)
+ {
+ if (strlen(b)+strlen(argv[i]+2)>1000) break;;
+ strcat(b," ");
+ strcat(b,argv[i]);
+ }
+ mgDebug(1,b);
+
+// Implement command line argument processing here if applicable.
+ static struct option
+ long_options[] =
+ {
+ {"host", required_argument, NULL, 'h'},
+ {"socket", required_argument, NULL, 's'},
+ {"name", required_argument, NULL, 'n'},
+ {"port", required_argument, NULL, 'p'},
+ {"user", required_argument, NULL, 'u'},
+ {"password", required_argument, NULL, 'w'},
+#ifndef HAVE_ONLY_SERVER
+ {"datadir", required_argument, NULL, 'd'},
+#endif
+ {"toplevel", required_argument, NULL, 't'},
+ {"giantdisc", no_argument, NULL, 'g'},
+ {NULL}
+ };
+
+ int
+ c,
+ option_index = 0;
+ while ((c =
+#ifndef HAVE_ONLY_SERVER
+ getopt_long (argc, argv, "gh:s:n:p:t:u:w:d:v:", long_options,
+#else
+ getopt_long (argc, argv, "gh:s:n:p:t:u:w:v:", long_options,
+#endif
+ &option_index)) != -1)
+ {
+ switch (c)
+ {
+ case 'h':
+ {
+ the_setup.DbHost = strcpyrealloc (the_setup.DbHost, optarg);
+ }
+ break;
+ case 's':
+ {
+ the_setup.DbSocket = strcpyrealloc (the_setup.DbSocket, optarg);
+ }
+ break;
+ case 'n':
+ {
+ the_setup.DbName = strcpyrealloc (the_setup.DbName, optarg);
+ }
+ break;
+ case 'p':
+ {
+ the_setup.DbPort = atoi (optarg);
+ }
+ break;
+ case 'u':
+ {
+ the_setup.DbUser = strcpyrealloc (the_setup.DbUser, optarg);
+ }
+ break;
+ case 'w':
+ {
+ the_setup.DbPass = strcpyrealloc (the_setup.DbPass, optarg);
+ }
+ break;
+#ifndef HAVE_ONLY_SERVER
+ case 'd':
+ {
+ set_datadir(optarg);
+ }
+ break;
+#endif
+ case 'v':
+ {
+ mgSetDebugLevel (atol(optarg));
+ }
+ break;
+ case 't':
+ {
+ if (optarg[strlen (optarg) - 1] != '/')
+ {
+ std::string res = std::string (optarg) + "/";
+ the_setup.ToplevelDir = strdup (res.c_str ());
+ }
+ else
+ {
+ the_setup.ToplevelDir =
+ strcpyrealloc (the_setup.ToplevelDir, optarg);
+ }
+ }
+ break;
+ case 'g':
+ {
+ the_setup.DbName = strcpyrealloc (the_setup.DbName, "GiantDisc");
+ the_setup.GdCompatibility = true;
+ }
+ break;
+ default:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+bool mgMuggle::Initialize (void)
+{
+// Initialize any background activities the plugin shall perform.
+ return true;
+}
+
+
+bool mgMuggle::Start (void)
+{
+// Start any background activities the plugin shall perform.
+ RegisterI18n (Phrases);
+ return true;
+}
+
+
+void
+mgMuggle::Housekeeping (void)
+{
+// Perform any cleanup or other regular tasks.
+}
+
+
+cOsdObject *
+mgMuggle::MainMenuAction (void)
+{
+// Perform the action when selected from the main VDR menu.
+ return new mgMainMenu ();
+}
+
+
+cMenuSetupPage *
+mgMuggle::SetupMenu (void)
+{
+ return new mgMenuSetup ();
+}
+
+
+bool mgMuggle::SetupParse (const char *Name, const char *Value)
+{
+ if (!strcasecmp (Name, "InitLoopMode"))
+ the_setup.InitLoopMode = atoi (Value);
+ else if (!strcasecmp (Name, "InitShuffleMode"))
+ the_setup.InitShuffleMode = atoi (Value);
+ else if (!strcasecmp (Name, "AudioMode"))
+ the_setup.AudioMode = atoi (Value);
+ else if (!strcasecmp (Name, "DisplayMode"))
+ the_setup.DisplayMode = atoi (Value);
+ else if (!strcasecmp (Name, "BackgrMode"))
+ the_setup.BackgrMode = atoi (Value);
+ else if (!strcasecmp (Name, "TargetLevel"))
+ the_setup.TargetLevel = atoi (Value);
+ else if (!strcasecmp (Name, "LimiterLevel"))
+ the_setup.LimiterLevel = atoi (Value);
+ else if (!strcasecmp (Name, "Only48kHz"))
+ the_setup.Only48kHz = atoi (Value);
+ else
+ return false;
+
+ return true;
+}
+
+
+VDRPLUGINCREATOR (mgMuggle); // Don't touch this!
diff --git a/muggle-plugin/muggle.doxygen b/muggle-plugin/muggle.doxygen
new file mode 100644
index 0000000..45daa09
--- /dev/null
+++ b/muggle-plugin/muggle.doxygen
@@ -0,0 +1,1078 @@
+# Doxyfile 1.3.4
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project
+#
+# All text after a hash (#) is considered a comment and will be ignored
+# The format is:
+# TAG = value [value, ...]
+# For lists items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ")
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded
+# by quotes) that should identify the project.
+
+PROJECT_NAME = Muggle media plugin
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number.
+# This could be handy for archiving the generated documentation or
+# if some version control system is used.
+
+PROJECT_NUMBER = 0.1.6
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
+# base path where the generated documentation will be put.
+# If a relative path is entered, it will be relative to the location
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY = doc
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# The default language is English, other supported languages are:
+# Brazilian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch,
+# Finnish, French, German, Greek, Hungarian, Italian, Japanese, Japanese-en
+# (Japanese with English messages), Korean, Norwegian, Polish, Portuguese,
+# Romanian, Russian, Serbian, Slovak, Slovene, Spanish, Swedish, and Ukrainian.
+
+OUTPUT_LANGUAGE = English
+
+# This tag can be used to specify the encoding used in the generated output.
+# The encoding is not always determined by the language that is chosen,
+# but also whether or not the output is meant for Windows or non-Windows users.
+# In case there is a difference, setting the USE_WINDOWS_ENCODING tag to YES
+# forces the Windows encoding (this is the default for the Windows binary),
+# whereas setting the tag to NO uses a Unix-style encoding (the default for
+# all platforms other than Windows).
+
+USE_WINDOWS_ENCODING = NO
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will
+# include brief member descriptions after the members that are listed in
+# the file and class documentation (similar to JavaDoc).
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend
+# the brief description of a member or function before the detailed description.
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF = YES
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# Doxygen will generate a detailed section even if there is only a brief
+# description.
+
+ALWAYS_DETAILED_SEC = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all inherited
+# members of a class in the documentation of that class as if those members were
+# ordinary class members. Constructors, destructors and assignment operators of
+# the base classes will not be shown.
+
+INLINE_INHERITED_MEMB = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full
+# path before files name in the file list and in the header files. If set
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag
+# can be used to strip a user-defined part of the path. Stripping is
+# only done if one of the specified strings matches the left-hand part of
+# the path. It is allowed to use relative paths in the argument list.
+
+STRIP_FROM_PATH = /home/lvw/muggle-plugin
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter
+# (but less readable) file names. This can be useful is your file systems
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen
+# will interpret the first line (until the first dot) of a JavaDoc-style
+# comment as the brief description. If set to NO, the JavaDoc
+# comments will behave just like the Qt-style comments (thus requiring an
+# explict @brief command for a brief description.
+
+JAVADOC_AUTOBRIEF = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen
+# treat a multi-line C++ special comment block (i.e. a block of //! or ///
+# comments) as a brief description. This used to be the default behaviour.
+# The new default is to treat a multi-line C++ comment block as a detailed
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the DETAILS_AT_TOP tag is set to YES then Doxygen
+# will output the detailed description near the top, like JavaDoc.
+# If set to NO, the detailed description appears after the member
+# documentation.
+
+DETAILS_AT_TOP = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented
+# member inherits the documentation from any documented member that it
+# reimplements.
+
+INHERIT_DOCS = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab.
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE = 8
+
+# This tag can be used to specify a number of aliases that acts
+# as commands in the documentation. An alias has the form "name=value".
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to
+# put the command \sideeffect (or @sideeffect) in the documentation, which
+# will result in a user-defined paragraph with heading "Side Effects:".
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C.
+# For instance, some of the names that are used will be different. The list
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java sources
+# only. Doxygen will then generate output that is more tailored for Java.
+# For instance, namespaces will be presented as packages, qualified scopes
+# will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of
+# the same type (for instance a group of public functions) to be put as a
+# subgroup of that type (e.g. under the Public Functions section). Set it to
+# NO to prevent subgrouping. Alternatively, this can be done per class using
+# the \nosubgrouping command.
+
+SUBGROUPING = YES
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available.
+# Private class members and static file members will be hidden unless
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
+# will be included in the documentation.
+
+EXTRACT_PRIVATE = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file
+# will be included in the documentation.
+
+EXTRACT_STATIC = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs)
+# defined locally in source files will be included in the documentation.
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES = YES
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all
+# undocumented members of documented classes, files or namespaces.
+# If set to NO (the default) these members will be included in the
+# various overviews, but no documentation section is generated.
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy.
+# If set to NO (the default) these classes will be included in the various
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all
+# friend (class|struct|union) declarations.
+# If set to NO (the default) these declarations will be included in the
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any
+# documentation blocks found inside the body of a function.
+# If set to NO (the default) these blocks will be appended to the
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS = NO
+
+# The INTERNAL_DOCS tag determines if documentation
+# that is typed after a \internal command is included. If the tag is set
+# to NO (the default) then the documentation will be excluded.
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate
+# file names in lower-case letters. If set to YES upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# users are advised to set this option to NO.
+
+CASE_SENSE_NAMES = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen
+# will show members with their full class and namespace scopes in the
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES = NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen
+# will put a list of the files that are included by a file in the documentation
+# of that file.
+
+SHOW_INCLUDE_FILES = YES
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline]
+# is inserted in the documentation for inline members.
+
+INLINE_INFO = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen
+# will sort the (detailed) documentation of file and class members
+# alphabetically by member name. If set to NO the members will appear in
+# declaration order.
+
+SORT_MEMBER_DOCS = YES
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or
+# disable (NO) the todo list. This list is created by putting \todo
+# commands in the documentation.
+
+GENERATE_TODOLIST = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or
+# disable (NO) the test list. This list is created by putting \test
+# commands in the documentation.
+
+GENERATE_TESTLIST = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or
+# disable (NO) the bug list. This list is created by putting \bug
+# commands in the documentation.
+
+GENERATE_BUGLIST = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or
+# disable (NO) the deprecated list. This list is created by putting
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines
+# the initial value of a variable or define consists of for it to appear in
+# the documentation. If the initializer consists of more lines than specified
+# here it will be hidden. Use a value of 0 to hide initializers completely.
+# The appearance of the initializer of individual variables and defines in the
+# documentation can be controlled using \showinitializer or \hideinitializer
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated
+# at the bottom of the documentation of classes and structs. If set to YES the
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated by doxygen. Possible values are YES and NO. If left blank
+# NO is used.
+
+WARNINGS = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED = YES
+
+# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some
+# parameters in a documented function, or documenting parameters that
+# don't exist or using markup commands wrongly.
+
+WARN_IF_DOC_ERROR = YES
+
+# The WARN_FORMAT tag determines the format of the warning messages that
+# doxygen can produce. The string should contain the $file, $line, and $text
+# tags, which will be replaced by the file and line number from which the
+# warning originated and the warning text.
+
+WARN_FORMAT = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning
+# and error messages should be written. If left blank the output is written
+# to stderr.
+
+WARN_LOGFILE =
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain
+# documented source files. You may enter file names like "myfile.cpp" or
+# directories like "/usr/src/myproject". Separate the files or directories
+# with spaces.
+
+INPUT = README TODO .
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank the following patterns are tested:
+# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx *.hpp
+# *.h++ *.idl *.odl *.cs *.php *.php3 *.inc
+
+FILE_PATTERNS = *.h *.c
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories
+# should be searched for input files as well. Possible values are YES and NO.
+# If left blank NO is used.
+
+RECURSIVE = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+
+EXCLUDE =
+
+# The EXCLUDE_SYMLINKS tag can be used select whether or not files or directories
+# that are symbolic links (a Unix filesystem feature) are excluded from the input.
+
+EXCLUDE_SYMLINKS = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+
+EXCLUDE_PATTERNS =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or
+# directories that contain example code fragments that are included (see
+# the \include command).
+
+EXAMPLE_PATH =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank all files are included.
+
+EXAMPLE_PATTERNS =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude
+# commands irrespective of the value of the RECURSIVE tag.
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or
+# directories that contain image that are included in the documentation (see
+# the \image command).
+
+IMAGE_PATH =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command <filter> <input-file>, where <filter>
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an
+# input file. Doxygen will then use the output that the filter program writes
+# to standard output.
+
+INPUT_FILTER =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will be used to filter the input files when producing source
+# files to browse (i.e. when SOURCE_BROWSER is set to YES).
+
+FILTER_SOURCE_FILES = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will
+# be generated. Documented entities will be cross-referenced with these sources.
+
+SOURCE_BROWSER = YES
+
+# Setting the INLINE_SOURCES tag to YES will include the body
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES = YES
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct
+# doxygen to hide any special comment blocks from generated source code
+# fragments. Normal C and C++ comments will always remain visible.
+
+STRIP_CODE_COMMENTS = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES (the default)
+# then for each documented function all documented
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = YES
+
+# If the REFERENCES_RELATION tag is set to YES (the default)
+# then for each documented function all documented entities
+# called/used by that function will be listed.
+
+REFERENCES_RELATION = YES
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen
+# will generate a verbatim copy of the header file for each class for
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index
+# of all compounds will be generated. Enable this if the project
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX = NO
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX = 5
+
+# In case all classes in a project start with a common prefix, all
+# classes will be put under the same header in the alphabetical index.
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX =
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will
+# generate HTML output.
+
+GENERATE_HTML = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard header.
+
+HTML_HEADER =
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard footer.
+
+HTML_FOOTER =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading
+# style sheet that is used by each HTML page. It can be used to
+# fine-tune the look of the HTML output. If the tag is left blank doxygen
+# will generate a default style sheet
+
+HTML_STYLESHEET = stylesheet.css
+
+# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes,
+# files or namespaces will be aligned in HTML using tables. If set to
+# NO a bullet list will be used.
+
+HTML_ALIGN_MEMBERS = YES
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files
+# will be generated that can be used as input for tools like the
+# Microsoft HTML help workshop to generate a compressed HTML help file (.chm)
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can
+# be used to specify the file name of the resulting .chm file. You
+# can add a path in front of the file if the result should not be
+# written to the html output dir.
+
+CHM_FILE =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can
+# be used to specify the location (absolute path including file name) of
+# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run
+# the HTML help compiler on the generated index.hhp.
+
+HHC_LOCATION =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag
+# controls if a separate .chi index file is generated (YES) or that
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag
+# controls whether a binary table of contents is generated (YES) or a
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members
+# to the contents of the HTML help documentation and to the tree view.
+
+TOC_EXPAND = NO
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index at
+# top of each HTML page. The value NO (the default) enables the index and
+# the value YES disables it.
+
+DISABLE_INDEX = NO
+
+# This tag can be used to set the number of enum values (range [1..20])
+# that doxygen will group on one line in the generated HTML documentation.
+
+ENUM_VALUES_PER_LINE = 4
+
+# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be
+# generated containing a tree-like index structure (just like the one that
+# is generated for HTML Help). For this to work a browser that supports
+# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+,
+# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are
+# probably better off using the HTML help feature.
+
+GENERATE_TREEVIEW = NO
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be
+# used to set the initial width (in pixels) of the frame in which the tree
+# is shown.
+
+TREEVIEW_WIDTH = 250
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will
+# generate Latex output.
+
+GENERATE_LATEX = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked. If left blank `latex' will be used as the default command name.
+
+LATEX_CMD_NAME = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to
+# generate index for LaTeX. If left blank `makeindex' will be used as the
+# default command name.
+
+MAKEINDEX_CMD_NAME = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact
+# LaTeX documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_LATEX = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used
+# by the printer. Possible values are: a4, a4wide, letter, legal and
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE = a4wide
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for
+# the generated latex document. The header should contain everything until
+# the first chapter. If it is left blank doxygen will generate a
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will
+# contain links (just like the HTML output) instead of page references
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS = NO
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of
+# plain latex in the generated Makefile. Set this option to YES to get a
+# higher quality PDF documentation.
+
+USE_PDFLATEX = NO
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode.
+# command to the generated LaTeX files. This will instruct LaTeX to keep
+# running if errors occur, instead of asking the user for help.
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE = NO
+
+# If LATEX_HIDE_INDICES is set to YES then doxygen will not
+# include the index chapters (such as File Index, Compound Index, etc.)
+# in the output.
+
+LATEX_HIDE_INDICES = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output
+# The RTF output is optimised for Word 97 and may not look very pretty with
+# other RTF readers or editors.
+
+GENERATE_RTF = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT = rtf
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact
+# RTF documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_RTF = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated
+# will contain hyperlink fields. The RTF file will
+# contain links (just like the HTML output) instead of page references.
+# This makes the output suitable for online browsing using WORD or other
+# programs which support those fields.
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's
+# config file, i.e. a series of assigments. You only have to provide
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE =
+
+# Set optional variables used in the generation of an rtf document.
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE =
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will
+# generate man pages
+
+GENERATE_MAN = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT = man
+
+# The MAN_EXTENSION tag determines the extension that is added to
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output,
+# then it will generate one additional man file for each entity
+# documented in the real man page(s). These additional files
+# only source the real man page, but without them the man command
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will
+# generate an XML file that captures the structure of
+# the code including all documentation. Note that this
+# feature is still experimental and incomplete at the
+# moment.
+
+GENERATE_XML = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `xml' will be used as the default path.
+
+XML_OUTPUT = xml
+
+# The XML_SCHEMA tag can be used to specify an XML schema,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_SCHEMA =
+
+# The XML_DTD tag can be used to specify an XML DTD,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_DTD =
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will
+# generate an AutoGen Definitions (see autogen.sf.net) file
+# that captures the structure of the code including all
+# documentation. Note that this feature is still experimental
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES Doxygen will
+# generate a Perl module file that captures the structure of
+# the code including all documentation. Note that this
+# feature is still experimental and incomplete at the
+# moment.
+
+GENERATE_PERLMOD = NO
+
+# If the PERLMOD_LATEX tag is set to YES Doxygen will generate
+# the necessary Makefile rules, Perl scripts and LaTeX code to be able
+# to generate PDF and DVI output from the Perl module output.
+
+PERLMOD_LATEX = NO
+
+# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be
+# nicely formatted so it can be parsed by a human reader. This is useful
+# if you want to understand what is going on. On the other hand, if this
+# tag is set to NO the size of the Perl module output will be much smaller
+# and Perl will parse it just the same.
+
+PERLMOD_PRETTY = YES
+
+# The names of the make variables in the generated doxyrules.make file
+# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX.
+# This is useful so different doxyrules.make files included by the same
+# Makefile don't overwrite each other's variables.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will
+# evaluate all C-preprocessor directives found in the sources and include
+# files.
+
+ENABLE_PREPROCESSING = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro
+# names in the source code. If set to NO (the default) only conditional
+# compilation will be performed. Macro expansion can be done in a controlled
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES
+# then the macro expansion is limited to the macros specified with the
+# PREDEFINED and EXPAND_AS_PREDEFINED tags.
+
+EXPAND_ONLY_PREDEF = NO
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files
+# in the INCLUDE_PATH (see below) will be search if a #include is found.
+
+SEARCH_INCLUDES = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by
+# the preprocessor.
+
+INCLUDE_PATH =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will
+# be used.
+
+INCLUDE_FILE_PATTERNS =
+
+# The PREDEFINED tag can be used to specify one or more macro names that
+# are defined before the preprocessor is started (similar to the -D option of
+# gcc). The argument of the tag is a list of macros of the form: name
+# or name=definition (no spaces). If the definition and the = are
+# omitted =1 is assumed.
+
+PREDEFINED =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then
+# this tag can be used to specify a list of macro names that should be expanded.
+# The macro definition that is found in the sources will be used.
+# Use the PREDEFINED tag if you want to use a different macro definition.
+
+EXPAND_AS_DEFINED =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then
+# doxygen's preprocessor will remove all function-like macros that are alone
+# on a line, have an all uppercase name, and do not end with a semicolon. Such
+# function macros are typically used for boiler-plate code, and will confuse the
+# parser if not removed.
+
+SKIP_FUNCTION_MACROS = YES
+
+#---------------------------------------------------------------------------
+# Configuration::addtions related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES option can be used to specify one or more tagfiles.
+# Optionally an initial location of the external documentation
+# can be added for each tagfile. The format of a tag file without
+# this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where "loc1" and "loc2" can be relative or absolute paths or
+# URLs. If a location is present for each tag, the installdox tool
+# does not have to be run to correct the links.
+# Note that each tag file must have a unique name
+# (where the name does NOT include the path)
+# If a tag file is not located in the directory in which doxygen
+# is run, you must also specify the path to the tagfile here.
+
+TAGFILES =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE =
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed
+# in the class index. If set to NO only the inherited external classes
+# will be listed.
+
+ALLEXTERNALS = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will
+# be listed.
+
+EXTERNAL_GROUPS = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will
+# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base or
+# super classes. Setting the tag to NO turns the diagrams off. Note that this
+# option is superceded by the HAVE_DOT option below. This is only a fallback. It is
+# recommended to install and use dot, since it yields more powerful graphs.
+
+CLASS_DIAGRAMS = YES
+
+# If set to YES, the inheritance and collaboration graphs will hide
+# inheritance and usage relations if the target is undocumented
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz, a graph visualization
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT = YES
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect inheritance relations. Setting this tag to YES will force the
+# the CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect implementation dependencies (inheritance, containment, and
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and
+# collaboration diagrams in a style similiar to the OMG's Unified Modeling
+# Language.
+
+UML_LOOK = NO
+
+# If set to YES, the inheritance and collaboration graphs will show the
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS = NO
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT
+# tags are set to YES then doxygen will generate a graph for each documented
+# file showing the direct and indirect include dependencies of the file with
+# other documented files.
+
+INCLUDE_GRAPH = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each
+# documented header file showing the documented files that directly or
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH = YES
+
+# If the CALL_GRAPH and HAVE_DOT tags are set to YES then doxygen will
+# generate a call dependency graph for every global function or class method.
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command.
+
+CALL_GRAPH = NO
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen
+# will graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. Possible values are png, jpg, or gif
+# If left blank png will be used.
+
+DOT_IMAGE_FORMAT = png
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found on the path.
+
+DOT_PATH =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the
+# \dotfile command).
+
+DOTFILE_DIRS =
+
+# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than
+# this value, doxygen will try to truncate the graph, so that it fits within
+# the specified constraint. Beware that most browsers cannot cope with very
+# large images.
+
+MAX_DOT_GRAPH_WIDTH = 1024
+
+# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than
+# this value, doxygen will try to truncate the graph, so that it fits within
+# the specified constraint. Beware that most browsers cannot cope with very
+# large images.
+
+MAX_DOT_GRAPH_HEIGHT = 1024
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the
+# graphs generated by dot. A depth value of 3 means that only nodes reachable
+# from the root by following a path via at most 3 edges will be shown. Nodes that
+# lay further from the root node will be omitted. Note that setting this option to
+# 1 or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that a graph may be further truncated if the graph's image dimensions are
+# not sufficient to fit the graph (see MAX_DOT_GRAPH_WIDTH and MAX_DOT_GRAPH_HEIGHT).
+# If 0 is used for the depth value (the default), the graph is not depth-constrained.
+
+MAX_DOT_GRAPH_DEPTH = 0
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will
+# generate a legend page explaining the meaning of the various boxes and
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will
+# remove the intermediate dot files that are used to generate
+# the various graphs.
+
+DOT_CLEANUP = YES
+
+#---------------------------------------------------------------------------
+# Configuration::addtions related to the search engine
+#---------------------------------------------------------------------------
+
+# The SEARCHENGINE tag specifies whether or not a search engine should be
+# used. If set to NO the values of all tags below this one will be ignored.
+
+SEARCHENGINE = NO
diff --git a/muggle-plugin/muggle.h b/muggle-plugin/muggle.h
new file mode 100644
index 0000000..9ccdd3a
--- /dev/null
+++ b/muggle-plugin/muggle.h
@@ -0,0 +1,73 @@
+/*!
+ * \file muggle.h
+ * \ingroup vdr
+ * \brief Implements a plugin for browsing media libraries within VDR
+ *
+ * \version $Revision: 1.6 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author file owner: $Author$
+ *
+ * $Id$
+ */
+
+// Some notes about the general structure of the plugin
+
+/* \defgroup giantdisc GiantDisc integration layer
+ * The GiantDisc integration layer contains functions and classes
+ * which enable interoperability with a database schema that conforms
+ * to the GiantDisc layout.
+ *
+ * \defgroup vdr VDR integration layer
+ * The VDR integration layer contains components which allow the
+ * plugin functionality to be accessed by VDR. These are mainly
+ * related to the OSD and a player/control combination.
+ *
+ * \defgroup muggle Main muggle business
+ * The core of the plugin is an abstract representation of information
+ * organized in trees (thus suitable for OSD navigation) as well as
+ * means to organize
+ */
+
+#ifndef _MUGGLE_H
+#define _MUGGLE_H
+#include <string>
+#include <stdio.h>
+#include <sys/types.h>
+#include <pthread.h>
+#include <plugin.h>
+
+class mgMainMenu;
+
+class mgMuggle:public cPlugin
+{
+ public:
+
+ mgMuggle (void);
+
+ virtual const char *Version (void);
+
+ virtual const char *Description (void);
+
+ virtual const char *CommandLineHelp (void);
+
+ virtual bool ProcessArgs (int argc, char *argv[]);
+
+ virtual bool Initialize (void);
+
+ virtual bool Start (void);
+
+ virtual void Stop (void);
+
+ virtual void Housekeeping (void);
+
+ virtual const char *MainMenuEntry (void);
+
+ virtual cOsdObject *MainMenuAction (void);
+
+ virtual cMenuSetupPage *SetupMenu (void);
+
+ virtual bool SetupParse (const char *Name, const char *Value);
+
+};
+#endif
diff --git a/muggle-plugin/mugglei.c b/muggle-plugin/mugglei.c
new file mode 100755
index 0000000..76cf7a4
--- /dev/null
+++ b/muggle-plugin/mugglei.c
@@ -0,0 +1,180 @@
+/*!
+ * \file mugglei.c
+ * \brief implement a small utility for importing files
+ *
+ * \author Lars von Wedel
+ */
+
+// #define VERBOSE
+
+#include <unistd.h>
+#include <string>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <mysql/mysql.h>
+#include <getopt.h>
+/*extern "C"
+{*/
+ #include <stdarg.h>
+ #include <stdio.h>
+/*}
+*/
+#include <stdlib.h>
+
+#include <tag.h>
+#include <mpegfile.h>
+#include <flacfile.h>
+#include <id3v2tag.h>
+#include <fileref.h>
+
+#include "mg_tools.h"
+#include "mg_setup.h"
+#include "mg_sync.h"
+
+
+using namespace std;
+
+int SysLogLevel = 1;
+
+bool import_assorted, delete_mode, create_mode;
+
+void showmessage(const char *msg,int duration)
+{
+}
+
+void showimportcount(unsigned int count,bool final=false)
+{
+}
+
+const char *I18nTranslate(const char *s,const char *Plugin)
+{
+ return s;
+}
+
+int main( int argc, char *argv[] )
+{
+ mgSetDebugLevel(1);
+
+ if( argc < 2 )
+ { // we need at least a filename!
+ std::cout << "mugglei -- import helper for Muggle VDR plugin" << std::endl;
+ std::cout << "(C) Lars von Wedel" << std::endl;
+ std::cout << "This is free software; see the source for copying conditions." << std::endl;
+ std::cout << "" << std::endl;
+ std::cout << "Usage: mugglei [OPTION]... [FILE]..." << std::endl;
+ std::cout << "" << std::endl;
+ std::cout << " all FILE arguments will be imported. If they are directories, their content is imported"<< std::endl;
+ std::cout << "" << std::endl;
+ std::cout << "Only files ending in .flac, .mp3, .ogg (ignoring case) will be imported" << std::endl;
+ std::cout << "" << std::endl;
+ std::cout << "Options:" << std::endl;
+#ifdef HAVE_ONLY_SERVER
+ std::cout << " -h <hostname> - specify host of mySql database server (default is 'localhost')" << std::endl;
+#else
+ std::cout << " -h <hostname> - specify host of mySql database server (default is mysql embedded')" << std::endl;
+#endif
+ std::cout << " -s <socket> - specify a socket for mySQL communication (default is TCP)" << std::endl;
+ std::cout << " -n <database> - specify database name (default is 'GiantDisc')" << std::endl;
+ std::cout << " -u <username> - specify user of mySql database (default is empty)" << std::endl;
+ std::cout << " -p <password> - specify password of user (default is empty password)" << std::endl;
+ std::cout << " -t <topleveldir> - name of music top level directory" << std::endl;
+ std::cout << " -z - scan all database entries and delete entries for files not found" << std::endl;
+ std::cout << " -z is not yet implemented" << std::endl;
+ std::cout << " -c - delete the entire database and recreate a new empty one" << std::endl;
+#ifndef HAVE_ONLY_SERVER
+ std::cout << " -d <datadir> - the data directory for the embedded mysql server. Defaults to ./.muggle" << std::endl;
+#endif
+ std::cout << " -v - the wanted log level, the higher the more. Default is 1" << std::endl;
+ std::cout << std::endl << std::endl;
+ std::cout << "if the specified host is localhost, sockets will be used if possible." << std::endl;
+ std::cout << "Otherwise the -s parameter will be ignored" << std::endl;
+
+ exit( 1 );
+ }
+
+ // option defaults
+ import_assorted = false;
+ delete_mode = false;
+ create_mode = false;
+#ifndef HAVE_ONLY_SERVER
+ char *buf;
+ asprintf(&buf,"%s/.muggle",getenv("HOME"));
+ set_datadir(buf);
+ free(buf);
+#endif
+
+ // parse command line options
+ while( 1 )
+ {
+#ifndef HAVE_ONLY_SERVER
+ int c = getopt(argc, argv, "h:s:n:u:p:t:zcv:d:");
+#else
+ int c = getopt(argc, argv, "h:s:n:u:p:t:zcv:");
+#endif
+
+ if (c == -1)
+ break;
+
+ switch (c)
+ {
+ case 0:
+ { // long option
+
+ } break;
+ case 'h':
+ {
+ the_setup.DbHost = optarg;
+ } break;
+ case 'n':
+ {
+ the_setup.DbName = optarg;
+ } break;
+ case 'u':
+ {
+ the_setup.DbUser = optarg;
+ } break;
+ case 'p':
+ {
+ the_setup.DbPass = optarg;
+ } break;
+ case 's':
+ {
+ the_setup.DbSocket = optarg;
+ } break;
+ case 't':
+ {
+ the_setup.ToplevelDir = optarg;
+ } break;
+ case 'z':
+ {
+ delete_mode = true;
+ } break;
+ case 'c':
+ {
+ create_mode = true;
+ } break;
+ case 'v':
+ {
+ mgSetDebugLevel(atol(optarg));
+ } break;
+#ifndef HAVE_ONLY_SERVER
+ case 'd':
+ {
+ set_datadir(optarg);
+ } break;
+#endif
+ }
+ }
+ mgSync *sync = new mgSync; // because we want to delete it before database_end
+ if (create_mode)
+ sync->Create();
+ if (optind<argc)
+ sync->Sync(argv+optind,delete_mode);
+ delete sync;
+ database_end();
+ return 0;
+}
+
diff --git a/muggle-plugin/mugglei.h b/muggle-plugin/mugglei.h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/muggle-plugin/mugglei.h
diff --git a/muggle-plugin/scripts/COPYRIGHT b/muggle-plugin/scripts/COPYRIGHT
new file mode 100644
index 0000000..05339ef
--- /dev/null
+++ b/muggle-plugin/scripts/COPYRIGHT
@@ -0,0 +1,17 @@
+the content of languages.txt is generated from the file
+iso_639.xml which contains this copyright:
+
+<?xml version="1.0" encoding="UTF-8" ?>
+<!-- iso-639.tab -->
+<!-- -->
+<!-- Copyright (C) 2004 Alastair McKinstry <mckinstry@computer.org> -->
+<!-- Released under the GNU License; see file COPYING for details -->
+<!-- -->
+<!-- Last update: 2004-10-27 -->
+<!-- -->
+<!-- This file gives a list of all languages in the ISO-639 -->
+<!-- standard, and is used to provide translations (via gettext) -->
+<!-- -->
+<!-- Status: ISO 639-2:1998 + additions and changes until 2004-03-05 -->
+<!-- Source: http://lcweb.loc.gov/standards/iso639-2/englangn.html -->
+
diff --git a/muggle-plugin/scripts/createdb.mysql b/muggle-plugin/scripts/createdb.mysql
new file mode 100755
index 0000000..da1234a
--- /dev/null
+++ b/muggle-plugin/scripts/createdb.mysql
@@ -0,0 +1,2 @@
+-- NO LONGER NEEDED.
+-- SEE README \ No newline at end of file
diff --git a/muggle-plugin/scripts/createtables.mysql b/muggle-plugin/scripts/createtables.mysql
new file mode 100755
index 0000000..da1234a
--- /dev/null
+++ b/muggle-plugin/scripts/createtables.mysql
@@ -0,0 +1,2 @@
+-- NO LONGER NEEDED.
+-- SEE README \ No newline at end of file
diff --git a/muggle-plugin/scripts/genres.txt b/muggle-plugin/scripts/genres.txt
new file mode 100755
index 0000000..25655e4
--- /dev/null
+++ b/muggle-plugin/scripts/genres.txt
@@ -0,0 +1,274 @@
+b 20 Alternative
+ba 40 Alt. Rock
+bb \N Art Rock
+bc 90 Avantgarde
+be \N Experimental
+bh 6 Grunge
+bi 131 Indie
+bm 12 Other
+bn 139 Crossover
+c \N Books & Spoken
+ca \N Short Stories
+cb 57 Comedy
+cc 77 Musical
+cd \N Poetry
+ce 65 Cabaret
+cf \N Religion
+cg 101 Speech
+ch \N Stories/Fairytales
+ci \N Radio Play
+cia \N Literary Radio Play
+cib \N Thriller
+e 32 Classical
+ea 104 Chamber Music
+eaa 105 Sonata
+eb \N Classical General
+ec \N Contemporary
+eca \N Contemp. Crossover
+ecb \N Electronic Classical
+ecc \N Experimental Classical
+ecd \N Minimal Music
+ed 24 Soundtrack
+ee 33 Instrumental
+ef \N Period Music
+efa \N Baroque
+efb \N Medieval
+efc \N Renaissance
+efd \N Romantic
+efda \N 19th Century
+eg \N Solo Instruments
+ega \N Guitar
+egb \N Percussion
+egc \N Piano
+eh 106 Symphony
+ei 28 Vocal
+eia 97 Chorus
+eib \N Ensembles
+eic 103 Opera
+f 2 Country
+fa \N Alternative Country
+fb 89 Bluegrass
+fd \N Country Blues
+fe \N Country General
+fg \N Country and Western
+fh 80 Folk
+fha \N Irish Folk
+fi \N Rockabilly
+g 98 Easy Listening
+gb \N Lounge
+gc \N Love Songs
+gca 116 Ballad
+gd \N Mood Music
+ge 10 New Age
+gf \N Soft Rock
+gfa \N Acoustic Rock
+gg \N Schlager
+gh \N Soft Pop
+gi 45 Meditative
+gj \N Pair Dance
+gja \N Walz
+gjb \N Tango
+h 102 Chanson
+ha \N Singer-Songwriter
+hb 65 Children's Music
+i 52 Electronic
+ia 34 Acid
+ib 26 Ambient
+ic \N Breakbeat/Breaks
+ica \N Breakbeat
+icb \N Darkside
+icc 63 Jungle
+icd \N Ragga
+ice 27 Trip-Hop
+id 3 Dance
+ie 127 Drum & Bass
+if \N Electronica
+ig \N Envir. Soundscapes
+ih \N Experimental Elect.
+iha \N Minimal Experimental
+ihb 39 Noise
+ii 37 Sound Clip
+ij 35 House
+ija \N Acid House
+ijb \N Funk House
+ijc \N Hard House
+ijd \N Progressive House
+ije 124 Euro-House
+ijf 128 Club-House
+ik 19 Industrial
+il 18 Techno
+ilb \N Dub
+ild 126 Goa
+ile \N Hardcore Techno
+ilf \N Illbient
+ilg \N Minimal
+ilh 25 Euro-Techno
+ili 68 Rave
+ilj 31 Trance
+im 44 Space Music
+j \N Hip Hop/Rap
+ja 7 Hip-Hop
+jb 15 Rap
+jbb 61 Christian Rap
+jbd \N Hardcore Rap
+jbf 59 Gangsta
+k \N Blues/R&B
+ka 0 Blues
+kaa \N Acoustic Blues
+kab \N Blues Rock
+kac \N Blues Vocalist
+kae \N Electric Blues
+kag \N Jazz Blues
+kb 38 Gospel
+kc \N Improvised
+kd 14 R&B
+ke 42 Soul
+kea \N Sweet Soul
+l 8 Jazz
+la 73 Acid Punk
+lb 85 Bebob
+lc \N Dancefloor Jazz
+lf 30 Jazz Fusion
+lh \N Jazz Vocals
+lj \N Ragtime
+lk \N Smooth Jazz
+ll 83 Swing
+lla 96 Big Band
+llb 76 Retro-Swing
+lm \N Cool Jazz
+ln \N Ethno Jazz
+lna \N African Ethno Jazz
+lnb \N Arab Ethno Jazz
+lnc \N Cuban Jazz
+lnd \N Latin Jazz
+lne \N Far East Jazz
+lo \N Modern Jazz
+lp \N New Orleans Brass
+m \N Pop & Rock
+ma \N Country Rock
+mb 5 Funk
+mba \N Acid Funk
+mc 9 Metal
+mcc 22 Death Metal
+mcd \N Doom Metal
+mcf \N Hard Core Metal
+mcg \N Heavy Metal
+mck \N Thrash Metal
+md 13 Pop
+mda 99 Acoustic
+mdb \N Synthesizer Pop
+mdd \N Latin Pop
+mdg 123 A Capella
+mdh \N Neue Deutsche Welle
+mdi 4 Disco
+mdj \N Dance Pop
+mdja \N Twist
+mdk \N Doo-Wop
+me 43 Punk
+mea 121 Hardcore/Punk Rock
+meb 71 Lo-Fi/Garage
+mec \N Old School Punk
+med 21 Ska
+mf 17 Rock
+mfb \N Acid Rock
+mfe 81 Folk/Rock
+mfg \N Groove Rock
+mfh \N Guitar Rock
+mfha 1 Classic Rock
+mfhb \N Improv Rock
+mfhc 47 Instrum. Rock
+mfhf \N Surf Rock
+mfj 66 New Wave
+mfk 67 Psychadelic
+mfl 78 Rock & Roll
+mfm 94 Symphonic Rock
+mg \N Rock En Espanol
+n \N World
+na 16 Reggae
+nb \N Steel Drums
+nc \N World Popular
+nca \N African Pop
+ncb \N Oriental Pop
+ncc \N Scandinavian Ethnopop
+ncd \N Asian Pop
+nce \N Arabian Pop
+ncf \N European Ethnopop
+ncg \N Latin Pop
+nch \N Caribbean Pop
+nd 82 National Folk
+nda \N African
+ndaa \N Mali Blues
+ndb \N Arabic
+ndc \N Asian
+ndd \N Bossa Nova
+nde \N Caribbean
+ndf 88 Celtic
+ndg 53 Pop-Folk
+ndga \N Jodel
+ndh \N France
+ndi \N Germany
+ndj \N India
+ndk \N Ireland
+ndl 86 Latin
+ndlb \N Flamenco
+ndlc \N Mambo
+ndld \N Mariachi
+ndle 142 Merengue
+ndlg 143 Salsa
+ndlh 114 Samba
+ndm 64 Native American
+ndn \N Quebecois
+ndo \N Russian
+ndp \N South/Cent. American
+ndq \N Spain
+ndr 113 Tango
+y 11 Oldies
+y2 49 Gothic
+y6 23 Pranks
+y9 29 Jazz+Funk
+y12 46 Instrum. Pop
+y14 48 Ethnic
+y15 50 Darkwave
+y16 51 Techno-Indust.
+y18 54 Eurodance
+y19 55 Dream
+y20 56 Southern Rock
+y21 58 Cult
+y22 60 Top 40
+y23 62 Pop/Funk
+y25 69 Showtunes
+y26 70 Trailer
+y27 72 Tribal
+y29 75 Polka
+y30 79 Hard Rock
+y34 87 Revival
+y36 91 Gothic Rock
+y37 92 Progress. Rock
+y38 93 Psychadel. Rock
+y39 95 Slow Rock
+y41 100 Humour
+y42 107 Booty Bass
+y43 108 Primus
+y44 109 Porn Groove
+y45 111 Slow Jam
+y46 112 Club
+y47 115 Folklore
+y48 117 Power Ballad
+y49 118 Rhythmic Soul
+y50 119 Freestyle
+y51 120 Duet
+y52 122 Drum Solo
+y55 125 Dance Hall
+y58 130 Terror
+y59 132 BritPop
+y60 133 Negerpunk
+y61 134 Polsk Punk
+y62 135 Beat
+y63 136 Christian Gangsta Rap
+y64 138 Black Metal
+y65 140 Contemporary Christian
+y66 141 Christian Rock
+y69 145 Anime
+y70 146 Jpop
+y71 147 Synthpop
+ NULL
diff --git a/muggle-plugin/scripts/gentables b/muggle-plugin/scripts/gentables
new file mode 100755
index 0000000..f7fcc89
--- /dev/null
+++ b/muggle-plugin/scripts/gentables
@@ -0,0 +1,47 @@
+
+(
+ echo "
+//autogenerated by `pwd -P`/$0
+
+genres_t genres[] = {"
+
+cat scripts/genres.txt | while read gdid id3 name
+do
+ test "$id3" = N && id3=-1
+ test "$gdid" = NULL && break
+ echo ' { "'$gdid'", '$id3', "'$name'" },'
+done
+echo '};
+'
+
+echo "lang_t languages[] = {"
+
+scripts/iso639tab.py scripts/iso_639.xml |
+ grep -v '^#' |
+ grep -v '^$' |
+ while read iso1 iso2 iso3 name
+do
+ echo ' { "'$iso2'", "'$name'" },'
+done
+echo '};
+'
+
+echo "musictypes_t musictypes[] = {"
+
+cat scripts/musictypes.txt | while read mtype
+do
+ echo ' { "'$mtype'"},'
+done
+echo '};
+'
+
+echo "sources_t sources[] = {"
+
+cat scripts/sources.txt | while read stype
+do
+ echo ' { "'$stype'"},'
+done
+echo '};'
+) >mg_tables.h
+
+
diff --git a/muggle-plugin/scripts/iso639tab.py b/muggle-plugin/scripts/iso639tab.py
new file mode 100755
index 0000000..b2739c9
--- /dev/null
+++ b/muggle-plugin/scripts/iso639tab.py
@@ -0,0 +1,81 @@
+#!/usr/bin/python
+#
+# Read iso-codes iso_639.xml data file and output a .tab file
+#
+# Copyright (C) 2005 Alastair McKinstry <mckinstry@debian.org>
+# Released under the GPL.
+# $Id: iso639tab.py,v 1.1 2005/03/02 07:24:51 mckinstry Exp $
+
+from xml.sax import saxutils, make_parser, saxlib, saxexts, ContentHandler
+from xml.sax.handler import feature_namespaces
+import sys, os, getopt, urllib2
+
+class printLines(saxutils.DefaultHandler):
+ def __init__(self, ofile):
+ self.ofile = ofile
+
+ def startElement(self, name, attrs):
+ if name != 'iso_639_entry':
+ return
+ t_code = attrs.get('iso_639_2T_code', None)
+ if t_code == None:
+ raise RunTimeError, "Bad file"
+ if type(t_code) == unicode:
+ t_code = t_code.encode('UTF-8')
+ b_code = attrs.get('iso_639_2B_code', None)
+ if b_code == None:
+ raise RunTimeError, "Bad file"
+ if type(b_code) == unicode:
+ b_code = b_code.encode('UTF-8')
+ name = attrs.get('name', None)
+ if name == None:
+ raise RunTimeError, " BadFile"
+ short_code=attrs.get('iso_639_1_code','XX')
+ short_code=short_code.encode('UTF-8')
+ if type(name) == unicode:
+ name = name.encode('UTF-8')
+ self.ofile.write (t_code + '\t' + b_code + '\t' + short_code + '\t' + name + '\n')
+
+
+##
+## MAIN
+##
+
+
+ofile = sys.stdout
+ofile.write("""
+## iso-639.tab
+##
+## Copyright (C) 2005 Alastair McKinstry <mckinstry@computer.org>
+## Released under the GNU License; see file COPYING for details
+##
+## PLEASE NOTE: THIS FILE IS DEPRECATED AND SCHEDULED TO BE REMOVED.
+## IT IS FOR BACKWARD-COMPATIBILITY ONLY: PLEASE USE THE ISO-639.XML
+## FILE INSTEAD.
+##
+## This file gives a list of all languages in the ISO-639
+## standard, and is used to provide translations (via gettext)
+##
+## Status: ISO 639-2:1998 + additions and changes until 2003-03-05
+## Source: http://lcweb.loc.gov/standards/iso639-2/englangn.html
+##
+## Columns:
+## iso-639-2 terminology code
+## iso-639-2 bibliography code
+## iso-639-1 code (XX if none exists)
+## Name (English)
+##
+##
+""")
+p = make_parser()
+p.setErrorHandler(saxutils.ErrorPrinter())
+try:
+ dh = printLines(ofile)
+ p.setContentHandler(dh)
+ p.parse(sys.argv[1])
+except IOError,e:
+ print in_sysID+": "+str(e)
+except saxlib.SAXException,e:
+ print str(e)
+
+ofile.close()
diff --git a/muggle-plugin/scripts/iso_639.xml b/muggle-plugin/scripts/iso_639.xml
new file mode 100644
index 0000000..ed6fc73
--- /dev/null
+++ b/muggle-plugin/scripts/iso_639.xml
@@ -0,0 +1,2072 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!-- iso-639.tab -->
+<!-- -->
+<!-- Copyright (C) 2004 Alastair McKinstry <mckinstry@computer.org> -->
+<!-- Released under the GNU License; see file COPYING for details -->
+<!-- -->
+<!-- Last update: 2004-10-27 -->
+<!-- -->
+<!-- This file gives a list of all languages in the ISO-639 -->
+<!-- standard, and is used to provide translations (via gettext) -->
+<!-- -->
+<!-- Status: ISO 639-2:1998 + additions and changes until 2004-03-05 -->
+<!-- Source: http://lcweb.loc.gov/standards/iso639-2/englangn.html -->
+
+<iso_639_entries>
+ <iso_639_entry
+ iso_639_2B_code="aar"
+ iso_639_2T_code="aar"
+ iso_639_1_code="aa"
+ name="Afar" />
+ <iso_639_entry
+ iso_639_2B_code="abk"
+ iso_639_2T_code="abk"
+ iso_639_1_code="ab"
+ name="Abkhazian" />
+ <iso_639_entry
+ iso_639_2B_code="ace"
+ iso_639_2T_code="ace"
+ name="Achinese" />
+ <iso_639_entry
+ iso_639_2B_code="ach"
+ iso_639_2T_code="ach"
+ name="Acoli" />
+ <iso_639_entry
+ iso_639_2B_code="ada"
+ iso_639_2T_code="ada"
+ name="Adangme" />
+ <iso_639_entry
+ iso_639_2B_code="ady"
+ iso_639_2T_code="ady"
+ name="Adyghe; Adygei" />
+ <iso_639_entry
+ iso_639_2B_code="afa"
+ iso_639_2T_code="afa"
+ name="Afro-Asiatic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="afh"
+ iso_639_2T_code="afh"
+ name="Afrihili" />
+ <iso_639_entry
+ iso_639_2B_code="afr"
+ iso_639_2T_code="afr"
+ iso_639_1_code="af"
+ name="Afrikaans" />
+ <iso_639_entry
+ iso_639_2B_code="aka"
+ iso_639_2T_code="aka"
+ iso_639_1_code="ak"
+ name="Akan" />
+ <iso_639_entry
+ iso_639_2B_code="akk"
+ iso_639_2T_code="akk"
+ name="Akkadian" />
+ <iso_639_entry
+ iso_639_2B_code="alb"
+ iso_639_2T_code="sqi"
+ iso_639_1_code="sq"
+ name="Albanian" />
+ <iso_639_entr
+ iso_639_2B_code="alg"
+ iso_639_2T_code="alg"
+ name="Algonquian languages" />
+ <iso_639_entry
+ iso_639_2B_code="amh"
+ iso_639_2T_code="amh"
+ iso_639_1_code="am"
+ name="Amharic" />
+ <iso_639_entry
+ iso_639_2B_code="ang"
+ iso_639_2T_code="ang"
+ name="English, Old (ca.450-1100)" />
+ <iso_639_entry
+ iso_639_2B_code="apa"
+ iso_639_2T_code="apa"
+ name="Apache languages" />
+ <iso_639_entry
+ iso_639_2B_code="ara"
+ iso_639_2T_code="ara"
+ iso_639_1_code="ar"
+ name="Arabic" />
+ <iso_639_entry
+ iso_639_2B_code="arc"
+ iso_639_2T_code="arc"
+ name="Aramaic" />
+ <iso_639_entry
+ iso_639_2B_code="arg"
+ iso_639_2T_code="arg"
+ iso_639_1_code="an"
+ name="Aragonese" />
+ <iso_639_entry
+ iso_639_2B_code="arm"
+ iso_639_2T_code="hye"
+ iso_639_1_code="hy"
+ name="Armenian" />
+ <iso_639_entry
+ iso_639_2B_code="arn"
+ iso_639_2T_code="arn"
+ name="Araucanian" />
+ <iso_639_entry
+ iso_639_2B_code="arp"
+ iso_639_2T_code="arp"
+ name="Arapaho" />
+ <iso_639_entry
+ iso_639_2B_code="art"
+ iso_639_2T_code="art"
+ name="Artificial (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="arw"
+ iso_639_2T_code="arw"
+ name="Arawak" />
+ <iso_639_entry
+ iso_639_2B_code="asm"
+ iso_639_2T_code="asm"
+ iso_639_1_code="as"
+ name="Assamese" />
+ <iso_639_entry
+ iso_639_2B_code="ast"
+ iso_639_2T_code="ast"
+ name="Asturian; Bable" />
+ <iso_639_entry
+ iso_639_2B_code="ath"
+ iso_639_2T_code="ath"
+ name="Athapascan language" />
+ <iso_639_entry
+ iso_639_2B_code="aus"
+ iso_639_2T_code="aus"
+ name="Australian languages" />
+ <iso_639_entry
+ iso_639_2B_code="ava"
+ iso_639_2T_code="ava"
+ iso_639_1_code="av"
+ name="Avaric" />
+ <iso_639_entry
+ iso_639_2B_code="ave"
+ iso_639_2T_code="ave"
+ iso_639_1_code="ae"
+ name="Avestan" />
+ <iso_639_entry
+ iso_639_2B_code="awa"
+ iso_639_2T_code="awa"
+ name="Awadhi" />
+ <iso_639_entry
+ iso_639_2B_code="aym"
+ iso_639_2T_code="aym"
+ iso_639_1_code="ay"
+ name="Aymara" />
+ <iso_639_entry
+ iso_639_2B_code="aze"
+ iso_639_2T_code="aze"
+ iso_639_1_code="az"
+ name="Azerbaijani" />
+ <iso_639_entry
+ iso_639_2B_code="bad"
+ iso_639_2T_code="bad"
+ name="Banda" />
+ <iso_639_entry
+ iso_639_2B_code="bai"
+ iso_639_2T_code="bai"
+ name="Bamileke languages" />
+ <iso_639_entry
+ iso_639_2B_code="bak"
+ iso_639_2T_code="bak"
+ iso_639_1_code="ba"
+ name="Bashkir" />
+ <iso_639_entry
+ iso_639_2B_code="bal"
+ iso_639_2T_code="bal"
+ name="Baluchi" />
+ <iso_639_entry
+ iso_639_2B_code="bam"
+ iso_639_2T_code="bam"
+ iso_639_1_code="bm"
+ name="Bambara" />
+ <iso_639_entry
+ iso_639_2B_code="ban"
+ iso_639_2T_code="ban"
+ name="Balinese" />
+ <iso_639_entry
+ iso_639_2B_code="baq"
+ iso_639_2T_code="eus"
+ iso_639_1_code="eu"
+ name="Basque" />
+ <iso_639_entry
+ iso_639_2B_code="bas"
+ iso_639_2T_code="bas"
+ name="Basa" />
+ <iso_639_entry
+ iso_639_2B_code="bat"
+ iso_639_2T_code="bat"
+ name="Baltic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="bej"
+ iso_639_2T_code="bej"
+ name="Beja" />
+ <iso_639_entry
+ iso_639_2B_code="bel"
+ iso_639_2T_code="bel"
+ iso_639_1_code="be"
+ name="Belarusian" />
+ <iso_639_entry
+ iso_639_2B_code="bem"
+ iso_639_2T_code="bem"
+ name="Bemba" />
+ <iso_639_entry
+ iso_639_2B_code="ben"
+ iso_639_2T_code="ben"
+ iso_639_1_code="bn"
+ name="Bengali" />
+ <iso_639_entry
+ iso_639_2B_code="ber"
+ iso_639_2T_code="ber"
+ name="Berber (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="bho"
+ iso_639_2T_code="bho"
+ name="Bhojpuri" />
+ <iso_639_entry
+ iso_639_2B_code="bih"
+ iso_639_2T_code="bih"
+ iso_639_1_code="bh"
+ name="Bihari" />
+ <iso_639_entry
+ iso_639_2B_code="bik"
+ iso_639_2T_code="bik"
+ name="Bikol" />
+ <iso_639_entry
+ iso_639_2B_code="bin"
+ iso_639_2T_code="bin"
+ name="Bini" />
+ <iso_639_entry
+ iso_639_2B_code="bis"
+ iso_639_2T_code="bis"
+ iso_639_1_code="bi"
+ name="Bislama" />
+ <iso_639_entry
+ iso_639_2B_code="bla"
+ iso_639_2T_code="bla"
+ name="Siksika" />
+ <iso_639_entry
+ iso_639_2B_code="bnt"
+ iso_639_2T_code="bnt"
+ name="Bantu (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="bos"
+ iso_639_2T_code="bos"
+ iso_639_1_code="bs"
+ name="Bosnian" />
+ <iso_639_entry
+ iso_639_2B_code="bra"
+ iso_639_2T_code="bra"
+ name="Braj" />
+ <iso_639_entry
+ iso_639_2B_code="bre"
+ iso_639_2T_code="bre"
+ iso_639_1_code="br"
+ name="Breton" />
+ <iso_639_entry
+ iso_639_2B_code="btk"
+ iso_639_2T_code="btk"
+ name="Batak (Indonesia)" />
+ <iso_639_entry
+ iso_639_2B_code="bua"
+ iso_639_2T_code="bua"
+ name="Buriat" />
+ <iso_639_entry
+ iso_639_2B_code="bug"
+ iso_639_2T_code="bug"
+ name="Buginese" />
+ <iso_639_entry
+ iso_639_2B_code="bul"
+ iso_639_2T_code="bul"
+ iso_639_1_code="bg"
+ name="Bulgarian" />
+ <iso_639_entry
+ iso_639_2B_code="bur"
+ iso_639_2T_code="mya"
+ iso_639_1_code="my"
+ name="Burmese" />
+ <iso_639_entry
+ iso_639_2B_code="byn"
+ iso_639_2T_code="byn"
+ name="Blin; Bilin" />
+ <iso_639_entry
+ iso_639_2B_code="cad"
+ iso_639_2T_code="cad"
+ name="Caddo" />
+ <iso_639_entry
+ iso_639_2B_code="cai"
+ iso_639_2T_code="cai"
+ name="Central American Indian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="car"
+ iso_639_2T_code="car"
+ name="Carib" />
+ <iso_639_entry
+ iso_639_2B_code="cat"
+ iso_639_2T_code="cat"
+ iso_639_1_code="ca"
+ name="Catalan" />
+ <iso_639_entry
+ iso_639_2B_code="cau"
+ iso_639_2T_code="cau"
+ name="Caucasian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="ceb"
+ iso_639_2T_code="ceb"
+ name="Cebuano" />
+ <iso_639_entry
+ iso_639_2B_code="cel"
+ iso_639_2T_code="cel"
+ name="Celtic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="cha"
+ iso_639_2T_code="cha"
+ iso_639_1_code="ch"
+ name="Chamorro" />
+ <iso_639_entry
+ iso_639_2B_code="chb"
+ iso_639_2T_code="chb"
+ name="Chibcha" />
+ <iso_639_entry
+ iso_639_2B_code="che"
+ iso_639_2T_code="che"
+ iso_639_1_code="ce"
+ name="Chechen" />
+ <iso_639_entry
+ iso_639_2B_code="chg"
+ iso_639_2T_code="chg"
+ name="Chagatai" />
+ <iso_639_entry
+ iso_639_2B_code="chi"
+ iso_639_2T_code="zho"
+ iso_639_1_code="zh"
+ name="Chinese" />
+ <iso_639_entry
+ iso_639_2B_code="chk"
+ iso_639_2T_code="chk"
+ name="Chukese" />
+ <iso_639_entry
+ iso_639_2B_code="chm"
+ iso_639_2T_code="chm"
+ name="Mari" />
+ <iso_639_entry
+ iso_639_2B_code="chn"
+ iso_639_2T_code="chn"
+ name="Chinook jargon" />
+ <iso_639_entry
+ iso_639_2B_code="cho"
+ iso_639_2T_code="cho"
+ name="Choctaw" />
+ <iso_639_entry
+ iso_639_2B_code="chp"
+ iso_639_2T_code="chp"
+ name="Chipewyan" />
+ <iso_639_entry
+ iso_639_2B_code="chr"
+ iso_639_2T_code="chr"
+ name="Cherokee" />
+ <iso_639_entry
+ iso_639_2B_code="chu"
+ iso_639_2T_code="chu"
+ name="Church Slavic" />
+ <iso_639_entry
+ iso_639_2B_code="chv"
+ iso_639_2T_code="chv"
+ iso_639_1_code="cv"
+ name="Chuvash" />
+ <iso_639_entry
+ iso_639_2B_code="chy"
+ iso_639_2T_code="chy"
+ name="Cheyenne" />
+ <iso_639_entry
+ iso_639_2B_code="cmc"
+ iso_639_2T_code="cmc"
+ name="Chamic languages" />
+ <iso_639_entry
+ iso_639_2B_code="cop"
+ iso_639_2T_code="cop"
+ name="Coptic" />
+ <iso_639_entry
+ iso_639_2B_code="cor"
+ iso_639_2T_code="cor"
+ iso_639_1_code="kw"
+ name="Cornish" />
+ <iso_639_entry
+ iso_639_2B_code="cos"
+ iso_639_2T_code="cos"
+ iso_639_1_code="co"
+ name="Corsican" />
+ <iso_639_entry
+ iso_639_2B_code="cpe"
+ iso_639_2T_code="cpe"
+ name="English-based (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="cpf"
+ iso_639_2T_code="cpf"
+ name="French-based (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="cpp"
+ iso_639_2T_code="cpp"
+ name="Portuguese-based (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="cre"
+ iso_639_2T_code="cre"
+ iso_639_1_code="cr"
+ name="Cree" />
+ <iso_639_entry
+ iso_639_2B_code="crh"
+ iso_639_2T_code="crh"
+ name="Crimean Turkish; Crimean Tatar" />
+ <iso_639_entry
+ iso_639_2B_code="crp"
+ iso_639_2T_code="crp"
+ name="Creoles and pidgins (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="csb"
+ iso_639_2T_code="csb"
+ name="Kashubian" />
+ <iso_639_entry
+ iso_639_2B_code="cus"
+ iso_639_2T_code="cus"
+ name="Cushitic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="cze"
+ iso_639_2T_code="ces"
+ iso_639_1_code="cs"
+ name="Czech" />
+ <iso_639_entry
+ iso_639_2B_code="dak"
+ iso_639_2T_code="dak"
+ name="Dakota" />
+ <iso_639_entry
+ iso_639_2B_code="dan"
+ iso_639_2T_code="dan"
+ iso_639_1_code="da"
+ name="Danish" />
+ <iso_639_entry
+ iso_639_2B_code="dar"
+ iso_639_2T_code="dar"
+ name="Dargwa" />
+ <iso_639_entry
+ iso_639_2B_code="del"
+ iso_639_2T_code="del"
+ name="Delaware" />
+ <iso_639_entry
+ iso_639_2B_code="den"
+ iso_639_2T_code="den"
+ name="Slave (Athapascan)" />
+ <iso_639_entry
+ iso_639_2B_code="dgr"
+ iso_639_2T_code="dgr"
+ name="Dogrib" />
+ <iso_639_entry
+ iso_639_2B_code="din"
+ iso_639_2T_code="din"
+ name="Dinka" />
+ <iso_639_entry
+ iso_639_2B_code="div"
+ iso_639_2T_code="div"
+ iso_639_1_code="dv"
+ name="Divehi" />
+ <iso_639_entry
+ iso_639_2B_code="doi"
+ iso_639_2T_code="doi"
+ name="Dogri" />
+ <iso_639_entry
+ iso_639_2B_code="dra"
+ iso_639_2T_code="dra"
+ name="Dravidian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="dsb"
+ iso_639_2T_code="dsb"
+ name="Lower Sorbian" />
+ <iso_639_entry
+ iso_639_2B_code="dua"
+ iso_639_2T_code="dua"
+ name="Duala" />
+ <iso_639_entry
+ iso_639_2B_code="dum"
+ iso_639_2T_code="dum"
+ name="Dutch, Middle (ca. 1050-1350)" />
+ <iso_639_entry
+ iso_639_2B_code="dut"
+ iso_639_2T_code="nld"
+ iso_639_1_code="nl"
+ name="Dutch" />
+ <iso_639_entry
+ iso_639_2B_code="dyu"
+ iso_639_2T_code="dyu"
+ name="Dyula" />
+ <iso_639_entry
+ iso_639_2B_code="dzo"
+ iso_639_2T_code="dzo"
+ iso_639_1_code="dz"
+ name="Dzongkha" />
+ <iso_639_entry
+ iso_639_2B_code="efi"
+ iso_639_2T_code="efi"
+ name="Efik" />
+ <iso_639_entry
+ iso_639_2B_code="egy"
+ iso_639_2T_code="egy"
+ name="Egyptian (Ancient)" />
+ <iso_639_entry
+ iso_639_2B_code="eka"
+ iso_639_2T_code="eka"
+ name="Ekajuk" />
+ <iso_639_entry
+ iso_639_2B_code="elx"
+ iso_639_2T_code="elx"
+ name="Elamite" />
+ <iso_639_entry
+ iso_639_2B_code="eng"
+ iso_639_2T_code="eng"
+ iso_639_1_code="en"
+ name="English" />
+ <iso_639_entry
+ iso_639_2B_code="enm"
+ iso_639_2T_code="enm"
+ name="English, Middle (1100-1500)" />
+ <iso_639_entry
+ iso_639_2B_code="epo"
+ iso_639_2T_code="epo"
+ iso_639_1_code="eo"
+ name="Esperanto" />
+ <iso_639_entry
+ iso_639_2B_code="est"
+ iso_639_2T_code="est"
+ iso_639_1_code="et"
+ name="Estonian" />
+ <iso_639_entry
+ iso_639_2B_code="ewe"
+ iso_639_2T_code="ewe"
+ iso_639_1_code="ee"
+ name="Ewe" />
+ <iso_639_entry
+ iso_639_2B_code="ewo"
+ iso_639_2T_code="ewo"
+ name="Ewondo" />
+ <iso_639_entry
+ iso_639_2B_code="fan"
+ iso_639_2T_code="fan"
+ name="Fang" />
+ <iso_639_entry
+ iso_639_2B_code="fao"
+ iso_639_2T_code="fao"
+ iso_639_1_code="fo"
+ name="Faroese" />
+ <iso_639_entry
+ iso_639_2B_code="fat"
+ iso_639_2T_code="fat"
+ name="Fanti" />
+ <iso_639_entry
+ iso_639_2B_code="fij"
+ iso_639_2T_code="fij"
+ iso_639_1_code="fj"
+ name="Fijian" />
+ <iso_639_entry
+ iso_639_2B_code="fil"
+ iso_639_2T_code="fil"
+ name="Filipino; Pilipino" />
+ <iso_639_entry
+ iso_639_2B_code="fin"
+ iso_639_2T_code="fin"
+ iso_639_1_code="fi"
+ name="Finnish" />
+ <iso_639_entry
+ iso_639_2B_code="fiu"
+ iso_639_2T_code="fiu"
+ name="Finno-Ugrian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="fon"
+ iso_639_2T_code="fon"
+ name="Fon" />
+ <iso_639_entry
+ iso_639_2B_code="fre"
+ iso_639_2T_code="fra"
+ iso_639_1_code="fr"
+ name="French" />
+ <iso_639_entry
+ iso_639_2B_code="frm"
+ iso_639_2T_code="frm"
+ name="French, Middle (ca.1400-1600)" />
+ <iso_639_entry
+ iso_639_2B_code="fro"
+ iso_639_2T_code="fro"
+ name="French, Old (842-ca.1400)" />
+ <iso_639_entry
+ iso_639_2B_code="fry"
+ iso_639_2T_code="fry"
+ iso_639_1_code="fy"
+ name="Frisian" />
+ <iso_639_entry
+ iso_639_2B_code="ful"
+ iso_639_2T_code="ful"
+ iso_639_1_code="ff"
+ name="Fulah" />
+ <iso_639_entry
+ iso_639_2B_code="fur"
+ iso_639_2T_code="fur"
+ name="Friulian" />
+ <iso_639_entry
+ iso_639_2B_code="gaa"
+ iso_639_2T_code="gaa"
+ name="Ga" />
+ <iso_639_entry
+ iso_639_2B_code="gay"
+ iso_639_2T_code="gay"
+ name="Gayo" />
+ <iso_639_entry
+ iso_639_2B_code="gba"
+ iso_639_2T_code="gba"
+ name="Gbaya" />
+ <iso_639_entry
+ iso_639_2B_code="gem"
+ iso_639_2T_code="gem"
+ name="Germanic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="geo"
+ iso_639_2T_code="kat"
+ iso_639_1_code="ka"
+ name="Georgian" />
+ <iso_639_entry
+ iso_639_2B_code="ger"
+ iso_639_2T_code="deu"
+ iso_639_1_code="de"
+ name="German" />
+ <iso_639_entry
+ iso_639_2B_code="gez"
+ iso_639_2T_code="gez"
+ name="Geez" />
+ <iso_639_entry
+ iso_639_2B_code="gil"
+ iso_639_2T_code="gil"
+ name="Gilbertese" />
+ <iso_639_entry
+ iso_639_2B_code="gla"
+ iso_639_2T_code="gla"
+ iso_639_1_code="gd"
+ name="Gaelic; Scottish" />
+ <iso_639_entry
+ iso_639_2B_code="gle"
+ iso_639_2T_code="gle"
+ iso_639_1_code="ga"
+ name="Irish" />
+ <iso_639_entry
+ iso_639_2B_code="glg"
+ iso_639_2T_code="glg"
+ iso_639_1_code="gl"
+ name="Gallegan" />
+ <iso_639_entry
+ iso_639_2B_code="glv"
+ iso_639_2T_code="glv"
+ iso_639_1_code="gv"
+ name="Manx" />
+ <iso_639_entry
+ iso_639_2B_code="gmh"
+ iso_639_2T_code="gmh"
+ name="German, Middle High (ca.1050-1500)" />
+ <iso_639_entry
+ iso_639_2B_code="goh"
+ iso_639_2T_code="goh"
+ name="German, Old High (ca.750-1050)" />
+ <iso_639_entry
+ iso_639_2B_code="gon"
+ iso_639_2T_code="gon"
+ name="Gondi" />
+ <iso_639_entry
+ iso_639_2B_code="gor"
+ iso_639_2T_code="gor"
+ name="Gorontalo" />
+ <iso_639_entry
+ iso_639_2B_code="got"
+ iso_639_2T_code="got"
+ name="Gothic" />
+ <iso_639_entry
+ iso_639_2B_code="grb"
+ iso_639_2T_code="grb"
+ name="Grebo" />
+ <iso_639_entry
+ iso_639_2B_code="grc"
+ iso_639_2T_code="grc"
+ name="Greek, Ancient (to 1453)" />
+ <iso_639_entry
+ iso_639_2B_code="gre"
+ iso_639_2T_code="ell"
+ iso_639_1_code="el"
+ name="Greek, Modern (1453-)" />
+ <iso_639_entry
+ iso_639_2B_code="grn"
+ iso_639_2T_code="grn"
+ iso_639_1_code="gn"
+ name="Guarani" />
+ <iso_639_entry
+ iso_639_2B_code="guj"
+ iso_639_2T_code="guj"
+ iso_639_1_code="gu"
+ name="Gujarati" />
+ <iso_639_entry
+ iso_639_2B_code="gwi"
+ iso_639_2T_code="gwi"
+ name="Gwichin" />
+ <iso_639_entry
+ iso_639_2B_code="hai"
+ iso_639_2T_code="hai"
+ name="Haida" />
+ <iso_639_entry
+ iso_639_2B_code="hat"
+ iso_639_2T_code="hat"
+ iso_639_1_code="ht"
+ name="Haitian; Haitian Creole" />
+ <iso_639_entry
+ iso_639_2B_code="hau"
+ iso_639_2T_code="hau"
+ iso_639_1_code="ha"
+ name="Hausa" />
+ <iso_639_entry
+ iso_639_2B_code="haw"
+ iso_639_2T_code="haw"
+ name="Hawaiian" />
+ <iso_639_entry
+ iso_639_2B_code="heb"
+ iso_639_2T_code="heb"
+ iso_639_1_code="he"
+ name="Hebrew" />
+ <iso_639_entry
+ iso_639_2B_code="her"
+ iso_639_2T_code="her"
+ iso_639_1_code="hz"
+ name="Herero" />
+ <iso_639_entry
+ iso_639_2B_code="hil"
+ iso_639_2T_code="hil"
+ name="Hiligaynon" />
+ <iso_639_entry
+ iso_639_2B_code="him"
+ iso_639_2T_code="him"
+ name="Himachali" />
+ <iso_639_entry
+ iso_639_2B_code="hin"
+ iso_639_2T_code="hin"
+ iso_639_1_code="hi"
+ name="Hindi" />
+ <iso_639_entry
+ iso_639_2B_code="hit"
+ iso_639_2T_code="hit"
+ name="Hittite" />
+ <iso_639_entry
+ iso_639_2B_code="hmn"
+ iso_639_2T_code="hmn"
+ name="Hmong" />
+ <iso_639_entry
+ iso_639_2B_code="hmo"
+ iso_639_2T_code="hmo"
+ iso_639_1_code="ho"
+ name="Hiri" />
+ <iso_639_entry
+ iso_639_2B_code="hsb"
+ iso_639_2T_code="hsb"
+ name="Upper Sorbian" />
+ <iso_639_entry
+ iso_639_2B_code="hun"
+ iso_639_2T_code="hun"
+ iso_639_1_code="hu"
+ name="Hungarian" />
+ <iso_639_entry
+ iso_639_2B_code="hup"
+ iso_639_2T_code="hup"
+ name="Hupa" />
+ <iso_639_entry
+ iso_639_2B_code="iba"
+ iso_639_2T_code="iba"
+ name="Iban" />
+ <iso_639_entry
+ iso_639_2B_code="ibo"
+ iso_639_2T_code="ibo"
+ iso_639_1_code="ig"
+ name="Igbo" />
+ <iso_639_entry
+ iso_639_2B_code="ice"
+ iso_639_2T_code="isl"
+ iso_639_1_code="is"
+ name="Icelandic" />
+ <iso_639_entry
+ iso_639_2B_code="ido"
+ iso_639_2T_code="ido"
+ iso_639_1_code="io"
+ name="Ido" />
+ <iso_639_entry
+ iso_639_2B_code="iii"
+ iso_639_2T_code="iii"
+ iso_639_1_code="ii"
+ name="Sichuan Yi" />
+ <iso_639_entry
+ iso_639_2B_code="ijo"
+ iso_639_2T_code="ijo"
+ name="Ijo" />
+ <iso_639_entry
+ iso_639_2B_code="iku"
+ iso_639_2T_code="iku"
+ iso_639_1_code="iu"
+ name="Inuktitut" />
+ <iso_639_entry
+ iso_639_2B_code="ile"
+ iso_639_2T_code="ile"
+ iso_639_1_code="ie"
+ name="Interlingue" />
+ <iso_639_entry
+ iso_639_2B_code="ilo"
+ iso_639_2T_code="ilo"
+ name="Iloko" />
+ <iso_639_entry
+ iso_639_2B_code="ina"
+ iso_639_2T_code="ina"
+ iso_639_1_code="ia"
+ name="Interlingua" />
+ <iso_639_entry
+ iso_639_2B_code="inc"
+ iso_639_2T_code="inc"
+ name="Indic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="ind"
+ iso_639_2T_code="ind"
+ iso_639_1_code="id"
+ name="Indonesian" />
+ <iso_639_entry
+ iso_639_2B_code="ine"
+ iso_639_2T_code="ine"
+ name="Indo-European (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="inh"
+ iso_639_2T_code="inh"
+ name="Ingush" />
+ <iso_639_entry
+ iso_639_2B_code="ipk"
+ iso_639_2T_code="ipk"
+ iso_639_1_code="ik"
+ name="Inupiaq" />
+ <iso_639_entry
+ iso_639_2B_code="ira"
+ iso_639_2T_code="ira"
+ name="Iranian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="iro"
+ iso_639_2T_code="iro"
+ name="Iroquoian languages" />
+ <iso_639_entry
+ iso_639_2B_code="ita"
+ iso_639_2T_code="ita"
+ iso_639_1_code="it"
+ name="Italian" />
+ <iso_639_entry
+ iso_639_2B_code="jav"
+ iso_639_2T_code="jav"
+ iso_639_1_code="jv"
+ name="Javanese" />
+ <iso_639_entry
+ iso_639_2B_code="jbo"
+ iso_639_2T_code="jbo"
+ name="Lojban" />
+ <iso_639_entry
+ iso_639_2B_code="jpn"
+ iso_639_2T_code="jpn"
+ iso_639_1_code="ja"
+ name="Japanese" />
+ <iso_639_entry
+ iso_639_2B_code="jpr"
+ iso_639_2T_code="jpr"
+ name="Judeo-Persian" />
+ <iso_639_entry
+ iso_639_2B_code="jrb"
+ iso_639_2T_code="jrb"
+ name="Judeo-Arabic" />
+ <iso_639_entry
+ iso_639_2B_code="kaa"
+ iso_639_2T_code="kaa"
+ name="Kara-Kalpak" />
+ <iso_639_entry
+ iso_639_2B_code="kab"
+ iso_639_2T_code="kab"
+ name="Kabyle" />
+ <iso_639_entry
+ iso_639_2B_code="kac"
+ iso_639_2T_code="kac"
+ name="Kachin" />
+ <iso_639_entry
+ iso_639_2B_code="kal"
+ iso_639_2T_code="kal"
+ iso_639_1_code="kl"
+ name="Greenlandic (Kalaallisut)" />
+ <iso_639_entry
+ iso_639_2B_code="kam"
+ iso_639_2T_code="kam"
+ name="Kamba" />
+ <iso_639_entry
+ iso_639_2B_code="kan"
+ iso_639_2T_code="kan"
+ iso_639_1_code="kn"
+ name="Kannada" />
+ <iso_639_entry
+ iso_639_2B_code="kar"
+ iso_639_2T_code="kar"
+ name="Karen" />
+ <iso_639_entry
+ iso_639_2B_code="kas"
+ iso_639_2T_code="kas"
+ iso_639_1_code="ks"
+ name="Kashmiri" />
+ <iso_639_entry
+ iso_639_2B_code="kau"
+ iso_639_2T_code="kau"
+ iso_639_1_code="kr"
+ name="Kanuri" />
+ <iso_639_entry
+ iso_639_2B_code="kaw"
+ iso_639_2T_code="kaw"
+ name="Kawi" />
+ <iso_639_entry
+ iso_639_2B_code="kaz"
+ iso_639_2T_code="kaz"
+ iso_639_1_code="kk"
+ name="Kazakh" />
+ <iso_639_entry
+ iso_639_2B_code="kbd"
+ iso_639_2T_code="kbd"
+ name="Kabardian" />
+ <iso_639_entry
+ iso_639_2B_code="kha"
+ iso_639_2T_code="kha"
+ name="Khazi" />
+ <iso_639_entry
+ iso_639_2B_code="khi"
+ iso_639_2T_code="khi"
+ name="Khoisan (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="khm"
+ iso_639_2T_code="khm"
+ iso_639_1_code="km"
+ name="Khmer" />
+ <iso_639_entry
+ iso_639_2B_code="kho"
+ iso_639_2T_code="kho"
+ name="Khotanese" />
+ <iso_639_entry
+ iso_639_2B_code="kik"
+ iso_639_2T_code="kik"
+ iso_639_1_code="ki"
+ name="Kikuyu" />
+ <iso_639_entry
+ iso_639_2B_code="kin"
+ iso_639_2T_code="kin"
+ iso_639_1_code="rw"
+ name="Kinyarwanda" />
+ <iso_639_entry
+ iso_639_2B_code="kir"
+ iso_639_2T_code="kir"
+ iso_639_1_code="ky"
+ name="Kirghiz" />
+ <iso_639_entry
+ iso_639_2B_code="kmb"
+ iso_639_2T_code="kmb"
+ name="Kimbundu" />
+ <iso_639_entry
+ iso_639_2B_code="kok"
+ iso_639_2T_code="kok"
+ name="Konkani" />
+ <iso_639_entry
+ iso_639_2B_code="kom"
+ iso_639_2T_code="kom"
+ iso_639_1_code="kv"
+ name="Komi" />
+ <iso_639_entry
+ iso_639_2B_code="kon"
+ iso_639_2T_code="kon"
+ iso_639_1_code="kg"
+ name="Kongo" />
+ <iso_639_entry
+ iso_639_2B_code="kor"
+ iso_639_2T_code="kor"
+ iso_639_1_code="ko"
+ name="Korean" />
+ <iso_639_entry
+ iso_639_2B_code="kos"
+ iso_639_2T_code="kos"
+ name="Kosraean" />
+ <iso_639_entry
+ iso_639_2B_code="kpe"
+ iso_639_2T_code="kpe"
+ name="Kpelle" />
+ <iso_639_entry
+ iso_639_2B_code="krc"
+ iso_639_2T_code="krc"
+ name="Karachay-Balkar" />
+ <iso_639_entry
+ iso_639_2B_code="kro"
+ iso_639_2T_code="kro"
+ name="Kru" />
+ <iso_639_entry
+ iso_639_2B_code="kru"
+ iso_639_2T_code="kru"
+ name="Kurukh" />
+ <iso_639_entry
+ iso_639_2B_code="kua"
+ iso_639_2T_code="kua"
+ iso_639_1_code="kj"
+ name="Kuanyama" />
+ <iso_639_entry
+ iso_639_2B_code="kum"
+ iso_639_2T_code="kum"
+ name="Kumyk" />
+ <iso_639_entry
+ iso_639_2B_code="kur"
+ iso_639_2T_code="kur"
+ iso_639_1_code="ku"
+ name="Kurdish" />
+ <iso_639_entry
+ iso_639_2B_code="kut"
+ iso_639_2T_code="kut"
+ name="Kutenai" />
+ <iso_639_entry
+ iso_639_2B_code="lad"
+ iso_639_2T_code="lad"
+ name="Ladino" />
+ <iso_639_entry
+ iso_639_2B_code="lah"
+ iso_639_2T_code="lah"
+ name="Lahnda" />
+ <iso_639_entry
+ iso_639_2B_code="lam"
+ iso_639_2T_code="lam"
+ name="Lamba" />
+ <iso_639_entry
+ iso_639_2B_code="lao"
+ iso_639_2T_code="lao"
+ iso_639_1_code="lo"
+ name="Lao" />
+ <iso_639_entry
+ iso_639_2B_code="lat"
+ iso_639_2T_code="lat"
+ iso_639_1_code="la"
+ name="Latin" />
+ <iso_639_entry
+ iso_639_2B_code="lav"
+ iso_639_2T_code="lav"
+ iso_639_1_code="lv"
+ name="Latvian" />
+ <iso_639_entry
+ iso_639_2B_code="lez"
+ iso_639_2T_code="lez"
+ name="Lezghian" />
+ <iso_639_entry
+ iso_639_2B_code="lim"
+ iso_639_2T_code="lim"
+ iso_639_1_code="li"
+ name="Limburgian" />
+ <iso_639_entry
+ iso_639_2B_code="lin"
+ iso_639_2T_code="lin"
+ iso_639_1_code="ln"
+ name="Lingala" />
+ <iso_639_entry
+ iso_639_2B_code="lit"
+ iso_639_2T_code="lit"
+ iso_639_1_code="lt"
+ name="Lithuanian" />
+ <iso_639_entry
+ iso_639_2B_code="lol"
+ iso_639_2T_code="lol"
+ name="Mongo" />
+ <iso_639_entry
+ iso_639_2B_code="loz"
+ iso_639_2T_code="loz"
+ name="Lozi" />
+ <iso_639_entry
+ iso_639_2B_code="ltz"
+ iso_639_2T_code="ltz"
+ iso_639_1_code="lb"
+ name="Luxembourgish" />
+ <iso_639_entry
+ iso_639_2B_code="lua"
+ iso_639_2T_code="lua"
+ name="Luba-Lulua" />
+ <iso_639_entry
+ iso_639_2B_code="lub"
+ iso_639_2T_code="lub"
+ iso_639_1_code="lu"
+ name="Luba-Katanga" />
+ <iso_639_entry
+ iso_639_2B_code="lug"
+ iso_639_2T_code="lug"
+ iso_639_1_code="lg"
+ name="Ganda" />
+ <iso_639_entry
+ iso_639_2B_code="lui"
+ iso_639_2T_code="lui"
+ name="Luiseno" />
+ <iso_639_entry
+ iso_639_2B_code="lun"
+ iso_639_2T_code="lun"
+ name="Lunda" />
+ <iso_639_entry
+ iso_639_2B_code="luo"
+ iso_639_2T_code="luo"
+ name="Luo (Kenya and Tanzania)" />
+ <iso_639_entry
+ iso_639_2B_code="lus"
+ iso_639_2T_code="lus"
+ name="Lushai" />
+ <iso_639_entry
+ iso_639_2B_code="mac"
+ iso_639_2T_code="mkd"
+ iso_639_1_code="mk"
+ name="Macedonian" />
+ <iso_639_entry
+ iso_639_2B_code="mad"
+ iso_639_2T_code="mad"
+ name="Madurese" />
+ <iso_639_entry
+ iso_639_2B_code="mag"
+ iso_639_2T_code="mag"
+ name="Magahi" />
+ <iso_639_entry
+ iso_639_2B_code="mah"
+ iso_639_2T_code="mah"
+ iso_639_1_code="mh"
+ name="Marshallese" />
+ <iso_639_entry
+ iso_639_2B_code="mai"
+ iso_639_2T_code="mai"
+ name="Maithili" />
+ <iso_639_entry
+ iso_639_2B_code="mak"
+ iso_639_2T_code="mak"
+ name="Makasar" />
+ <iso_639_entry
+ iso_639_2B_code="mal"
+ iso_639_2T_code="mal"
+ iso_639_1_code="ml"
+ name="Malayalam" />
+ <iso_639_entry
+ iso_639_2B_code="man"
+ iso_639_2T_code="man"
+ name="Mandingo" />
+ <iso_639_entry
+ iso_639_2B_code="mao"
+ iso_639_2T_code="mri"
+ iso_639_1_code="mi"
+ name="Maori" />
+ <iso_639_entry
+ iso_639_2B_code="map"
+ iso_639_2T_code="map"
+ name="Austronesian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="mar"
+ iso_639_2T_code="mar"
+ iso_639_1_code="mr"
+ name="Marathi" />
+ <iso_639_entry
+ iso_639_2B_code="mas"
+ iso_639_2T_code="mas"
+ name="Masai" />
+ <iso_639_entry
+ iso_639_2B_code="may"
+ iso_639_2T_code="msa"
+ iso_639_1_code="ms"
+ name="Malay" />
+ <iso_639_entry
+ iso_639_2B_code="mdf"
+ iso_639_2T_code="mdf"
+ name="Moksha" />
+ <iso_639_entry
+ iso_639_2B_code="mdr"
+ iso_639_2T_code="mdr"
+ name="Mandar" />
+ <iso_639_entry
+ iso_639_2B_code="men"
+ iso_639_2T_code="men"
+ name="Mende" />
+ <iso_639_entry
+ iso_639_2B_code="mga"
+ iso_639_2T_code="mga"
+ name="Irish, Middle (900-1200)" />
+ <iso_639_entry
+ iso_639_2B_code="mic"
+ iso_639_2T_code="mic"
+ name="Mi'kmaq; Micmac" />
+ <iso_639_entry
+ iso_639_2B_code="min"
+ iso_639_2T_code="min"
+ name="Minangkabau" />
+ <iso_639_entry
+ iso_639_2B_code="mis"
+ iso_639_2T_code="mis"
+ name="Miscellaneous languages" />
+ <iso_639_entry
+ iso_639_2B_code="mkh"
+ iso_639_2T_code="mkh"
+ name="Mon-Khmer (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="mlg"
+ iso_639_2T_code="mlg"
+ iso_639_1_code="mg"
+ name="Malagasy" />
+ <iso_639_entry
+ iso_639_2B_code="mlt"
+ iso_639_2T_code="mlt"
+ iso_639_1_code="mt"
+ name="Maltese" />
+ <iso_639_entry
+ iso_639_2B_code="mnc"
+ iso_639_2T_code="mnc"
+ name="Manchu" />
+ <iso_639_entry
+ iso_639_2B_code="mno"
+ iso_639_2T_code="mno"
+ name="Manobo languages" />
+ <iso_639_entry
+ iso_639_2B_code="moh"
+ iso_639_2T_code="moh"
+ name="Mohawk" />
+ <iso_639_entry
+ iso_639_2B_code="mol"
+ iso_639_2T_code="mol"
+ iso_639_1_code="mo"
+ name="Moldavian" />
+ <iso_639_entry
+ iso_639_2B_code="mon"
+ iso_639_2T_code="mon"
+ iso_639_1_code="mn"
+ name="Mongolian" />
+ <iso_639_entry
+ iso_639_2B_code="mos"
+ iso_639_2T_code="mos"
+ name="Mossi" />
+ <iso_639_entry
+ iso_639_2B_code="mul"
+ iso_639_2T_code="mul"
+ name="Multiple languages" />
+ <iso_639_entry
+ iso_639_2B_code="mun"
+ iso_639_2T_code="mun"
+ name="Munda languages" />
+ <iso_639_entry
+ iso_639_2B_code="mus"
+ iso_639_2T_code="mus"
+ name="Creek" />
+ <iso_639_entry
+ iso_639_2B_code="mwl"
+ iso_639_2T_code="mwl"
+ name="Mirandese" />
+ <iso_639_entry
+ iso_639_2B_code="mwr"
+ iso_639_2T_code="mwr"
+ name="Marwari" />
+ <iso_639_entry
+ iso_639_2B_code="myn"
+ iso_639_2T_code="myn"
+ name="Mayan languages" />
+ <iso_639_entry
+ iso_639_2B_code="myv"
+ iso_639_2T_code="myv"
+ name="Erzya" />
+ <iso_639_entry
+ iso_639_2B_code="nah"
+ iso_639_2T_code="nah"
+ name="Nahuatl" />
+ <iso_639_entry
+ iso_639_2B_code="nai"
+ iso_639_2T_code="nai"
+ name="North American Indian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="nap"
+ iso_639_2T_code="nap"
+ name="Neapolitan" />
+ <iso_639_entry
+ iso_639_2B_code="nau"
+ iso_639_2T_code="nau"
+ iso_639_1_code="na"
+ name="Nauru" />
+ <iso_639_entry
+ iso_639_2B_code="nav"
+ iso_639_2T_code="nav"
+ iso_639_1_code="nv"
+ name="Navaho" />
+ <iso_639_entry
+ iso_639_2B_code="nbl"
+ iso_639_2T_code="nbl"
+ iso_639_1_code="nr"
+ name="Ndebele, South" />
+ <iso_639_entry
+ iso_639_2B_code="nde"
+ iso_639_2T_code="nde"
+ iso_639_1_code="nd"
+ name="Ndebele, North" />
+ <iso_639_entry
+ iso_639_2B_code="ndo"
+ iso_639_2T_code="ndo"
+ iso_639_1_code="ng"
+ name="Ndonga" />
+ <iso_639_entry
+ iso_639_2B_code="nds"
+ iso_639_2T_code="nds"
+ name="German, Low" />
+ <iso_639_entry
+ iso_639_2B_code="nep"
+ iso_639_2T_code="nep"
+ iso_639_1_code="ne"
+ name="Nepali" />
+ <iso_639_entry
+ iso_639_2B_code="new"
+ iso_639_2T_code="new"
+ name="Newari" />
+ <iso_639_entry
+ iso_639_2B_code="nia"
+ iso_639_2T_code="nia"
+ name="Nias" />
+ <iso_639_entry
+ iso_639_2B_code="nic"
+ iso_639_2T_code="nic"
+ name="Niger-Kordofanian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="niu"
+ iso_639_2T_code="niu"
+ name="Niuean" />
+ <iso_639_entry
+ iso_639_2B_code="nno"
+ iso_639_2T_code="nno"
+ iso_639_1_code="nn"
+ name="Norwegian Nynorsk" />
+ <iso_639_entry
+ iso_639_2B_code="nob"
+ iso_639_2T_code="nob"
+ iso_639_1_code="nb"
+ name="Bokmål, Norwegian" />
+ <iso_639_entry
+ iso_639_2B_code="nog"
+ iso_639_2T_code="nog"
+ name="Nogai" />
+ <iso_639_entry
+ iso_639_2B_code="non"
+ iso_639_2T_code="non"
+ name="Norse, Old" />
+ <iso_639_entry
+ iso_639_2B_code="nor"
+ iso_639_2T_code="nor"
+ iso_639_1_code="no"
+ name="Norwegian" />
+ <iso_639_entry
+ iso_639_2B_code="nso"
+ iso_639_2T_code="nso"
+ name="Northern Sotho; Pedi; Sepedi" />
+ <iso_639_entry
+ iso_639_2B_code="nub"
+ iso_639_2T_code="nub"
+ name="Nubian languages" />
+ <iso_639_entry
+ iso_639_2B_code="nym"
+ iso_639_2T_code="nym"
+ name="Nyamwezi" />
+ <iso_639_entry
+ iso_639_2B_code="nwc"
+ iso_639_2T_code="nwc"
+ name="Classical Newari; Old Newari" />
+ <iso_639_entry
+ iso_639_2B_code="nya"
+ iso_639_2T_code="nya"
+ iso_639_1_code="ny"
+ name="Chewa; Chichewa; Nyanja" />
+ <iso_639_entry
+ iso_639_2B_code="nyn"
+ iso_639_2T_code="nyn"
+ name="Nyankole" />
+ <iso_639_entry
+ iso_639_2B_code="nyo"
+ iso_639_2T_code="nyo"
+ name="Nyoro" />
+ <iso_639_entry
+ iso_639_2B_code="nzi"
+ iso_639_2T_code="nzi"
+ name="Nzima" />
+ <iso_639_entry
+ iso_639_2B_code="oci"
+ iso_639_2T_code="oci"
+ iso_639_1_code="oc"
+ name="Occitan (post 1500)" />
+ <iso_639_entry
+ iso_639_2B_code="oji"
+ iso_639_2T_code="oji"
+ iso_639_1_code="oj"
+ name="Ojibwa" />
+ <iso_639_entry
+ iso_639_2B_code="ori"
+ iso_639_2T_code="ori"
+ iso_639_1_code="or"
+ name="Oriya" />
+ <iso_639_entry
+ iso_639_2B_code="orm"
+ iso_639_2T_code="orm"
+ iso_639_1_code="om"
+ name="Oromo" />
+ <iso_639_entry
+ iso_639_2B_code="osa"
+ iso_639_2T_code="osa"
+ name="Osage" />
+ <iso_639_entry
+ iso_639_2B_code="oss"
+ iso_639_2T_code="oss"
+ iso_639_1_code="os"
+ name="Ossetian" />
+ <iso_639_entry
+ iso_639_2B_code="ota"
+ iso_639_2T_code="ota"
+ name="Turkish, Ottoman (1500-1928)" />
+ <iso_639_entry
+ iso_639_2B_code="oto"
+ iso_639_2T_code="oto"
+ name="Otomian languages" />
+ <iso_639_entry
+ iso_639_2B_code="paa"
+ iso_639_2T_code="paa"
+ name="Papuan (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="pag"
+ iso_639_2T_code="pag"
+ name="Pangasinan" />
+ <iso_639_entry
+ iso_639_2B_code="pal"
+ iso_639_2T_code="pal"
+ name="Pahlavi" />
+ <iso_639_entry
+ iso_639_2B_code="pam"
+ iso_639_2T_code="pam"
+ name="Pampanga" />
+ <iso_639_entry
+ iso_639_2B_code="pan"
+ iso_639_2T_code="pan"
+ iso_639_1_code="pa"
+ name="Punjabi" />
+ <iso_639_entry
+ iso_639_2B_code="pap"
+ iso_639_2T_code="pap"
+ name="Papiamento" />
+ <iso_639_entry
+ iso_639_2B_code="pau"
+ iso_639_2T_code="pau"
+ name="Palauan" />
+ <iso_639_entry
+ iso_639_2B_code="peo"
+ iso_639_2T_code="peo"
+ name="Persian, Old (ca.600-400 B.C.)" />
+ <iso_639_entry
+ iso_639_2B_code="per"
+ iso_639_2T_code="fas"
+ iso_639_1_code="fa"
+ name="Persian" />
+ <iso_639_entry
+ iso_639_2B_code="phi"
+ iso_639_2T_code="phi"
+ name="Philippine (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="phn"
+ iso_639_2T_code="phn"
+ name="Phoenician" />
+ <iso_639_entry
+ iso_639_2B_code="pli"
+ iso_639_2T_code="pli"
+ iso_639_1_code="pi"
+ name="Pali" />
+ <iso_639_entry
+ iso_639_2B_code="pol"
+ iso_639_2T_code="pol"
+ iso_639_1_code="pl"
+ name="Polish" />
+ <iso_639_entry
+ iso_639_2B_code="por"
+ iso_639_2T_code="por"
+ iso_639_1_code="pt"
+ name="Portuguese" />
+ <iso_639_entry
+ iso_639_2B_code="pon"
+ iso_639_2T_code="pon"
+ name="Pohnpeian" />
+ <iso_639_entry
+ iso_639_2B_code="pra"
+ iso_639_2T_code="pra"
+ name="Prakrit languages" />
+ <iso_639_entry
+ iso_639_2B_code="pro"
+ iso_639_2T_code="pro"
+ name="Provençal, Old (to 1500)" />
+ <iso_639_entry
+ iso_639_2B_code="pus"
+ iso_639_2T_code="pus"
+ iso_639_1_code="ps"
+ name="Pushto" />
+ <iso_639_entry
+ iso_639_2B_code="que"
+ iso_639_2T_code="que"
+ iso_639_1_code="qu"
+ name="Quechua" />
+ <iso_639_entry
+ iso_639_2B_code="raj"
+ iso_639_2T_code="raj"
+ name="Rajasthani" />
+ <iso_639_entry
+ iso_639_2B_code="rap"
+ iso_639_2T_code="rap"
+ name="Rapanui" />
+ <iso_639_entry
+ iso_639_2B_code="rar"
+ iso_639_2T_code="rar"
+ name="Rarotongan" />
+ <iso_639_entry
+ iso_639_2B_code="roa"
+ iso_639_2T_code="roa"
+ name="Romance (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="roh"
+ iso_639_2T_code="roh"
+ iso_639_1_code="rm"
+ name="Raeto-Romance" />
+ <iso_639_entry
+ iso_639_2B_code="rom"
+ iso_639_2T_code="rom"
+ name="Romany" />
+ <iso_639_entry
+ iso_639_2B_code="rum"
+ iso_639_2T_code="ron"
+ iso_639_1_code="ro"
+ name="Romanian" />
+ <iso_639_entry
+ iso_639_2B_code="run"
+ iso_639_2T_code="run"
+ iso_639_1_code="rn"
+ name="Rundi" />
+ <iso_639_entry
+ iso_639_2B_code="rus"
+ iso_639_2T_code="rus"
+ iso_639_1_code="ru"
+ name="Russian" />
+ <iso_639_entry
+ iso_639_2B_code="sad"
+ iso_639_2T_code="sad"
+ name="Sandawe" />
+ <iso_639_entry
+ iso_639_2B_code="sag"
+ iso_639_2T_code="sag"
+ iso_639_1_code="sg"
+ name="Sango" />
+ <iso_639_entry
+ iso_639_2B_code="sah"
+ iso_639_2T_code="sah"
+ name="Yakut" />
+ <iso_639_entry
+ iso_639_2B_code="sai"
+ iso_639_2T_code="sai"
+ name="South American Indian (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="sal"
+ iso_639_2T_code="sal"
+ name="Salishan languages" />
+ <iso_639_entry
+ iso_639_2B_code="sam"
+ iso_639_2T_code="sam"
+ name="Samaritan Aramaic" />
+ <iso_639_entry
+ iso_639_2B_code="san"
+ iso_639_2T_code="san"
+ iso_639_1_code="sa"
+ name="Sanskrit" />
+ <iso_639_entry
+ iso_639_2B_code="sas"
+ iso_639_2T_code="sas"
+ name="Sasak" />
+ <iso_639_entry
+ iso_639_2B_code="sat"
+ iso_639_2T_code="sat"
+ name="Santali" />
+ <iso_639_entry
+ iso_639_2B_code="scc"
+ iso_639_2T_code="srp"
+ iso_639_1_code="sr"
+ name="Serbian" />
+ <iso_639_entry
+ iso_639_2B_code="scn"
+ iso_639_2T_code="scn"
+ name="Sicilian" />
+ <iso_639_entry
+ iso_639_2B_code="sco"
+ iso_639_2T_code="sco"
+ name="Scots" />
+ <iso_639_entry
+ iso_639_2B_code="scr"
+ iso_639_2T_code="hrv"
+ iso_639_1_code="hr"
+ name="Croatian" />
+ <iso_639_entry
+ iso_639_2B_code="sel"
+ iso_639_2T_code="sel"
+ name="Selkup" />
+ <iso_639_entry
+ iso_639_2B_code="sem"
+ iso_639_2T_code="sem"
+ name="Semitic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="sga"
+ iso_639_2T_code="sga"
+ name="Irish, Old (to 900)" />
+ <iso_639_entry
+ iso_639_2B_code="sgn"
+ iso_639_2T_code="sgn"
+ name="Sign languages" />
+ <iso_639_entry
+ iso_639_2B_code="shn"
+ iso_639_2T_code="shn"
+ name="Shan" />
+ <iso_639_entry
+ iso_639_2B_code="sid"
+ iso_639_2T_code="sid"
+ name="Sidamo" />
+ <iso_639_entry
+ iso_639_2B_code="sin"
+ iso_639_2T_code="sin"
+ iso_639_1_code="si"
+ name="Sinhala; Sinhalese" />
+ <iso_639_entry
+ iso_639_2B_code="sio"
+ iso_639_2T_code="sio"
+ name="Siouan languages" />
+ <iso_639_entry
+ iso_639_2B_code="sit"
+ iso_639_2T_code="sit"
+ name="Sino-Tibetan (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="sla"
+ iso_639_2T_code="sla"
+ name="Slavic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="slo"
+ iso_639_2T_code="slk"
+ iso_639_1_code="sk"
+ name="Slovak" />
+ <iso_639_entry
+ iso_639_2B_code="slv"
+ iso_639_2T_code="slv"
+ iso_639_1_code="sl"
+ name="Slovenian" />
+ <iso_639_entry
+ iso_639_2B_code="sma"
+ iso_639_2T_code="sma"
+ name="Southern Sami" />
+ <iso_639_entry
+ iso_639_2B_code="sme"
+ iso_639_2T_code="sme"
+ iso_639_1_code="se"
+ name="Northern Sami" />
+ <iso_639_entry
+ iso_639_2B_code="smi"
+ iso_639_2T_code="smi"
+ name="Sami languages (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="smj"
+ iso_639_2T_code="smj"
+ name="Lule Sami" />
+ <iso_639_entry
+ iso_639_2B_code="smn"
+ iso_639_2T_code="smn"
+ name="Inari Sami" />
+ <iso_639_entry
+ iso_639_2B_code="smo"
+ iso_639_2T_code="smo"
+ iso_639_1_code="sm"
+ name="Samoan" />
+ <iso_639_entry
+ iso_639_2B_code="sms"
+ iso_639_2T_code="sms"
+ name="Skolt Sami" />
+ <iso_639_entry
+ iso_639_2B_code="sna"
+ iso_639_2T_code="sna"
+ iso_639_1_code="sn"
+ name="Shona" />
+ <iso_639_entry
+ iso_639_2B_code="snd"
+ iso_639_2T_code="snd"
+ iso_639_1_code="sd"
+ name="Sindhi" />
+ <iso_639_entry
+ iso_639_2B_code="snk"
+ iso_639_2T_code="snk"
+ name="Soninke" />
+ <iso_639_entry
+ iso_639_2B_code="sog"
+ iso_639_2T_code="sog"
+ name="Sogdian" />
+ <iso_639_entry
+ iso_639_2B_code="som"
+ iso_639_2T_code="som"
+ iso_639_1_code="so"
+ name="Somali" />
+ <iso_639_entry
+ iso_639_2B_code="son"
+ iso_639_2T_code="son"
+ name="Songhai" />
+ <iso_639_entry
+ iso_639_2B_code="sot"
+ iso_639_2T_code="sot"
+ iso_639_1_code="st"
+ name="Sotho, Southern" />
+ <iso_639_entry
+ iso_639_2B_code="spa"
+ iso_639_2T_code="spa"
+ iso_639_1_code="es"
+ name="Spanish" />
+ <iso_639_entry
+ iso_639_2B_code="srd"
+ iso_639_2T_code="srd"
+ iso_639_1_code="sc"
+ name="Sardinian" />
+ <iso_639_entry
+ iso_639_2B_code="srr"
+ iso_639_2T_code="srr"
+ name="Serer" />
+ <iso_639_entry
+ iso_639_2B_code="ssa"
+ iso_639_2T_code="ssa"
+ name="Nilo-Saharan (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="ssw"
+ iso_639_2T_code="ssw"
+ iso_639_1_code="ss"
+ name="Swati" />
+ <iso_639_entry
+ iso_639_2B_code="suk"
+ iso_639_2T_code="suk"
+ name="Sukuma" />
+ <iso_639_entry
+ iso_639_2B_code="sun"
+ iso_639_2T_code="sun"
+ iso_639_1_code="su"
+ name="Sundanese" />
+ <iso_639_entry
+ iso_639_2B_code="sus"
+ iso_639_2T_code="sus"
+ name="Susu" />
+ <iso_639_entry
+ iso_639_2B_code="sux"
+ iso_639_2T_code="sux"
+ name="Sumerian" />
+ <iso_639_entry
+ iso_639_2B_code="swa"
+ iso_639_2T_code="swa"
+ iso_639_1_code="sw"
+ name="Swahili" />
+ <iso_639_entry
+ iso_639_2B_code="swe"
+ iso_639_2T_code="swe"
+ iso_639_1_code="sv"
+ name="Swedish" />
+ <iso_639_entry
+ iso_639_2B_code="syr"
+ iso_639_2T_code="syr"
+ name="Syriac" />
+ <iso_639_entry
+ iso_639_2B_code="tah"
+ iso_639_2T_code="tah"
+ iso_639_1_code="ty"
+ name="Tahitian" />
+ <iso_639_entry
+ iso_639_2B_code="tai"
+ iso_639_2T_code="tai"
+ name="Tai (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="tam"
+ iso_639_2T_code="tam"
+ iso_639_1_code="ta"
+ name="Tamil" />
+ <iso_639_entry
+ iso_639_2B_code="tso"
+ iso_639_2T_code="tso"
+ iso_639_1_code="ts"
+ name="Tsonga" />
+ <iso_639_entry
+ iso_639_2B_code="tat"
+ iso_639_2T_code="tat"
+ iso_639_1_code="tt"
+ name="Tatar" />
+ <iso_639_entry
+ iso_639_2B_code="tel"
+ iso_639_2T_code="tel"
+ iso_639_1_code="te"
+ name="Telugu" />
+ <iso_639_entry
+ iso_639_2B_code="tem"
+ iso_639_2T_code="tem"
+ name="Timne" />
+ <iso_639_entry
+ iso_639_2B_code="ter"
+ iso_639_2T_code="ter"
+ name="Tereno" />
+ <iso_639_entry
+ iso_639_2B_code="tet"
+ iso_639_2T_code="tet"
+ name="Tetum" />
+ <iso_639_entry
+ iso_639_2B_code="tgk"
+ iso_639_2T_code="tgk"
+ iso_639_1_code="tg"
+ name="Tajik" />
+ <iso_639_entry
+ iso_639_2B_code="tgl"
+ iso_639_2T_code="tgl"
+ iso_639_1_code="tl"
+ name="Tagalog" />
+ <iso_639_entry
+ iso_639_2B_code="tha"
+ iso_639_2T_code="tha"
+ iso_639_1_code="th"
+ name="Thai" />
+ <iso_639_entry
+ iso_639_2B_code="tib"
+ iso_639_2T_code="bod"
+ iso_639_1_code="bo"
+ name="Tibetan" />
+ <iso_639_entry
+ iso_639_2B_code="tig"
+ iso_639_2T_code="tig"
+ name="Tigre" />
+ <iso_639_entry
+ iso_639_2B_code="tir"
+ iso_639_2T_code="tir"
+ iso_639_1_code="ti"
+ name="Tigrinya" />
+ <iso_639_entry
+ iso_639_2B_code="tiv"
+ iso_639_2T_code="tiv"
+ name="Tiv" />
+ <iso_639_entry
+ iso_639_2B_code="tlh"
+ iso_639_2T_code="tlh"
+ name="Klingon; tlhIngan-Hol" />
+ <iso_639_entry
+ iso_639_2B_code="tkl"
+ iso_639_2T_code="tkl"
+ name="Tokelau" />
+ <iso_639_entry
+ iso_639_2B_code="tli"
+ iso_639_2T_code="tli"
+ name="Tlinglit" />
+ <iso_639_entry
+ iso_639_2B_code="tmh"
+ iso_639_2T_code="tmh"
+ name="Tamashek" />
+ <iso_639_entry
+ iso_639_2B_code="tog"
+ iso_639_2T_code="tog"
+ name="Tonga (Nyasa)" />
+ <iso_639_entry
+ iso_639_2B_code="ton"
+ iso_639_2T_code="ton"
+ iso_639_1_code="to"
+ name="Tonga (Tonga Islands)" />
+ <iso_639_entry
+ iso_639_2B_code="tpi"
+ iso_639_2T_code="tpi"
+ name="Tok Pisin" />
+ <iso_639_entry
+ iso_639_2B_code="tsi"
+ iso_639_2T_code="tsi"
+ name="Tsimshian" />
+ <iso_639_entry
+ iso_639_2B_code="tsn"
+ iso_639_2T_code="tsn"
+ iso_639_1_code="tn"
+ name="Tswana" />
+ <iso_639_entry
+ iso_639_2B_code="tuk"
+ iso_639_2T_code="tuk"
+ iso_639_1_code="tk"
+ name="Turkmen" />
+ <iso_639_entry
+ iso_639_2B_code="tum"
+ iso_639_2T_code="tum"
+ name="Tumbuka" />
+ <iso_639_entry
+ iso_639_2B_code="tup"
+ iso_639_2T_code="tup"
+ name="Tupi languages" />
+ <iso_639_entry
+ iso_639_2B_code="tur"
+ iso_639_2T_code="tur"
+ iso_639_1_code="tr"
+ name="Turkish" />
+ <iso_639_entry
+ iso_639_2B_code="tut"
+ iso_639_2T_code="tut"
+ name="Altaic (Other)" />
+ <iso_639_entry
+ iso_639_2B_code="tvl"
+ iso_639_2T_code="tvl"
+ name="Tuvalu" />
+ <iso_639_entry
+ iso_639_2B_code="twi"
+ iso_639_2T_code="twi"
+ iso_639_1_code="tw"
+ name="Twi" />
+ <iso_639_entry
+ iso_639_2B_code="tyv"
+ iso_639_2T_code="tyv"
+ name="Tuvinian" />
+ <iso_639_entry
+ iso_639_2B_code="udm"
+ iso_639_2T_code="udm"
+ name="Udmurt" />
+ <iso_639_entry
+ iso_639_2B_code="uga"
+ iso_639_2T_code="uga"
+ name="Ugaritic" />
+ <iso_639_entry
+ iso_639_2B_code="uig"
+ iso_639_2T_code="uig"
+ iso_639_1_code="ug"
+ name="Uighur" />
+ <iso_639_entry
+ iso_639_2B_code="ukr"
+ iso_639_2T_code="ukr"
+ iso_639_1_code="uk"
+ name="Ukrainian" />
+ <iso_639_entry
+ iso_639_2B_code="umb"
+ iso_639_2T_code="umb"
+ name="Umbundu" />
+ <iso_639_entry
+ iso_639_2B_code="und"
+ iso_639_2T_code="und"
+ name="Undetermined" />
+ <iso_639_entry
+ iso_639_2B_code="urd"
+ iso_639_2T_code="urd"
+ ido_639_1_code="ur"
+ name="Urdu" />
+ <iso_639_entry
+ iso_639_2B_code="uzb"
+ iso_639_2T_code="uzb"
+ iso_639_1_code="uz"
+ name="Uzbek" />
+ <iso_639_entry
+ iso_639_2B_code="vai"
+ iso_639_2T_code="vai"
+ name="Vai" />
+ <iso_639_entry
+ iso_639_2B_code="ven"
+ iso_639_2T_code="ven"
+ iso_639_1_code="ve"
+ name="Venda" />
+ <iso_639_entry
+ iso_639_2B_code="vie"
+ iso_639_2T_code="vie"
+ iso_639_1_code="vi"
+ name="Vietnamese" />
+ <iso_639_entry
+ iso_639_2B_code="vol"
+ iso_639_2T_code="vol"
+ iso_639_1_code="vo"
+ name="Volapuk" />
+ <iso_639_entry
+ iso_639_2B_code="vot"
+ iso_639_2T_code="vot"
+ name="Votic" />
+ <iso_639_entry
+ iso_639_2B_code="wak"
+ iso_639_2T_code="wak"
+ name="Wakashan languages" />
+ <iso_639_entry
+ iso_639_2B_code="wal"
+ iso_639_2T_code="wal"
+ name="Walamo" />
+ <iso_639_entry
+ iso_639_2B_code="war"
+ iso_639_2T_code="war"
+ name="Waray" />
+ <iso_639_entry
+ iso_639_2B_code="was"
+ iso_639_2T_code="was"
+ name="Washo" />
+ <iso_639_entry
+ iso_639_2B_code="wel"
+ iso_639_2T_code="cym"
+ iso_639_1_code="cy"
+ name="Welsh" />
+ <iso_639_entry
+ iso_639_2B_code="wen"
+ iso_639_2T_code="wen"
+ name="Sorbian languages" />
+ <iso_639_entry
+ iso_639_2B_code="wln"
+ iso_639_2T_code="wln"
+ iso_639_1_code="wa"
+ name="Walloon" />
+ <iso_639_entry
+ iso_639_2B_code="wol"
+ iso_639_2T_code="wol"
+ iso_639_1_code="wo"
+ name="Wolof" />
+ <iso_639_entry
+ iso_639_2B_code="xal"
+ iso_639_2T_code="xal"
+ name="Kalmyk" />
+ <iso_639_entry
+ iso_639_2B_code="xho"
+ iso_639_2T_code="xho"
+ iso_639_1_code="xh"
+ name="Xhosa" />
+ <iso_639_entry
+ iso_639_2B_code="yao"
+ iso_639_2T_code="yao"
+ name="Yao" />
+ <iso_639_entry
+ iso_639_2B_code="yap"
+ iso_639_2T_code="yap"
+ name="Yapese" />
+ <iso_639_entry
+ iso_639_2B_code="yid"
+ iso_639_2T_code="yid"
+ iso_639_1_code="yi"
+ name="Yiddish" />
+ <iso_639_entry
+ iso_639_2B_code="yor"
+ iso_639_2T_code="yor"
+ iso_639_1_code="yo"
+ name="Yoruba" />
+ <iso_639_entry
+ iso_639_2B_code="ypk"
+ iso_639_2T_code="ypk"
+ name="Yupik languages" />
+ <iso_639_entry
+ iso_639_2B_code="zap"
+ iso_639_2T_code="zap"
+ name="Zapotec" />
+ <iso_639_entry
+ iso_639_2B_code="zen"
+ iso_639_2T_code="zen"
+ name="Zenaga" />
+ <iso_639_entry
+ iso_639_2B_code="zha"
+ iso_639_2T_code="zha"
+ iso_639_1_code="za"
+ name="Chuang; Zhuang" />
+ <iso_639_entry
+ iso_639_2B_code="znd"
+ iso_639_2T_code="znd"
+ name="Zande" />
+ <iso_639_entry
+ iso_639_2B_code="zul"
+ iso_639_2T_code="zul"
+ iso_639_1_code="zu"
+ name="Zulu" />
+ <iso_639_entry
+ iso_639_2B_code="zun"
+ iso_639_2T_code="zun"
+ name="Zuni" />
+</iso_639_entries>
diff --git a/muggle-plugin/scripts/languages.txt b/muggle-plugin/scripts/languages.txt
new file mode 100644
index 0000000..6378dcc
--- /dev/null
+++ b/muggle-plugin/scripts/languages.txt
@@ -0,0 +1,467 @@
+aar Afar
+abk Abkhazian
+ace Achinese
+ach Acoli
+ada Adangme
+ady Adyghe; Adygei
+afa Afro-Asiatic (Other)
+afh Afrihili
+afr Afrikaans
+aka Akan
+akk Akkadian
+alb Albanian
+amh Amharic
+ang English, Old (ca.450-1100)
+apa Apache languages
+ara Arabic
+arc Aramaic
+arg Aragonese
+arm Armenian
+arn Araucanian
+arp Arapaho
+art Artificial (Other)
+arw Arawak
+asm Assamese
+ast Asturian; Bable
+ath Athapascan language
+aus Australian languages
+ava Avaric
+ave Avestan
+awa Awadhi
+aym Aymara
+aze Azerbaijani
+bad Banda
+bai Bamileke languages
+bak Bashkir
+bal Baluchi
+bam Bambara
+ban Balinese
+baq Basque
+bas Basa
+bat Baltic (Other)
+bej Beja
+bel Belarusian
+bem Bemba
+ben Bengali
+ber Berber (Other)
+bho Bhojpuri
+bih Bihari
+bik Bikol
+bin Bini
+bis Bislama
+bla Siksika
+bnt Bantu (Other)
+bos Bosnian
+bra Braj
+bre Breton
+btk Batak (Indonesia)
+bua Buriat
+bug Buginese
+bul Bulgarian
+bur Burmese
+byn Blin; Bilin
+cad Caddo
+cai Central American Indian (Other)
+car Carib
+cat Catalan
+cau Caucasian (Other)
+ceb Cebuano
+cel Celtic (Other)
+cha Chamorro
+chb Chibcha
+che Chechen
+chg Chagatai
+chi Chinese
+chk Chukese
+chm Mari
+chn Chinook jargon
+cho Choctaw
+chp Chipewyan
+chr Cherokee
+chu Church Slavic
+chv Chuvash
+chy Cheyenne
+cmc Chamic languages
+cop Coptic
+cor Cornish
+cos Corsican
+cpe English-based (Other)
+cpf French-based (Other)
+cpp Portuguese-based (Other)
+cre Cree
+crh Crimean Turkish; Crimean Tatar
+crp Creoles and pidgins (Other)
+csb Kashubian
+cus Cushitic (Other)
+cze Czech
+dak Dakota
+dan Danish
+dar Dargwa
+del Delaware
+den Slave (Athapascan)
+dgr Dogrib
+din Dinka
+div Divehi
+doi Dogri
+dra Dravidian (Other)
+dsb Lower Sorbian
+dua Duala
+dum Dutch, Middle (ca. 1050-1350)
+dut Dutch
+dyu Dyula
+dzo Dzongkha
+efi Efik
+egy Egyptian (Ancient)
+eka Ekajuk
+elx Elamite
+eng English
+enm English, Middle (1100-1500)
+epo Esperanto
+est Estonian
+ewe Ewe
+ewo Ewondo
+fan Fang
+fao Faroese
+fat Fanti
+fij Fijian
+fil Filipino; Pilipino
+fin Finnish
+fiu Finno-Ugrian (Other)
+fon Fon
+fre French
+frm French, Middle (ca.1400-1600)
+fro French, Old (842-ca.1400)
+fry Frisian
+ful Fulah
+fur Friulian
+gaa Ga
+gay Gayo
+gba Gbaya
+gem Germanic (Other)
+geo Georgian
+ger German
+gez Geez
+gil Gilbertese
+gla Gaelic; Scottish
+gle Irish
+glg Gallegan
+glv Manx
+gmh German, Middle High (ca.1050-1500)
+goh German, Old High (ca.750-1050)
+gon Gondi
+gor Gorontalo
+got Gothic
+grb Grebo
+grc Greek, Ancient (to 1453)
+gre Greek, Modern (1453-)
+grn Guarani
+guj Gujarati
+gwi Gwichin
+hai Haida
+hat Haitian; Haitian Creole
+hau Hausa
+haw Hawaiian
+heb Hebrew
+her Herero
+hil Hiligaynon
+him Himachali
+hin Hindi
+hit Hittite
+hmn Hmong
+hmo Hiri
+hsb Upper Sorbian
+hun Hungarian
+hup Hupa
+iba Iban
+ibo Igbo
+ice Icelandic
+ido Ido
+iii Sichuan Yi
+ijo Ijo
+iku Inuktitut
+ile Interlingue
+ilo Iloko
+ina Interlingua
+inc Indic (Other)
+ind Indonesian
+ine Indo-European (Other)
+inh Ingush
+ipk Inupiaq
+ira Iranian (Other)
+iro Iroquoian languages
+ita Italian
+jav Javanese
+jbo Lojban
+jpn Japanese
+jpr Judeo-Persian
+jrb Judeo-Arabic
+kaa Kara-Kalpak
+kab Kabyle
+kac Kachin
+kal Greenlandic (Kalaallisut)
+kam Kamba
+kan Kannada
+kar Karen
+kas Kashmiri
+kau Kanuri
+kaw Kawi
+kaz Kazakh
+kbd Kabardian
+kha Khazi
+khi Khoisan (Other)
+khm Khmer
+kho Khotanese
+kik Kikuyu
+kin Kinyarwanda
+kir Kirghiz
+kmb Kimbundu
+kok Konkani
+kom Komi
+kon Kongo
+kor Korean
+kos Kosraean
+kpe Kpelle
+krc Karachay-Balkar
+kro Kru
+kru Kurukh
+kua Kuanyama
+kum Kumyk
+kur Kurdish
+kut Kutenai
+lad Ladino
+lah Lahnda
+lam Lamba
+lao Lao
+lat Latin
+lav Latvian
+lez Lezghian
+lim Limburgian
+lin Lingala
+lit Lithuanian
+lol Mongo
+loz Lozi
+ltz Luxembourgish
+lua Luba-Lulua
+lub Luba-Katanga
+lug Ganda
+lui Luiseno
+lun Lunda
+luo Luo (Kenya and Tanzania)
+lus Lushai
+mac Macedonian
+mad Madurese
+mag Magahi
+mah Marshallese
+mai Maithili
+mak Makasar
+mal Malayalam
+man Mandingo
+mao Maori
+map Austronesian (Other)
+mar Marathi
+mas Masai
+may Malay
+mdf Moksha
+mdr Mandar
+men Mende
+mga Irish, Middle (900-1200)
+mic Mi'kmaq; Micmac
+min Minangkabau
+mis Miscellaneous languages
+mkh Mon-Khmer (Other)
+mlg Malagasy
+mlt Maltese
+mnc Manchu
+mno Manobo languages
+moh Mohawk
+mol Moldavian
+mon Mongolian
+mos Mossi
+mul Multiple languages
+mun Munda languages
+mus Creek
+mwl Mirandese
+mwr Marwari
+myn Mayan languages
+myv Erzya
+nah Nahuatl
+nai North American Indian (Other)
+nap Neapolitan
+nau Nauru
+nav Navaho
+nbl Ndebele, South
+nde Ndebele, North
+ndo Ndonga
+nds German, Low
+nep Nepali
+new Newari
+nia Nias
+nic Niger-Kordofanian (Other)
+niu Niuean
+nno Norwegian Nynorsk
+nob Bokmål, Norwegian
+nog Nogai
+non Norse, Old
+nor Norwegian
+nso Northern Sotho; Pedi; Sepedi
+nub Nubian languages
+nym Nyamwezi
+nwc Classical Newari; Old Newari
+nya Chewa; Chichewa; Nyanja
+nyn Nyankole
+nyo Nyoro
+nzi Nzima
+oci Occitan (post 1500)
+oji Ojibwa
+ori Oriya
+orm Oromo
+osa Osage
+oss Ossetian
+ota Turkish, Ottoman (1500-1928)
+oto Otomian languages
+paa Papuan (Other)
+pag Pangasinan
+pal Pahlavi
+pam Pampanga
+pan Punjabi
+pap Papiamento
+pau Palauan
+peo Persian, Old (ca.600-400 B.C.)
+per Persian
+phi Philippine (Other)
+phn Phoenician
+pli Pali
+pol Polish
+por Portuguese
+pon Pohnpeian
+pra Prakrit languages
+pro Provençal, Old (to 1500)
+pus Pushto
+que Quechua
+raj Rajasthani
+rap Rapanui
+rar Rarotongan
+roa Romance (Other)
+roh Raeto-Romance
+rom Romany
+rum Romanian
+run Rundi
+rus Russian
+sad Sandawe
+sag Sango
+sah Yakut
+sai South American Indian (Other)
+sal Salishan languages
+sam Samaritan Aramaic
+san Sanskrit
+sas Sasak
+sat Santali
+scc Serbian
+scn Sicilian
+sco Scots
+scr Croatian
+sel Selkup
+sem Semitic (Other)
+sga Irish, Old (to 900)
+sgn Sign languages
+shn Shan
+sid Sidamo
+sin Sinhala; Sinhalese
+sio Siouan languages
+sit Sino-Tibetan (Other)
+sla Slavic (Other)
+slo Slovak
+slv Slovenian
+sma Southern Sami
+sme Northern Sami
+smi Sami languages (Other)
+smj Lule Sami
+smn Inari Sami
+smo Samoan
+sms Skolt Sami
+sna Shona
+snd Sindhi
+snk Soninke
+sog Sogdian
+som Somali
+son Songhai
+sot Sotho, Southern
+spa Spanish
+srd Sardinian
+srr Serer
+ssa Nilo-Saharan (Other)
+ssw Swati
+suk Sukuma
+sun Sundanese
+sus Susu
+sux Sumerian
+swa Swahili
+swe Swedish
+syr Syriac
+tah Tahitian
+tai Tai (Other)
+tam Tamil
+tso Tsonga
+tat Tatar
+tel Telugu
+tem Timne
+ter Tereno
+tet Tetum
+tgk Tajik
+tgl Tagalog
+tha Thai
+tib Tibetan
+tig Tigre
+tir Tigrinya
+tiv Tiv
+tlh Klingon; tlhIngan-Hol
+tkl Tokelau
+tli Tlinglit
+tmh Tamashek
+tog Tonga (Nyasa)
+ton Tonga (Tonga Islands)
+tpi Tok Pisin
+tsi Tsimshian
+tsn Tswana
+tuk Turkmen
+tum Tumbuka
+tup Tupi languages
+tur Turkish
+tut Altaic (Other)
+tvl Tuvalu
+twi Twi
+tyv Tuvinian
+udm Udmurt
+uga Ugaritic
+uig Uighur
+ukr Ukrainian
+umb Umbundu
+und Undetermined
+urd Urdu
+uzb Uzbek
+vai Vai
+ven Venda
+vie Vietnamese
+vol Volapuk
+vot Votic
+wak Wakashan languages
+wal Walamo
+war Waray
+was Washo
+wel Welsh
+wen Sorbian languages
+wln Walloon
+wol Wolof
+xal Kalmyk
+xho Xhosa
+yao Yao
+yap Yapese
+yid Yiddish
+yor Yoruba
+ypk Yupik languages
+zap Zapotec
+zen Zenaga
+zha Chuang; Zhuang
+znd Zande
+zul Zulu
+zun Zuni
diff --git a/muggle-plugin/scripts/make-empty-db b/muggle-plugin/scripts/make-empty-db
new file mode 100755
index 0000000..16c14c5
--- /dev/null
+++ b/muggle-plugin/scripts/make-empty-db
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+echo "THIS SCRIPT IS NO LONGER NEEDED."
+echo "SEE THE README"
diff --git a/muggle-plugin/scripts/musictypes.txt b/muggle-plugin/scripts/musictypes.txt
new file mode 100755
index 0000000..50ca2c3
--- /dev/null
+++ b/muggle-plugin/scripts/musictypes.txt
@@ -0,0 +1,4 @@
+soft/slow
+medium
+groovy
+hard
diff --git a/muggle-plugin/scripts/sources.txt b/muggle-plugin/scripts/sources.txt
new file mode 100755
index 0000000..b900632
--- /dev/null
+++ b/muggle-plugin/scripts/sources.txt
@@ -0,0 +1,6 @@
+cd
+radio
+vinyl
+tape
+tv
+video
diff --git a/muggle-plugin/stylesheet.css b/muggle-plugin/stylesheet.css
new file mode 100644
index 0000000..15ad718
--- /dev/null
+++ b/muggle-plugin/stylesheet.css
@@ -0,0 +1,115 @@
+body { background-color: white ;
+ font-family: arial, sans-serif;
+ font-size: 10pt;
+ color: black;
+ text-align: justify;
+ vertical-align: top;
+ margin-right: 10pt
+ }
+
+P { font-family: arial, sans-serif;
+ font-size: 10pt;
+ color: black;
+ text-align: justify;
+ vertical-align: top;
+ margin-right: 10pt
+ }
+
+P.header { font-family: arial,sans-serif;
+ font-size: 10pt;
+ color: black;
+ text-align: right;
+ margin-right: 10pt
+ }
+
+P.klein { font-family: arial,sans-serif;
+ font-size: 8pt;
+ color: black;
+ text-align: justify;
+ margin-right: 10pt
+ }
+
+P.kleinmitte { font-family: arial,sans-serif;
+ font-size: 8pt;
+ color: black;
+ text-align: center;
+ margin-right: 10pt
+ }
+
+P.kleinrechts { font-family: arial,sans-serif;
+ font-size: 8pt;
+ color: black;
+ text-align: right;
+ margin-right: 10pt
+ }
+
+P.kleinlinks { font-family: arial,sans-serif;
+ font-size: 8pt;
+ color: black;
+ text-align: left;
+ margin-right: 10pt
+ }
+
+P.bildtitel { font-family: arial,sans-serif;
+ font-size: 8pt;
+ font-weight: 900;
+ color: black;
+ text-align: left;
+ vertikal-align: top;
+ margin-left: 10pt
+ }
+
+P.bild { font-family: arial,sans-serif;
+ font-size: 8pt;
+ font-weight: 900;
+ color: black;
+ text-align: left;
+ vertikal-align: top;
+ margin-right: 10pt
+ }
+
+H1 { font-family: arial,sans-serif;
+ font-size: 20pt;
+ color: black;
+ text-align: left;
+ text-transform: uppercase;
+ letter-spacing: 10pt;
+ margin-right: 10pt
+ }
+
+H2 { font-family: arial,sans-serif;
+ font-size: 12pt;
+ color:black;
+ text-align: left;
+ margin-right: 10pt
+ }
+
+H3 { font-family: arial,sans-serif;
+ font-size: 10pt;
+ color:black;
+ text-align:left;
+ margin-right: 10pt;
+ }
+
+UL { font-family: arial,sans-serif;
+ font-size: 10pt;
+ color:black;
+ text-align: justify;
+ margin-right: 10pt
+ }
+DT { font-family: arial,sans-serif;
+ font-size: 10pt;
+ font-weight: bold;
+ color:black;
+ text-align: left;
+ }
+
+DD { font-family: arial,sans-serif;
+ font-size: 10pt;
+ color:black;
+ text-align: justify;
+ margin-right: 10pt;
+ }
+TD { vertical-align: top
+ }
+
diff --git a/muggle-plugin/vdr_actions.c b/muggle-plugin/vdr_actions.c
new file mode 100644
index 0000000..6f58da7
--- /dev/null
+++ b/muggle-plugin/vdr_actions.c
@@ -0,0 +1,1414 @@
+/*!
+ * \file vdr_actions.c
+ * \brief Implements all actions for browsing media libraries within VDR
+ *
+ * \version $Revision: 1.27 $ * \date $Date: 2004-12-25 16:52:35 +0100 (Sat, 25 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr61 $
+ *
+ * $Id: vdr_actions.c 276 2004-12-25 15:52:35Z wr61 $
+ */
+
+#include <stdio.h>
+#include <libintl.h>
+
+#include <typeinfo>
+#include <string>
+#include <vector>
+
+#include <menuitems.h>
+#include <tools.h>
+#include <plugin.h>
+
+#include "vdr_setup.h"
+#include "vdr_actions.h"
+#include "vdr_menu.h"
+#include "i18n.h"
+#include <vdr/interface.h>
+
+#define DEBUG
+#include "mg_tools.h"
+#include "mg_order.h"
+#include "mg_thread_sync.h"
+
+static bool
+IsEntry(mgActions i)
+{
+ return i == actEntry;
+}
+
+class mgOsdItem : public mgAction, public cOsdItem
+{
+ public:
+ eOSState ProcessKey(eKeys key) { return mgAction::ProcessKey(key); }
+};
+
+
+void
+mgAction::setHandle(unsigned int handle)
+{
+ m_handle = handle;
+}
+
+eOSState
+mgAction::ProcessKey(eKeys key)
+{
+ if (key!=kNone)
+ mgDebug(1,"mgAction::ProcessKey(%d)",key);
+ eOSState result = Process(key);
+ if (result != osUnknown)
+ IgnoreNextEvent = true;
+ return result;
+}
+
+class mgNone: public mgOsdItem
+{ public:
+ void Notify() {};
+ bool Enabled(mgActions on) { return false; }
+ eOSState Process(eKeys key) { return osUnknown; }
+};
+
+//! \brief used for normal data base items
+class mgEntry : public mgOsdItem
+{
+ public:
+ void Notify();
+ bool Enabled(mgActions on) { return IsEntry(on);}
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+ eOSState Process(eKeys key);
+ void Execute();
+ eOSState Back();
+};
+
+class mgKeyItem : public mgAction, public cMenuEditStraItem
+{
+ public:
+ mgKeyItem(const char *Name, int *Value, int NumStrings, const char *const *Strings) : cMenuEditStraItem(Name, Value, NumStrings, Strings) {}
+ eOSState ProcessKey(eKeys key) { return mgAction::ProcessKey(key); }
+ eOSState Process(eKeys key);
+};
+
+class mgBoolItem: public mgAction, public cMenuEditBoolItem
+{
+ public:
+ mgBoolItem(const char *Name,int *Value) : cMenuEditBoolItem(Name, Value) {}
+ eOSState ProcessKey(eKeys key) { return mgAction::ProcessKey(key); }
+ eOSState Process(eKeys key);
+};
+
+class mgDoCollEntry : public mgEntry
+{
+ public:
+ virtual eOSState Process(eKeys key);
+ protected:
+ string getTarget();
+};
+
+class mgAddCollEntry : public mgDoCollEntry
+{
+ public:
+ void Execute();
+};
+
+class mgRemoveCollEntry : public mgDoCollEntry
+{
+ public:
+ void Execute();
+};
+
+void
+mgAction::TryNotify()
+{
+ if (IgnoreNextEvent)
+ IgnoreNextEvent = false;
+ else
+ Notify();
+}
+
+eOSState
+mgDoCollEntry::Process(eKeys key)
+{
+ mgMenu *n = osd ()->newmenu;
+ osd ()->newmenu = NULL;
+ eOSState result = osContinue;
+ switch (key)
+ {
+ case kBack:
+ break;
+ case kOk:
+ Execute ();
+ break;
+ default:
+ osd ()->newmenu = n; // wrong key: stay in submenu
+ result = osUnknown;
+ break;
+ }
+ return result;
+}
+
+string
+mgDoCollEntry::getTarget()
+{
+ string result = cOsdItem::Text();
+ if (result[0]==' ')
+ result.erase(0,5);
+ else
+ result.erase(0,3);
+ string::size_type lparen = result.find(" [");
+ result.erase(lparen,string::npos);
+ return result;
+}
+
+void
+mgAddCollEntry::Execute()
+{
+ string target = getTarget();
+ osd()->default_collection = target;
+ if (target == osd()->play_collection)
+ if (!PlayerControl())
+ collselection()->ClearCollection(target);
+
+ osd()->Message1 ("Added %s entries",itos (osd()->moveselection->AddToCollection (target)));
+ osd()->CollectionChanged(target);
+}
+
+
+void
+mgRemoveCollEntry::Execute()
+{
+ string target = getTarget();
+ int removed = osd()->moveselection->RemoveFromCollection (target);
+ osd()->Message1 ("Removed %s entries",ltos(removed));
+ osd()->CollectionChanged(target);
+}
+
+
+void
+mgAction::Notify()
+{
+ m->SetHelpKeys(Type());
+}
+
+void
+mgAction::SetMenu(mgMenu *menu)
+{
+ m = menu;
+ m_osd = m->osd();
+}
+
+void
+mgAction::SetText(const char *text,bool copy)
+{
+ cOsdItem *c = dynamic_cast<cOsdItem*>(this);
+ if (!c)
+ mgError("mgAction::SetText() on wrong type");
+ c->SetText(text,copy);
+}
+
+const char *
+mgAction::Text()
+{
+ cOsdItem *c = dynamic_cast<cOsdItem*>(this);
+ if (!c)
+ mgError("mgAction::Text() on wrong type");
+ return c->Text();
+}
+
+
+bool
+mgAction::Enabled(mgActions on)
+{
+ return true;
+}
+
+mgAction::mgAction()
+{
+ m = 0;
+ m_osd = 0;
+ m_handle = 0;
+ IgnoreNextEvent = false;
+}
+
+mgAction::~mgAction()
+{
+}
+
+class mgCommand : public mgOsdItem
+{
+ public:
+ bool Enabled(mgActions on);
+ virtual eOSState Process(eKeys key);
+};
+
+class mgActOrder : public mgOsdItem
+{
+ public:
+ const char* MenuName(const unsigned int idx,const mgSelItem& item);
+ virtual eOSState Process(eKeys key);
+ void Execute();
+};
+
+const char*
+mgActOrder::MenuName(const unsigned int idx,const mgSelItem& item)
+{
+ return strdup(item.value().c_str());
+}
+
+eOSState
+mgActOrder::Process(eKeys key)
+{
+ mgMenu *n = osd ()->newmenu;
+ osd ()->newmenu = NULL;
+ eOSState result = osContinue;
+ switch (key)
+ {
+ case kBack:
+ break;
+ case kOk:
+ Execute ();
+ break;
+ default:
+ osd ()->newmenu = n; // wrong key: stay in submenu
+ result = osUnknown;
+ break;
+ }
+ return result;
+}
+
+void
+mgActOrder::Execute()
+{
+ mgSelection *s = osd()->selection();
+ mgOrder oldorder = s->getOrder();
+ mgContentItem o;
+ s->select();
+ if (s->getNumTracks()==1)
+ o = s->getTrack(0);
+ osd()->UseNormalSelection(); // Default for all orders
+ osd()->setOrder(s,osd()->Current());
+ mgSelection *newsel = osd()->selection();
+ newsel->selectfrom(oldorder,&o);
+ osd()->newposition = newsel->getPosition();
+ osd()->SaveState();
+}
+
+bool
+mgCommand::Enabled(mgActions on)
+{
+ return IsEntry(on);
+}
+
+mgSelection*
+mgAction::playselection ()
+{
+ return m->playselection ();
+}
+mgMainMenu*
+mgAction::osd ()
+{
+ return m_osd;
+}
+
+eOSState
+mgAction::Back()
+{
+ osd()->newmenu = NULL;
+ return osContinue;
+}
+
+
+void
+mgEntry::Notify()
+{
+ selection()->setPosition(m_handle);
+ selection()->gotoPosition();
+ osd()->SaveState();
+ mgAction::Notify(); // only after selection is updated
+}
+
+
+const char *
+mgEntry::MenuName(const unsigned int idx,const mgSelItem& item)
+{
+ char *result;
+ char ct[20];
+ ct[0]=0;
+ unsigned int selcount = item.count();
+ if (selection()->level()<selection()->ordersize()-1 || selcount>1)
+ sprintf(ct," [%u]",selcount);
+ // when changing this, also change mgDoCollEntry::getTarget()
+ if (selection()->isCollectionlist())
+ {
+ if (item.value() == osd()->default_collection)
+ asprintf(&result,"-> %s%s",item.value().c_str(),ct);
+ else
+ asprintf(&result," %s%s",item.value().c_str(),ct);
+ }
+ else if (selection()->inCollection())
+ asprintf(&result,"%4d %s%s",idx,item.value().c_str(),ct);
+ else if (selection()->isLanguagelist())
+ asprintf(&result,"%s%s",dgettext("iso_639",item.value().c_str()),ct);
+ else
+ asprintf(&result,"%s%s",item.value().c_str(),ct);
+ return result;
+}
+
+void
+mgEntry::Execute()
+{
+ if (selection ()->enter ())
+ {
+ osd()->forcerefresh = true;
+ }
+ else
+ {
+ m->ExecuteAction(actInstantPlay,Type());
+ }
+}
+
+eOSState
+mgEntry::Process(eKeys key)
+{
+ switch (key) {
+ case kOk:
+ Execute();
+ return osContinue;
+ case kBack:
+ return Back();
+ default:
+ return osUnknown;
+ }
+}
+
+
+eOSState
+mgEntry::Back()
+{
+ osd()->forcerefresh = true;
+ if (!selection ()->leave ())
+ osd()->newmenu = NULL;
+ return osContinue;
+}
+
+eOSState
+mgCommand::Process(eKeys key)
+{
+ mgMenu *parent = osd ()->Parent ();
+ mgMenu *n = osd ()->newmenu;
+ osd ()->newmenu = NULL;
+ eOSState result = osContinue;
+ switch (key)
+ {
+ case kRed:
+ if (osd()->UsingCollection)
+ parent->CollRedAction = Type();
+ else
+ parent->TreeRedAction = Type();
+ break;
+ case kGreen:
+ if (osd()->UsingCollection)
+ parent->CollGreenAction = Type();
+ else
+ parent->TreeGreenAction = Type();
+ break;
+ case kYellow:
+ if (osd()->UsingCollection)
+ parent->CollYellowAction = Type();
+ else
+ parent->TreeYellowAction = Type();
+ break;
+ case kBack:
+ break;
+ case kOk:
+ Execute ();
+ break;
+ default:
+ osd ()->newmenu = n; // wrong key: stay in submenu
+ result = osUnknown;
+ break;
+ }
+ return result;
+}
+
+
+class mgExternal : public mgCommand
+{
+ public:
+ const char *ButtonName();
+ void Execute();
+ bool Enabled(mgActions on) { return true; }
+ private:
+ cCommand * Command();
+};
+
+
+class mgExternal0 : public mgExternal { };
+class mgExternal1 : public mgExternal { };
+class mgExternal2 : public mgExternal { };
+class mgExternal3 : public mgExternal { };
+class mgExternal4 : public mgExternal { };
+class mgExternal5 : public mgExternal { };
+class mgExternal6 : public mgExternal { };
+class mgExternal7 : public mgExternal { };
+class mgExternal8 : public mgExternal { };
+class mgExternal9 : public mgExternal { };
+class mgExternal10 : public mgExternal { };
+class mgExternal11 : public mgExternal { };
+class mgExternal12 : public mgExternal { };
+class mgExternal13 : public mgExternal { };
+class mgExternal14 : public mgExternal { };
+class mgExternal15 : public mgExternal { };
+class mgExternal16 : public mgExternal { };
+class mgExternal17 : public mgExternal { };
+class mgExternal18 : public mgExternal { };
+class mgExternal19 : public mgExternal { };
+
+const char*
+mgExternal::ButtonName()
+{
+ cCommand *command = Command();
+ if (command)
+ {
+ return command->Title();
+ }
+ else
+ return "";
+}
+
+cCommand *
+mgExternal::Command()
+{
+ cCommand *command = NULL;
+ if (osd()->external_commands)
+ {
+ unsigned int idx = Type() - actExternal0;
+ command = osd()->external_commands->Get (idx);
+ }
+ return command;
+}
+
+void
+mgExternal::Execute()
+{
+ cCommand *command = Command();
+ if (command)
+ {
+ bool confirmed = true;
+ if (command->Confirm ())
+ {
+ char *buffer;
+ asprintf (&buffer, "%s?", command->Title ());
+ confirmed = Interface->Confirm (buffer);
+ free (buffer);
+ }
+ if (confirmed)
+ {
+ osd()->Message1 ("%s...", command->Title ());
+ selection ()->select ();
+ string m3u_file = selection ()->exportM3U ();
+ selection ()->leave ();
+ if (!m3u_file.empty ())
+ {
+ /*char *result = (char *)*/
+ string quoted = "'" + m3u_file + "'";
+ char prev[1000];
+ if (!getcwd(prev,1000))
+ mgError("current path too long");
+ if (chdir(the_setup.ToplevelDir))
+ mgError("cannnot change to directory %s",
+ the_setup.ToplevelDir);
+ command->Execute (quoted.c_str ());
+ chdir(prev);
+ selection()->clearCache();
+ osd()->forcerefresh = true; // the ext cmd could change the database
+/* What to do? Recode cMenuText (not much)?
+ if (result)
+ {
+ free( result );
+ return AddSubMenu( new cMenuText( command->Title(), result ) );
+ }
+*/
+ }
+ }
+ }
+}
+
+//! \brief select search order
+class mgChooseOrder : public mgCommand
+{
+ public:
+ bool Enabled(mgActions on=mgActions(0));
+ virtual eOSState Process(eKeys key);
+ void Execute ();
+ const char *ButtonName() { return tr("Order"); }
+ const char *MenuName(const unsigned int idx,const mgSelItem& item)
+ { return strdup(tr("Select an order")); }
+};
+
+bool
+mgChooseOrder::Enabled(mgActions on)
+{
+ bool result = !osd()->UsingCollection;
+ result &= IsEntry(on);
+ return result;
+}
+
+eOSState
+mgChooseOrder::Process(eKeys key)
+{
+ if (key == kOk)
+ {
+ osd()->CloseMenu();
+ Execute();
+ return osContinue;
+ }
+ else
+ return mgCommand::Process(key);
+}
+
+void mgChooseOrder::Execute()
+{
+ osd ()->newmenu = new mgMenuOrders;
+ osd ()->newposition = osd()->getCurrentOrder();
+
+}
+
+class mgEditOrder : public mgCommand
+{
+ public:
+ bool Enabled(mgActions on) { return true; }
+ eOSState Process(eKeys key);
+ void Execute () { osd ()->newmenu = new mgMenuOrder; }
+ const char *ButtonName() { return tr("Edit"); }
+};
+
+eOSState
+mgEditOrder::Process(eKeys key)
+{
+ if (key == kOk)
+ {
+ Execute();
+ return osContinue;
+ }
+ else
+ return mgCommand::Process(key);
+}
+
+class mgCreateOrder : public mgCommand
+{
+ public:
+ bool Enabled(mgActions on) { return true; }
+ void Execute ();
+ const char *ButtonName() { return tr("Create"); }
+};
+
+void
+mgCreateOrder::Execute()
+{
+ osd()->AddOrder();
+ osd()->SaveState();
+ osd()->forcerefresh = true;
+}
+
+class mgDeleteOrder : public mgCommand
+{
+ public:
+ bool Enabled(mgActions on) { return true; }
+ void Execute ();
+ const char *ButtonName() { return tr("Delete"); }
+};
+
+void
+mgDeleteOrder::Execute()
+{
+ osd()->DeleteOrder();
+ osd()->SaveState();
+ osd()->forcerefresh = true;
+ osd()->newposition = osd()->Current();
+}
+
+//! \brief show the normal selection list
+class mgShowList: public mgOsdItem
+{
+ public:
+ bool Enabled(mgActions) { return true; }
+ const char *ButtonName () { return tr("List"); }
+ void Execute() { osd()->newmenu=NULL; }
+};
+
+
+//! \brief show the command submenu
+class mgShowCommands: public mgOsdItem
+{
+ public:
+ bool Enabled(mgActions on) { return true; }
+ const char *ButtonName () { return tr("Commands"); }
+ void Execute() { osd()->newmenu = new mgSubmenu; }
+};
+
+
+//! \brief toggles between the normal and the collection selection
+class mgToggleSelection:public mgCommand
+{
+ public:
+ bool Enabled(mgActions on) { return true; }
+ void Execute ();
+ const char *ButtonName ();
+};
+
+const char *
+mgToggleSelection::ButtonName ()
+{
+ if (osd ()->UsingCollection)
+ return tr ("Browse");
+ else
+ return tr ("Collections");
+}
+
+void
+mgToggleSelection::Execute ()
+{
+ if (osd ()->UsingCollection)
+ osd ()->UseNormalSelection ();
+ else
+ {
+ osd ()->UseCollectionSelection ();
+ selection()->clearCache();
+ }
+ osd()->newposition = selection ()->gotoPosition ();
+}
+
+class mgCmdSync : public mgOsdItem
+{
+ public:
+ bool Enabled(mgActions on) { return true; }
+ void Execute();
+ eOSState ProcessKey(eKeys key);
+ const char *ButtonName() { return tr("Synchronize database"); }
+};
+
+
+char *sync_args[] =
+{
+ ".",
+ 0
+};
+
+eOSState
+mgCmdSync::ProcessKey(eKeys key)
+{
+ if (key==kOk)
+ if (Interface->Confirm(tr("Synchronize database with track files?")))
+ {
+ Execute();
+ return osContinue;
+ }
+ return osUnknown;
+}
+
+void
+mgCmdSync::Execute()
+{
+ mgThreadSync *s = mgThreadSync::get_instance();
+ if( s )
+ {
+ s->Sync( sync_args, (bool) the_setup.DeleteStaleReferences );
+ }
+}
+
+//! \brief sets the default collection selection
+class mgSetDefaultCollection:public mgCommand
+{
+ public:
+ bool Enabled(mgActions on);
+ void Execute ();
+ const char *ButtonName ()
+ {
+ return tr ("Default");
+ }
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+};
+
+const char * mgSetDefaultCollection::MenuName(const unsigned int idx,const mgSelItem& item)
+{
+ char *b;
+ asprintf (&b, tr("Set default to collection '%s'"),
+ selection ()->getCurrentValue().c_str());
+ return b;
+}
+
+
+bool
+mgSetDefaultCollection::Enabled(mgActions on)
+{
+ bool result = IsEntry(on);
+ result &= (!osd()->DefaultCollectionSelected());
+ result &= osd()->UsingCollection;
+ result &= (selection ()->level () == 0);
+ return result;
+}
+
+void
+mgSetDefaultCollection::Execute ()
+{
+ osd ()->default_collection = selection ()->getCurrentValue();
+ osd()->Message1 ("Default collection now is '%s'",
+ osd ()->default_collection);
+}
+
+
+class mgSetButton : public mgCommand
+{
+ public:
+ bool Enabled(mgActions on) { return true; }
+ const char *ButtonName() { return tr("Set"); }
+};
+
+
+//! \brief instant play
+class mgInstantPlay : public mgCommand {
+ public:
+ void Execute ();
+ const char *ButtonName () { return tr ("Instant play"); }
+};
+
+void
+mgInstantPlay::Execute()
+{
+ osd()->PlayInstant(true);
+}
+
+//! \brief add selected items to a collection
+class mgAddAllToCollection:public mgCommand {
+ public:
+ void Execute ();
+ //! \brief adds the whole selection to a collection
+ // \param collection the target collection. Default is the default collection
+ const char *ButtonName ()
+ {
+ return tr ("Add");
+ }
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+ protected:
+ void ExecuteMove();
+};
+
+const char *
+mgAddAllToCollection::MenuName (const unsigned int idx,const mgSelItem& item)
+{
+ return strdup(tr("Add all to a collection"));
+}
+
+void
+mgAddAllToCollection::Execute()
+{
+// work on a copy, so we don't have to clear the cache of selection()
+// which would result in an osd()->forcerefresh which could scroll.
+ osd()->moveselection = new mgSelection(selection());
+ ExecuteMove();
+}
+
+void
+mgAddAllToCollection::ExecuteMove()
+{
+ if (osd() ->Menus.size()>1)
+ osd ()->CloseMenu(); // TODO Gebastel...
+ char *b;
+ asprintf(&b,tr("'%s' to collection"),selection()->getCurrentValue().c_str());
+ osd ()->newmenu = new mgTreeAddToCollSelector(string(b));
+ osd ()->collselection()->leave_all();
+ osd ()->newposition = osd()->collselection()->getPosition();
+ free(b);
+}
+
+
+//! \brief add selected items to default collection
+class mgAddAllToDefaultCollection:public mgCommand {
+ public:
+ void Execute ();
+ //! \brief adds the whole selection to the default collection
+ // \param collection the default collection.
+ void ExecuteSelection (mgSelection *s);
+ const char *ButtonName ()
+ {
+ return tr ("Add");
+ }
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+};
+
+const char *
+mgAddAllToDefaultCollection::MenuName (const unsigned int idx,const mgSelItem& item)
+{
+ char *b;
+ asprintf (&b, tr ("Add all to '%s'"),
+ osd ()->default_collection.c_str ());
+ return b;
+}
+
+void
+mgAddAllToDefaultCollection::Execute()
+{
+ mgSelection *sel = new mgSelection(selection());
+ sel->select ();
+ ExecuteSelection(sel);
+ delete sel;
+}
+
+
+void
+mgAddAllToDefaultCollection::ExecuteSelection (mgSelection *s)
+{
+ string target = osd()->default_collection;
+ if (target == osd()->play_collection)
+ if (!PlayerControl())
+ collselection()->ClearCollection(target);
+
+ osd()->Message1 ("Added %s entries",itos (s->AddToCollection (target)));
+
+ if (target == osd()->play_collection)
+ {
+ playselection()->clearCache();
+ mgPlayerControl *c = PlayerControl();
+ if (c)
+ c->ReloadPlaylist();
+ else
+ osd()->PlayQueue();
+ }
+}
+
+//! \brief add selected items to a collection
+class mgAddThisToCollection:public mgAddAllToCollection
+{
+ public:
+ bool Enabled(mgActions on);
+ void Execute ();
+ const char *ButtonName ();
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+};
+
+
+void
+mgAddThisToCollection::Execute ()
+{
+// work on a copy, so we don't have to clear the cache of selection()
+// which would result in an osd()->forcerefresh which could scroll.
+ osd()->moveselection = new mgSelection(selection());
+ osd()->moveselection->select ();
+ mgAddAllToCollection::ExecuteMove();
+}
+
+const char *
+mgAddThisToCollection::ButtonName ()
+{
+ return tr("Add");
+}
+
+bool
+mgAddThisToCollection::Enabled(mgActions on)
+{
+ return IsEntry(on);
+}
+
+const char *
+mgAddThisToCollection::MenuName (const unsigned int idx,const mgSelItem& item)
+{
+ return strdup(tr("Add to a collection"));
+}
+
+//! \brief add selected items to default collection
+class mgAddThisToDefaultCollection:public mgAddAllToDefaultCollection
+{
+ public:
+ bool Enabled(mgActions on);
+ void Execute ();
+ const char *ButtonName ();
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+};
+
+
+void
+mgAddThisToDefaultCollection::Execute ()
+{
+// work on a copy, so we don't have to clear the cache of selection()
+// which would result in an osd()->forcerefresh which could scroll.
+ mgSelection *sel = new mgSelection(selection());
+ sel->select ();
+ mgAddAllToDefaultCollection::ExecuteSelection(sel);
+ delete sel;
+}
+
+const char *
+mgAddThisToDefaultCollection::ButtonName ()
+{
+ return tr("Add");
+}
+
+bool
+mgAddThisToDefaultCollection::Enabled(mgActions on)
+{
+ bool result = IsEntry(on);
+ result &= (!osd()->DefaultCollectionSelected());
+ return result;
+}
+
+const char *
+mgAddThisToDefaultCollection::MenuName (const unsigned int idx,const mgSelItem& item)
+{
+ char *b;
+ asprintf (&b, tr ("Add to '%s'"), osd ()->default_collection.c_str ());
+ return b;
+}
+
+//! \brief remove selected items from default collection
+class mgRemoveAllFromCollection:public mgCommand
+{
+ public:
+ void Execute ();
+ const char *ButtonName ()
+ {
+ return tr ("Remove");
+ }
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+};
+
+void
+mgRemoveAllFromCollection::Execute ()
+{
+ if (osd() ->Menus.size()>1)
+ osd ()->CloseMenu(); // TODO Gebastel...
+ char *b;
+ asprintf(&b,tr("Remove '%s' from collection"),osd()->moveselection->getListname().c_str());
+ osd ()->newmenu = new mgTreeRemoveFromCollSelector(string(b));
+ free(b);
+}
+
+const char *
+mgRemoveAllFromCollection::MenuName (const unsigned int idx,const mgSelItem& item)
+{
+ return strdup(tr("Remove all from a collection"));
+}
+
+class mgClearCollection : public mgCommand
+{
+ public:
+ bool Enabled(mgActions on);
+ void Execute ();
+ const char *ButtonName ()
+ {
+ return tr ("Clear");
+ }
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+};
+
+const char *
+mgClearCollection::MenuName (const unsigned int idx,const mgSelItem& item)
+{
+ return strdup(tr("Clear the collection"));
+}
+
+bool
+mgClearCollection::Enabled(mgActions on)
+{
+ return selection()->isCollectionlist();
+}
+
+void
+mgClearCollection::Execute()
+{
+ if (Interface->Confirm(tr("Clear the collection?")))
+ {
+ string target = selection()->getCurrentValue();
+ selection()->ClearCollection(target);
+ osd()->CollectionChanged(target);
+ }
+}
+
+//! \brief remove selected items from default collection
+class mgRemoveThisFromCollection:public mgRemoveAllFromCollection
+{
+ public:
+ void Execute ();
+ const char *ButtonName ()
+ {
+ return tr ("Remove");
+ }
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+};
+
+
+void
+mgRemoveThisFromCollection::Execute ()
+{
+// work on a copy, so we don't have to clear the cache of selection()
+// which would result in an osd()->forcerefresh which could scroll.
+ osd()->moveselection = new mgSelection(selection());
+ osd()->moveselection->select ();
+ mgRemoveAllFromCollection::Execute();
+}
+
+
+const char *
+mgRemoveThisFromCollection::MenuName (const unsigned int idx,const mgSelItem& item)
+{
+ return strdup(tr("Remove from a collection"));
+}
+
+class mgEditAction : public mgAction
+{
+ public:
+ mgEditAction() : mgAction() { memset(m_value,0,30); }
+ protected:
+ char m_value[30];
+};
+
+class mgCreate : public mgEditAction, public cMenuEditStrItem
+{
+ public:
+ mgCreate(const char* mn);
+ virtual bool Enabled(mgActions on)=0;
+ void Notify();
+ eOSState ProcessKey(eKeys key) { return mgAction::ProcessKey(key); }
+ eOSState Process(eKeys key);
+ protected:
+ bool Editing();
+};
+
+mgCreate::mgCreate(const char *mn) : mgEditAction(), cMenuEditStrItem(mn,m_value,30,tr(FileNameChars))
+{
+}
+
+bool
+mgCreate::Editing()
+{
+ return (strchr(cOsdItem::Text(),'[') && strchr(cOsdItem::Text(),']'));
+}
+
+void
+mgCreate::Notify()
+{
+ if (!Editing())
+ m->SetHelpKeys();
+}
+
+eOSState
+mgCreate::Process(eKeys key)
+{
+ if (key == kOk)
+ if (Editing())
+ Execute();
+ else
+ return cMenuEditStrItem::ProcessKey(kRight);
+ if (key != kYellow || Editing())
+ return cMenuEditStrItem::ProcessKey(key);
+ else
+ return osUnknown;
+}
+
+class mgCreateCollection : public mgCreate
+{
+ public:
+ mgCreateCollection();
+ bool Enabled(mgActions on);
+ void Execute ();
+ const char *MenuName (const unsigned int idx=0,const mgSelItem& item=zeroitem);
+};
+
+mgCreateCollection::mgCreateCollection() : mgCreate(MenuName())
+{
+}
+
+const char*
+mgCreateCollection::MenuName(const unsigned int idx,const mgSelItem& item)
+{
+ return strdup(tr ("Create collection"));
+}
+
+void
+mgCreateCollection::Execute ()
+{
+ string name = trim(m_value);
+ if (name.empty()) return;
+ bool created = selection ()->CreateCollection (name);
+ if (created)
+ {
+ mgDebug(1,"created collection %s",name.c_str());
+ osd()->default_collection = name;
+ selection ()->clearCache();
+ if (selection()->isCollectionlist())
+ {
+// selection ()->setPosition(selection()->id(keyCollection,name));
+ selection ()->setPosition(name);
+ }
+ osd()->forcerefresh = true;
+ }
+ else
+ osd()->Message1 ("Collection '%s' NOT created", name);
+}
+
+bool
+mgCreateCollection::Enabled(mgActions on)
+{
+ return selection()->isCollectionlist();
+}
+
+
+//! \brief delete collection
+class mgDeleteCollection:public mgCommand
+{
+ public:
+ void Execute ();
+ bool Enabled(mgActions on);
+ const char *ButtonName ()
+ {
+ return tr ("Delete");
+ }
+ const char *MenuName (const unsigned int idx,const mgSelItem& item);
+};
+
+bool
+mgDeleteCollection::Enabled(mgActions on)
+{
+ bool result = IsEntry(on);
+ result &= selection()->isCollectionlist();
+ if (result)
+ {
+ string name = selection ()->getCurrentValue();
+ result &= (name != osd()->play_collection);
+ }
+ return result;
+}
+
+const char* mgDeleteCollection::MenuName(const unsigned int idx,const mgSelItem& item)
+{
+ return strdup(tr("Delete the collection"));
+}
+
+
+void
+mgDeleteCollection::Execute ()
+{
+ if (!Interface->Confirm(tr("Delete the collection?"))) return;
+ string name = selection ()->getCurrentValue();
+ if (selection ()->DeleteCollection (name))
+ {
+ osd()->Message1 ("Collection '%s' deleted", name);
+ mgDebug(1,"Deleted collection %s",name.c_str());
+ selection ()->clearCache();
+ osd()->forcerefresh = true;
+ }
+ else
+ osd()->Message1 ("Collection '%s' NOT deleted", name);
+}
+
+
+//! \brief export track list for all selected items
+class mgExportTracklist:public mgCommand
+{
+ public:
+ void Execute ();
+ const char *ButtonName ()
+ {
+ return tr ("Export");
+ }
+ const char *MenuName (const unsigned int idx,const mgSelItem& item)
+ {
+ return strdup(tr ("Export track list"));
+ }
+};
+
+void
+mgExportTracklist::Execute ()
+{
+ selection ()->select ();
+ string m3u_file = selection ()->exportM3U ();
+ selection ()->leave ();
+ osd()->Message1 ("written to %s", m3u_file);
+}
+
+mgActions
+mgAction::Type()
+{
+ const type_info& t = typeid(*this);
+ if (t == typeid(mgNone)) return actNone;
+ if (t == typeid(mgChooseOrder)) return actChooseOrder;
+ if (t == typeid(mgToggleSelection)) return actToggleSelection;
+ if (t == typeid(mgClearCollection)) return actClearCollection;
+ if (t == typeid(mgCreateCollection)) return actCreateCollection;
+ if (t == typeid(mgInstantPlay)) return actInstantPlay;
+ if (t == typeid(mgAddAllToCollection)) return actAddAllToCollection;
+ if (t == typeid(mgAddAllToDefaultCollection)) return actAddAllToDefaultCollection;
+ if (t == typeid(mgRemoveAllFromCollection)) return actRemoveAllFromCollection;
+ if (t == typeid(mgDeleteCollection)) return actDeleteCollection;
+ if (t == typeid(mgExportTracklist)) return actExportTracklist;
+ if (t == typeid(mgAddCollEntry)) return actAddCollEntry;
+ if (t == typeid(mgRemoveCollEntry)) return actRemoveCollEntry;
+ if (t == typeid(mgAddThisToCollection)) return actAddThisToCollection;
+ if (t == typeid(mgAddThisToDefaultCollection)) return actAddThisToDefaultCollection;
+ if (t == typeid(mgRemoveThisFromCollection)) return actRemoveThisFromCollection;
+ if (t == typeid(mgEntry)) return actEntry;
+ if (t == typeid(mgSetButton)) return actSetButton;
+ if (t == typeid(mgShowList)) return actShowList;
+ if (t == typeid(mgShowCommands)) return actShowCommands;
+ if (t == typeid(mgSetDefaultCollection)) return actSetDefaultCollection;
+ if (t == typeid(mgActOrder)) return actOrder;
+ if (t == typeid(mgCreateOrder)) return actCreateOrder;
+ if (t == typeid(mgDeleteOrder)) return actDeleteOrder;
+ if (t == typeid(mgEditOrder)) return actEditOrder;
+ if (t == typeid(mgCmdSync)) return actSync;
+ if (t == typeid(mgExternal0)) return actExternal0;
+ if (t == typeid(mgExternal1)) return actExternal1;
+ if (t == typeid(mgExternal2)) return actExternal2;
+ if (t == typeid(mgExternal3)) return actExternal3;
+ if (t == typeid(mgExternal4)) return actExternal4;
+ if (t == typeid(mgExternal5)) return actExternal5;
+ if (t == typeid(mgExternal6)) return actExternal6;
+ if (t == typeid(mgExternal7)) return actExternal7;
+ if (t == typeid(mgExternal8)) return actExternal8;
+ if (t == typeid(mgExternal9)) return actExternal9;
+ if (t == typeid(mgExternal10)) return actExternal10;
+ if (t == typeid(mgExternal11)) return actExternal11;
+ if (t == typeid(mgExternal12)) return actExternal12;
+ if (t == typeid(mgExternal13)) return actExternal13;
+ if (t == typeid(mgExternal14)) return actExternal14;
+ if (t == typeid(mgExternal15)) return actExternal15;
+ if (t == typeid(mgExternal16)) return actExternal16;
+ if (t == typeid(mgExternal17)) return actExternal17;
+ if (t == typeid(mgExternal18)) return actExternal18;
+ if (t == typeid(mgExternal19)) return actExternal19;
+ return mgActions(0);
+}
+
+mgAction*
+actGenerateKeyItem(const char *Name, int *Value, int NumStrings, const char * const * Strings)
+{
+ return new mgKeyItem(Name,Value,NumStrings,Strings);
+}
+
+mgAction*
+actGenerateBoolItem(const char *Name, int *Value)
+{
+ return new mgBoolItem(Name,Value);
+}
+
+mgAction*
+actGenerate(const mgActions action)
+{
+ mgAction * result = NULL;
+ switch (action)
+ {
+ case actNone: result = new mgNone;break;
+ case actChooseOrder: result = new mgChooseOrder;break;
+ case actToggleSelection: result = new mgToggleSelection;break;
+ case actClearCollection: result = new mgClearCollection;break;
+ case actCreateCollection: result = new mgCreateCollection;break;
+ case actInstantPlay: result = new mgInstantPlay;break;
+ case actAddAllToCollection: result = new mgAddAllToCollection;break;
+ case actAddAllToDefaultCollection: result = new mgAddAllToDefaultCollection;break;
+ case actRemoveAllFromCollection:result = new mgRemoveAllFromCollection;break;
+ case actDeleteCollection: result = new mgDeleteCollection;break;
+ case actExportTracklist: result = new mgExportTracklist;break;
+ case actAddCollEntry: result = new mgAddCollEntry;break;
+ case actRemoveCollEntry: result = new mgRemoveCollEntry;break;
+ case actAddThisToCollection: result = new mgAddThisToCollection;break;
+ case actAddThisToDefaultCollection: result = new mgAddThisToDefaultCollection;break;
+ case actRemoveThisFromCollection: result = new mgRemoveThisFromCollection;break;
+ case actEntry: result = new mgEntry;break;
+ case actSetButton: result = new mgSetButton;break;
+ case actShowList: result = new mgShowList;break;
+ case actShowCommands: result = new mgShowCommands;break;
+ case actSync: result = new mgCmdSync;break;
+ case actSetDefaultCollection: result = new mgSetDefaultCollection;break;
+ case actOrder: result = new mgActOrder;break;
+ case actUnused6: break;
+ case actCreateOrder: result = new mgCreateOrder;break;
+ case actDeleteOrder: result = new mgDeleteOrder;break;
+ case actEditOrder: result = new mgEditOrder;break;
+ case actExternal0: result = new mgExternal0;break;
+ case actExternal1: result = new mgExternal1;break;
+ case actExternal2: result = new mgExternal2;break;
+ case actExternal3: result = new mgExternal3;break;
+ case actExternal4: result = new mgExternal4;break;
+ case actExternal5: result = new mgExternal5;break;
+ case actExternal6: result = new mgExternal6;break;
+ case actExternal7: result = new mgExternal7;break;
+ case actExternal8: result = new mgExternal8;break;
+ case actExternal9: result = new mgExternal9;break;
+ case actExternal10: result = new mgExternal10;break;
+ case actExternal11: result = new mgExternal11;break;
+ case actExternal12: result = new mgExternal12;break;
+ case actExternal13: result = new mgExternal13;break;
+ case actExternal14: result = new mgExternal14;break;
+ case actExternal15: result = new mgExternal15;break;
+ case actExternal16: result = new mgExternal16;break;
+ case actExternal17: result = new mgExternal17;break;
+ case actExternal18: result = new mgExternal18;break;
+ case actExternal19: result = new mgExternal19;break;
+ }
+ return result;
+}
+
+
+mgSelection *
+mgAction::selection()
+{
+ return osd()->selection();
+}
+
+
+mgSelection *
+mgAction::collselection()
+{
+ return osd()->collselection();
+}
+
+eOSState
+mgKeyItem::Process(eKeys key)
+{
+ mgMenuOrder *menu = dynamic_cast<mgMenuOrder*>(m);
+ if (key==kOk)
+ {
+ if (menu->ChangeOrder(key))
+ return osContinue;
+ else
+ {
+ menu->SaveOrder();
+ osd ()->newmenu = NULL;
+ return osContinue;
+ }
+ } else if (key==kBack)
+ {
+ osd ()->newmenu = NULL;
+ return osContinue;
+ }
+ if (key==kUp || key==kDown)
+ if (menu->ChangeOrder(key))
+ return osContinue;
+ return cMenuEditStraItem::ProcessKey(key);
+}
+
+
+eOSState
+mgBoolItem::Process(eKeys key)
+{
+ mgMenuOrder *menu = dynamic_cast<mgMenuOrder*>(m);
+ if (key==kOk)
+ {
+ if (menu->ChangeOrder(key))
+ return osContinue;
+ else
+ {
+ menu->SaveOrder();
+ osd ()->newmenu = NULL;
+ return osContinue;
+ }
+ } else if (key==kBack)
+ {
+ osd ()->newmenu = NULL;
+ return osContinue;
+ }
+ if (key==kUp || key==kDown)
+ if (menu->ChangeOrder(key))
+ return osContinue;
+ return cMenuEditBoolItem::ProcessKey(key);
+}
+
diff --git a/muggle-plugin/vdr_actions.h b/muggle-plugin/vdr_actions.h
new file mode 100644
index 0000000..72f9caf
--- /dev/null
+++ b/muggle-plugin/vdr_actions.h
@@ -0,0 +1,188 @@
+/*!
+ * \file vdr_actions.h
+ * \brief Implements all actions for broswing media libraries within VDR
+ *
+ * \version $Revision: 1.13 $
+ * \date $Date: 2004-12-25 16:52:35 +0100 (Sat, 25 Dec 2004) $
+ * \author Wolfgang Rohdewald
+ * \author Responsible author: $Author: wr61 $
+ *
+ * $Id: vdr_actions.h 276 2004-12-25 15:52:35Z wr61 $
+ */
+
+#ifndef _VDR_ACTIONS_H
+#define _VDR_ACTIONS_H
+
+#include <string>
+
+#include <osd.h>
+#include <plugin.h>
+
+#include "mg_order.h"
+
+using namespace std;
+
+class mgSelection;
+class mgMenu;
+class mgMainMenu;
+
+/*! \brief defines all actions which can appear in command submenus.
+ * Since these values are saved in muggle.state, new actions should
+ * always be appended. The order does not matter. actNone should be 0.
+ */
+enum mgActions {
+ actNone,
+ actChooseOrder, //!< show a menu with all possible orders
+ actToggleSelection, //!< toggle between search and collection view
+ actClearCollection, //!< clear a collection,
+ actCreateCollection,
+ actInstantPlay, //!< instant play
+ actAddAllToCollection, //!< add all items of OSD list to default collection
+ actRemoveAllFromCollection,//!< remove from default collection
+ actDeleteCollection, //!< delete collection
+ actExportTracklist, //!< export track list into a *.m3u file
+ actAddCollEntry,
+ actRemoveCollEntry,
+ actAddThisToCollection, //!< add selected item to default collection
+ actRemoveThisFromCollection, //!< remove selected item from default collection
+ actEntry, //!< used for normal data base items
+ actSetButton, //!< connect a button with an action
+ actShowList,
+ actShowCommands,
+ actCreateOrder,
+ actDeleteOrder,
+ actSync,
+ actAddAllToDefaultCollection,
+ actAddThisToDefaultCollection,
+ actSetDefaultCollection,
+ actOrder,
+ actUnused6,
+ actEditOrder,
+ actExternal0 = 1000, //!< used for external commands, the number is the entry number in the .conf file starting with line 0
+ actExternal1, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal2, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal3, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal4, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal5, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal6, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal7, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal8, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal9, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal10, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal11, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal12, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal13, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal14, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal15, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal16, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal17, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal18, //!< used for external commands, the number is the entry number in the .conf file
+ actExternal19, //!< used for external commands, the number is the entry number in the .conf file
+};
+
+//! \brief the highest possible actExternal value
+const mgActions actExternalHigh = actExternal19;
+
+//! \brief a generic class for the definition of user actions
+class mgAction
+{
+ public:
+
+ //! \brief if true, can be displayed
+ virtual bool Enabled(mgActions on = mgActions(0));
+
+ //! \brief the action to be executed
+ virtual void Execute () {}
+
+ //! \brief handles the kBack key
+ virtual eOSState Back();
+
+/*! \brief the name for a button.
+ * The returned C string should be static, it will never be freed.
+ */
+ virtual const char *ButtonName ()
+ {
+ return "";
+ }
+
+/*! \brief the name for a menu entry. If empty, no button will be able
+ * to execute this. The returned C string must be freeable at any time.
+ * \param value a string that can be used for building the menu name.
+ */
+ virtual const char *MenuName (const unsigned int idx=0,const mgSelItem& item=zeroitem)
+ {
+ return strdup(ButtonName());
+ }
+
+ //! \brief default constructor
+ mgAction ();
+
+ //! \brief default destructor
+ virtual ~ mgAction ();
+
+ //! \brief assoiates the action with the menu which created it
+ void SetMenu (mgMenu * const menu);
+
+ //! \brief what to do when mgStatus::OsdCurrentItem is called
+ //for this one
+ mgActions Type();
+ void SetText(const char *text,bool copy=true);
+ const char *Text();
+
+ void TryNotify();
+
+ /*! \brief vdr calls OsdCurrentItem more often than we
+ * want. This tells mgStatus to ignore the next call
+ * for a specific item.
+ * \todo is this behaviour intended or a bug in vdr
+ * or in muggle ?
+ */
+ bool IgnoreNextEvent;
+
+ /*! \brief defines a reference. Can be used depending on the
+ * concrete class type. Currently used only by mgEntry
+ */
+ void setHandle(unsigned int handle);
+
+ protected:
+
+ //! \brief returns the OSD owning the menu owning this item
+ mgMainMenu* osd ();
+
+ //! \brief the menu that created this object
+ mgMenu * m;
+
+ //! \brief returns the active selection
+ mgSelection* selection ();
+
+ //! \brief returns the collection selection
+ mgSelection* collselection ();
+
+ //! \brief returns the playselection
+ mgSelection* playselection ();
+
+ virtual void Notify();
+ eOSState ProcessKey(eKeys key);
+ virtual eOSState Process(eKeys key) { return osUnknown; }
+
+ unsigned int m_handle;
+ private:
+ mgMainMenu *m_osd;
+};
+
+
+class mgActionWithIntValue: public mgAction
+{
+ public:
+ mgActionWithIntValue() { intval=0; }
+ void setValue(int i) { intval = i; }
+ protected:
+ int intval;
+};
+
+//! \brief generate an mgAction for action
+mgAction* actGenerate(const mgActions action);
+mgAction* actGenerateBoolItem(const char *Name, int *Value);
+mgAction* actGenerateKeyItem(const char *Name, int *Value, int NumStrings, const char * const * Strings);
+
+#endif
diff --git a/muggle-plugin/vdr_config.h b/muggle-plugin/vdr_config.h
new file mode 100644
index 0000000..20f7d23
--- /dev/null
+++ b/muggle-plugin/vdr_config.h
@@ -0,0 +1,118 @@
+/*!
+ * \file vdr_menu.c
+ * \brief Implements menu handling for browsing media libraries within VDR
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+// This files contains some option switches/values for compile time
+// configuration of the MP3/MPlayer plugin. You should only alter
+// this file, if you have understand the source code parts which deal
+// with the changed value.
+
+// After changing this file you should do a "make plugins-clean ; make plugins"
+// to recompile vdr.
+
+#ifndef ___CONFIG_H
+#define ___CONFIG_H
+
+// The buffer size in bytes for the decoded audio data which is about to
+// be send to the dvb driver. Should not be made to big, as this delays the
+// pause function too much.
+#define MP3BUFSIZE (100*1024)
+
+// The number of consecutive frame decoding error that may occure during
+// decoding before aborting the file.
+#define MAX_FRAME_ERR 10
+
+// Defines the min. gain required to launch the normalizer. Don't bother normalizing
+// songs which wouldn't change much in volumen.
+#define MIN_GAIN 0.03
+
+// Comment this out to disable the fast limiter function in the normalizer. The fast function
+// uses a lookup table and is 12-14 times faster than the normal function, but less
+// accurate (rel. error about 1e-7).
+#define USE_FAST_LIMITER
+
+// Defines the filename extention to use for playlist files.
+#define PLAYLISTEXT ".m3u"
+
+// Defines the text to identify WinAmp-Style playlists.
+#define WINAMPEXT "#EXTM3U"
+
+// Comment this out, if you don't want to use mmap() to access the data files.
+// Normaly there is no need to disable mmap(), as the code switches to normal
+// file i/o if mmap() is not available for a specific file/filesystem.
+#define USE_MMAP
+
+// Defines the max. memory size used for mmapping. This should not exceed the
+// available free memory on your machine.
+#define MAX_MMAP_SIZE (32*1024*1024)
+
+// The buffer size in bytes to use for normal file i/o if a file cannot be
+// mmap()ed. If set to large on slow media, may cause audio drop outs as the
+// audio buffer may underrun while filling this buffer.
+#define MP3FILE_BUFSIZE (32*1024)
+
+// The filename to save the cached id3 information. The file is located in the
+// video directory and this definition must not contain any path (no "/" )
+#define CACHEFILENAME "id3info.cache"
+
+// The interval in seconds in which the id3 cache is saved to disk (only
+// if any changes occured).
+#define CACHESAVETIMEOUT 120
+
+// How many days to keep unused entries in the id3 cache. Unused entries from
+// undefined mp3sources are purged after timeout/10 days.
+#define CACHEPURGETIMEOUT 120
+
+// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
+
+// Defines the timeout in seconds for functions which use a single key
+// (e.g. openning the playlist window). If the key is repressed during
+// the timeout, the secondary function is activated.
+#define MULTI_TIMEOUT 3
+
+// Defines the timeout in ms for entering the single digits in direct song
+// selection.
+#define SELECT_TIMEOUT 1000
+
+// If the progress display is closed on direct song selection, the display
+// is opend temporarily. This defines the time in seconds after the display
+// is closed again.
+#define SELECTHIDE_TIMEOUT 3
+
+// Defines the time in seconds to jump inside a song with left/right.
+#define JUMPSIZE 3
+
+// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
+
+// DEBUGGING OPTIONS
+// (not needed for normal operation)
+
+// Uncomment to enable generic debugging messages to the console. This may slow
+// down operation in some cases.
+#define DEBUG
+
+// Uncomment to disable audio output to the dvb driver. The audio data is
+// simply discarded.
+//#define DUMMY_OUTPUT
+
+// Uncomment to enable dumping the normalize limiter lookup table to the file
+// "/tmp/limiter". The generated file will be about 3MB in size. This option shouldn't
+// be enabled for day-by-day operation.
+//#define ACC_DUMP
+#endif //___CONFIG_H
diff --git a/muggle-plugin/vdr_decoder.c b/muggle-plugin/vdr_decoder.c
new file mode 100644
index 0000000..0d7bbf1
--- /dev/null
+++ b/muggle-plugin/vdr_decoder.c
@@ -0,0 +1,217 @@
+/*!
+ * \file vdr_decoder.h
+ * \brief A generic decoder for a VDR media plugin (muggle)
+ * \ingroup vdr
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from:
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001-2003 Stefan Huelswitt <huels@iname.com>
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/vfs.h>
+
+#include "mg_selection.h"
+
+#include <videodir.h>
+#include <interface.h>
+
+
+#include "vdr_setup.h"
+#include "vdr_decoder.h"
+#include "vdr_decoder_mp3.h"
+
+extern void showmessage(const char *,int duration=0);
+
+#ifdef HAVE_VORBISFILE
+#include "vdr_decoder_ogg.h"
+#endif
+
+#ifdef HAVE_FLAC
+#include "vdr_decoder_flac.h"
+#endif
+
+
+// --- mgDecoders ---------------------------------------------------------------
+
+mgMediaType mgDecoders::getMediaType (std::string s)
+{
+ mgMediaType mt = MT_UNKNOWN;
+
+ char *
+ f = (char *) s.c_str ();
+ char *
+ p = f + strlen (f) - 1; // point to the end
+
+ while (p >= f && *p != '.')
+ --p;
+
+ if (!strcasecmp (p, ".mp3"))
+ {
+ mt = MT_MP3;
+ }
+ else
+ {
+ if (!strcasecmp (p, ".ogg"))
+ {
+#ifdef HAVE_VORBISFILE
+ mt = MT_OGG;
+#else
+ mgWarning("Support for vorbis not compiled in, define HAVE_VORBISFILE in Makefile");
+#endif
+ }
+ else
+ {
+ if (!strcasecmp (p, ".flac"))
+ {
+#ifdef HAVE_FLAC
+ mt = MT_FLAC;
+#else
+ mgWarning("Support for flac not compiled in, define HAVE_FLAC in Makefile");
+#endif
+ }
+ }
+ }
+ return mt;
+}
+
+
+mgDecoder *
+mgDecoders::findDecoder (mgContentItem * item)
+{
+ mgDecoder *decoder = 0;
+
+ std::string filename = item->getSourceFile ();
+
+ struct stat st;
+ if (stat (filename.c_str (), &st))
+ {
+ char *b=0;
+ int nsize = filename.size();
+ if (nsize<30)
+ asprintf(&b,tr("%s not readable"),filename.c_str());
+ else
+ asprintf(&b,tr("%s..%s not readable"),filename.substr(0,20).c_str(),filename.substr(nsize-20).c_str());;
+ showmessage(b);
+ free(b);
+ esyslog ("ERROR: cannot stat %s. Meaning not found, not a valid file, or no access rights", filename.c_str ());
+ return 0;
+ }
+
+ switch (getMediaType (filename))
+ {
+ case MT_MP3:
+ {
+ decoder = new mgMP3Decoder (item);
+ } break;
+#ifdef HAVE_VORBISFILE
+ case MT_OGG:
+ {
+ decoder = new mgOggDecoder (item);
+ } break;
+#endif
+#ifdef HAVE_FLAC
+ case MT_FLAC:
+ {
+ decoder = new mgFlacDecoder( item );
+ } break;
+#endif
+ /*
+ case MT_MP3_STREAM: decoder = new mgMP3StreamDecoder(full); break;
+ #ifdef HAVE_SNDFILE
+ case MT_SND: decoder = new cSndDecoder(full); break;
+ #endif
+ */
+ default:
+ {
+ esyslog ("ERROR: unknown media type ");
+ }
+ break;
+ }
+
+ if (decoder && !decoder->valid ())
+ {
+ // no decoder found or decoder doesn't match
+
+ delete decoder; // might be carried out on NULL pointer
+ decoder = 0;
+
+ esyslog ("ERROR: no valid decoder found for %s", filename.c_str ());
+ }
+ return decoder;
+}
+
+
+// --- mgDecoder ----------------------------------------------------------------
+
+mgDecoder::mgDecoder (mgContentItem * item)
+{
+ m_item = item;
+ m_locked = 0;
+ m_urgentLock = false;
+ m_playing = false;
+}
+
+mgDecoder::~mgDecoder ()
+{
+}
+
+void
+mgDecoder::lock (bool urgent)
+{
+ m_locklock.Lock ();
+
+ if (urgent && m_locked)
+ {
+ m_urgentLock = true; // signal other locks to release quickly
+ }
+ m_locked++;
+
+ m_locklock.Unlock (); // don't hold the "locklock" when locking "lock", may cause a deadlock
+ m_lock.Lock ();
+ m_urgentLock = false;
+}
+
+
+void
+mgDecoder::unlock (void)
+{
+ m_locklock.Lock ();
+
+ m_locked--;
+
+ m_lock.Unlock ();
+ m_locklock.Unlock ();
+}
+
+
+bool mgDecoder::tryLock (void)
+{
+ bool
+ res = false;
+ m_locklock.Lock ();
+
+ if (!m_locked && !m_playing)
+ {
+ m_locked++;
+
+ m_locklock.Unlock (); // don't hold the "locklock" when locking
+ // "lock", may cause a deadlock
+ m_lock.Lock ();
+ m_urgentLock = false;
+ res = true;
+ }
+ else
+ m_locklock.Unlock ();
+ return res;
+}
diff --git a/muggle-plugin/vdr_decoder.h b/muggle-plugin/vdr_decoder.h
new file mode 100644
index 0000000..3dd6c00
--- /dev/null
+++ b/muggle-plugin/vdr_decoder.h
@@ -0,0 +1,170 @@
+/*!
+ * \file vdr_decoder.h
+ * \brief A generic decoder for a VDR media plugin (muggle)
+ * \ingroup vdr
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___DECODER_H
+#define ___DECODER_H
+
+#include <string>
+
+#include <thread.h>
+#include <string>
+
+#define DEC_ID(a,b,c,d) (((a)<<24)+((b)<<16)+((c)<<8)+(d))
+
+class mgContentItem;
+
+// --------From decoder_core.h ------------------------------------
+
+/*!
+ * \brief The current status of the decoder
+ * \ingroup vdr
+ */
+enum eDecodeStatus
+{
+ dsOK = 0, dsPlay, dsSkip, dsEof, dsError, dsSoftError
+};
+
+// ----------------------------------------------------------------
+
+/*!
+ * \brief A data structure to put decoded PCM data
+ * \ingroup vdr
+ */
+struct mgDecode
+{
+ eDecodeStatus status;
+ int index;
+ struct mad_pcm *pcm;
+};
+
+// ----------------------------------------------------------------
+
+/*!
+ * \brief Information about ???
+ * \ingroup vdr
+ */
+class mgPlayInfo
+{
+ public:
+ int m_index, m_total;
+};
+
+// ----------------------------------------------------------------
+
+/*!
+ * \brief Media types
+ * \ingroup vdr
+ */
+enum mgMediaType
+{
+ MT_MP3, MT_MP3_STREAM, MT_OGG, MT_FLAC, MT_UNKNOWN
+};
+
+/*!
+ * \brief A generic decoder class to handle conversion into PCM format
+ * \ingroup vdr
+ */
+class mgDecoder
+{
+ protected:
+
+ /*! \brief database handle to the track being decoded */
+ mgContentItem * m_item;
+
+ /*! \brief The currently playing file */
+ std::string m_filename;
+
+ /*! \brief Mutexes to coordinate threads */
+ cMutex m_lock, m_locklock;
+ int m_locked;
+ bool m_urgentLock;
+
+ /*! \brief Whether the decoder is currently active */
+ bool m_playing;
+
+ /*! \brief ??? */
+ mgPlayInfo m_playinfo;
+
+ /*! \brief Place a lock */
+ virtual void lock (bool urgent = false);
+
+ /*! \brief Release a lock */
+ virtual void unlock (void);
+
+ /*! \brief Try to obtain a lock */
+ virtual bool tryLock (void);
+
+ public:
+
+ //@{
+ /*! \brief The constructor */
+ mgDecoder (mgContentItem * item);
+
+ /*! \brief The destructor */
+ virtual ~ mgDecoder ();
+ //@}
+
+ /*! \brief Whether a decoder instance is able to play the given file */
+ virtual bool valid () = 0;
+
+ /*! \brief Whether a stream (i.e. from the network is being decoded */
+ virtual bool isStream ()
+ {
+ return false;
+ }
+
+ /*! \brief Start decoding */
+ virtual bool start () = 0;
+
+ /*! \brief Stop decoding */
+ virtual bool stop () = 0;
+
+ /*! \brief Skip an amount of time. Impossible by default */
+ virtual bool skip (int seconds, int avail, int rate)
+ {
+ return false;
+ }
+
+ /*! \brief Return decoded data */
+ virtual struct mgDecode *decode () = 0;
+
+ /*! \brief Information about the current playback status */
+ virtual mgPlayInfo *playInfo ()
+ {
+ return 0;
+ }
+};
+
+// ----------------------------------------------------------------
+
+/*!
+ * \brief A generic decoder manager class to handle different decoders
+ */
+class mgDecoders
+{
+ public:
+
+ /*! \brief Try to find a valid decoder for a file
+ */
+ static mgDecoder *findDecoder (mgContentItem * item);
+
+ /*! \brief determine the media type for a given source
+ */
+ static mgMediaType getMediaType (std::string filename);
+
+};
+#endif //___DECODER_H
diff --git a/muggle-plugin/vdr_decoder_flac.c b/muggle-plugin/vdr_decoder_flac.c
new file mode 100644
index 0000000..fd1ca53
--- /dev/null
+++ b/muggle-plugin/vdr_decoder_flac.c
@@ -0,0 +1,363 @@
+/*! \file vdr_decoder_flac.c
+ * \ingroup vdr
+ *
+ * The file implements a decoder which is used by the player to decode flac audio files.
+ *
+ * Based on code from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001-2003 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifdef HAVE_FLAC
+
+#define DEBUG
+
+#include <string>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "mg_tools.h"
+#include "mg_content.h"
+
+#include "vdr_setup.h"
+#include "vdr_decoder_flac.h"
+
+
+#include <mad.h>
+
+
+using namespace std;
+
+static const unsigned MAX_RES_SIZE = 16384;
+
+// --- mgFlacDecoder -------------------------------------------------------------
+
+mgFlacDecoder::mgFlacDecoder( mgContentItem *item )
+ : mgDecoder( item ), FLAC::Decoder::File()
+{
+ mgLog lg( "mgFlacDecoder::mgFlacDecoder" );
+
+ m_filename = item->getSourceFile();
+ m_pcm = 0;
+ m_reservoir = 0;
+
+ initialize();
+}
+
+mgFlacDecoder::~mgFlacDecoder()
+{
+ mgLog lg( "mgFlacDecoder::~mgFlacDecoder" );
+ clean();
+}
+
+bool mgFlacDecoder::valid()
+{
+ // how to check whether this is a valid flac file?
+ return is_valid();
+}
+
+mgPlayInfo *mgFlacDecoder::playInfo(void)
+{
+ return 0;
+}
+
+bool mgFlacDecoder::initialize()
+{
+ mgLog lg( "mgFlacDecoder::initialize" );
+ bool state = true;
+
+ clean();
+
+ // set_metadata_ignore_all();
+ set_metadata_respond( FLAC__METADATA_TYPE_STREAMINFO );
+ set_filename( m_filename.c_str() );
+
+ m_first = true;
+ m_reservoir_count = 0;
+ m_current_time_ms = 0;
+ m_len_decoded = 0;
+ m_index = 0;
+ m_pcm = new struct mad_pcm;
+
+ // init reservoir buffer; this should be according to the maximum
+ // frame/sample size that we can probably obtain from metadata
+ m_reservoir = new (FLAC__int32*)[2];
+ m_reservoir[0] = new FLAC__int32[MAX_RES_SIZE];
+ m_reservoir[1] = new FLAC__int32[MAX_RES_SIZE];
+
+ /*FLAC::Decoder::File::State d =*/ init(); // TODO: check this
+
+ process_until_end_of_metadata(); // basically just skip metadata
+
+ return state;
+}
+
+bool mgFlacDecoder::clean()
+{
+ mgLog lg( "mgFlacDecoder::clean" );
+ m_playing = false;
+
+ delete m_pcm;
+ m_pcm = 0;
+
+ if( m_reservoir )
+ {
+ delete[] m_reservoir[0];
+ delete[] m_reservoir[1];
+ }
+ delete[] m_reservoir;
+ m_reservoir = 0;
+
+ // why false? true?
+ return true;
+}
+
+bool mgFlacDecoder::start()
+{
+ MGLOG( "mgFlacDecoder::start" );
+ bool res = false;
+ lock(true);
+
+ // can FLAC handle more than 2 channels anyway?
+ if( m_item->getChannels() <= 2 )
+ {
+ m_playing = true;
+ res = true;
+ }
+ else
+ {
+ mgError( "ERROR: cannot play flac file %s: more than 2 channels", m_filename.c_str() );
+ clean();
+ }
+
+ unlock();
+ return res;
+}
+
+bool mgFlacDecoder::stop(void)
+{
+ MGLOG( "mgFlacDecoder::stop" );
+ lock();
+ finish();
+
+ if( m_playing )
+ {
+ clean();
+ }
+ unlock();
+
+ return true;
+}
+
+struct mgDecode *mgFlacDecoder::done( eDecodeStatus status )
+{
+ m_ds.status = status;
+ m_ds.index = m_index;
+ m_ds.pcm = m_pcm;
+
+ unlock(); // release the lock from Decode() !
+
+ return &m_ds;
+}
+
+struct mgDecode *mgFlacDecoder::decode()
+{
+ // mgLog lg( "mgFlacDecoder::decode" );
+ m_decode_status = dsPlay;
+
+ const unsigned SF_SAMPLES = (sizeof(m_pcm->samples[0])/sizeof(mad_fixed_t));
+
+ lock(true); // this is released in done()
+
+ if( m_playing )
+ {
+ m_pcm->samplerate = m_item->getSampleRate(); // from database
+ m_pcm->channels = m_item->getChannels(); // from database
+
+ // if there is enough data in the reservoir, don't start decoding
+ // PROBLEM: but we need a first time!
+ bool finished;
+ if( m_first )
+ {
+ finished = false;
+ m_first = false;
+ }
+ else
+ {
+ finished = m_reservoir_count >= SF_SAMPLES;
+ }
+
+ while( !finished )
+ { // decode single frames until m_reservoir_count >= SF_SAMPLES or eof/error
+ m_first = false;
+
+ // decode a single sample into reservoir_buffer (done by the write callback)
+ process_single();
+ if (get_stream_decoder_state()==FLAC__STREAM_DECODER_END_OF_STREAM)
+ {
+ m_decode_status = dsEof;
+ finished = true;
+ }
+
+ // check termination criterion
+ finished |= m_reservoir_count >= SF_SAMPLES || m_len_decoded == 0; // or error?
+ }
+
+ // transfer min( SF_SAMPLES, m_reservoir_count ) to pcm buffer
+
+ int n = ( SF_SAMPLES <= m_reservoir_count )? SF_SAMPLES: m_reservoir_count;
+
+ m_pcm->length = n;
+ m_index = m_current_time_ms;
+
+ // fill pcm container from reservoir buffer
+ FLAC__int32 *data0 = m_reservoir[0];
+ FLAC__int32 *data1 = m_reservoir[1];
+
+ mad_fixed_t *sam0 = m_pcm->samples[0];
+ mad_fixed_t *sam1 = m_pcm->samples[1];
+
+ // determine shift value for mad_fixed conversion
+ // TODO -- check for real bitsize and shit accordingly (left/right)
+ const int s = MAD_F_FRACBITS + 1 - ( sizeof(short)*8 );
+ // const int s = ( sizeof(int)*8 ) - 1 - MAD_F_FRACBITS; // from libsoundfile decoder
+
+ if( m_pcm->channels > 1 )
+ {
+ for( int j=n; j > 0 ; j-- )
+ {
+ // copy buffer and transform (cf. libsoundfile decoder)
+ *sam0++ = (*data0++) << s;
+ *sam1++ = (*data1++) << s;
+ }
+ // "delete" transferred samples from reservoir buffer
+ memmove( m_reservoir[0], m_reservoir[0] + n, (m_reservoir_count - n)*sizeof(FLAC__int32) );
+ memmove( m_reservoir[1], m_reservoir[1] + n, (m_reservoir_count - n)*sizeof(FLAC__int32) );
+ }
+ else
+ {
+ for( int j=n; j > 0 ; j--)
+ {
+ *sam0++ = (*data0++) << s;
+ }
+ memmove( m_reservoir[0], m_reservoir[0] + n, (m_reservoir_count - n)*sizeof(FLAC__int32) );
+ }
+ m_reservoir_count -= n;
+
+ // and return, indicating that playing will continue (unless an error has occurred)
+ return done( m_decode_status );
+ }
+
+ return done(dsError);
+}
+
+::FLAC__StreamDecoderWriteStatus
+mgFlacDecoder::write_callback(const ::FLAC__Frame *frame,
+ const FLAC__int32 * const buffer[])
+{
+ // mgLog lg( "mgFlacDecoder::write_callback" );
+
+ // add decoded buffer to reservoir
+ m_len_decoded = frame->header.blocksize;
+ m_current_samples += m_len_decoded;
+ m_current_time_ms += (m_len_decoded*1000) / m_pcm->samplerate; // in milliseconds
+
+ // cout << "Samples decoded: " << m_len_decoded << ", current time: " << m_current_time_ms << ", bits per sample: "<< m_nBitsPerSample << endl << flush;
+
+ // append buffer to m_reservoir
+ if( m_len_decoded > 0 )
+ {
+ memmove( m_reservoir[0] + m_reservoir_count, buffer[0], m_len_decoded*sizeof(FLAC__int32) );
+
+ if( m_pcm->channels > 1 )
+ {
+ memmove( m_reservoir[1] + m_reservoir_count, buffer[1], m_len_decoded*sizeof(FLAC__int32) );
+ }
+
+ m_reservoir_count += m_len_decoded;
+ }
+ else
+ {
+ m_decode_status = dsEof;
+ }
+
+ return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+void mgFlacDecoder::metadata_callback( const ::FLAC__StreamMetadata *metadata )
+{
+ // not needed since metadata is ignored!?
+ mgLog lg( "mgFlacDecoder::metadata_callback" );
+
+ if(metadata->type == FLAC__METADATA_TYPE_STREAMINFO)
+ {
+ m_nTotalSamples = (unsigned)(metadata->data.stream_info.total_samples & 0xfffffffful);
+ m_nBitsPerSample = metadata->data.stream_info.bits_per_sample;
+ m_nCurrentChannels = metadata->data.stream_info.channels;
+ m_nCurrentSampleRate = metadata->data.stream_info.sample_rate;
+ m_nFrameSize = metadata->data.stream_info.max_framesize;
+ m_nBlockSize = metadata->data.stream_info.max_blocksize;
+
+ m_nCurrentSampleRate = (int)get_sample_rate();
+ m_nCurrentChannels = (int)get_channels();
+ m_nCurrentBitsPerSample = (int)get_bits_per_sample();
+
+ // m_nLengthMS = m_nTotalSamples * 10 / (m_nCurrentSampleRate / 100);
+ m_nBlockAlign = (m_nBitsPerSample / 8) * m_nCurrentChannels;
+
+ // m_nAverageBitRate = ((m_pReader->GetLength() * 8) / (m_nLengthMS / 1000) / 1000);
+ // m_nCurrentBitrate = m_nAverageBitRate;
+ }
+}
+
+void mgFlacDecoder::error_callback( ::FLAC__StreamDecoderErrorStatus status )
+{
+ mgLog lg( "mgFlacDecoder::error_callback" );
+
+ // check status and return
+ switch( status )
+ {
+ case FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC:
+ {
+ m_error = "An error in the stream caused the decoder to lose synchronization";
+ } break;
+ case FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER:
+ {
+ m_error = "The decoder encountered a corrupted frame header.";
+ } break;
+ case FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH:
+ {
+ m_error = "The frame's data did not match the CRC in the footer.";
+ };
+ default:
+ {
+ m_error = "Unknown error occurred.";
+ }
+ }
+
+ // cout << "Error occured: " << m_error << endl;
+ m_decode_status = dsError;
+}
+
+bool mgFlacDecoder::skip(int seconds, int avail, int rate)
+{
+ lock();
+ bool res = false;
+
+ if( m_playing )
+ {
+ const long target_time_ms = ((seconds-avail)*rate / 1000) + m_current_time_ms;
+ const double distance = target_time_ms / (double)m_nLengthMS;
+ const unsigned target_sample = (unsigned)(distance * (double)m_nTotalSamples);
+
+ if( seek_absolute( (FLAC__uint64)target_sample) )
+ {
+ m_current_time_ms = target_time_ms;
+ res = true;
+ }
+ }
+ unlock();
+ return res;
+}
+
+#endif //HAVE_FLAC
diff --git a/muggle-plugin/vdr_decoder_flac.h b/muggle-plugin/vdr_decoder_flac.h
new file mode 100644
index 0000000..581d1af
--- /dev/null
+++ b/muggle-plugin/vdr_decoder_flac.h
@@ -0,0 +1,80 @@
+/*! \file vdr_decoder_flac.h
+ * \ingroup vdr
+ *
+ * The file contains a decoder which is used by the player to decode flac audio files.
+ *
+ * Based on code from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001-2003 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___DECODER_FLAC_H
+#define ___DECODER_FLAC_H
+
+#define DEC_FLAC DEC_ID('F','L','A','C')
+#define DEC_FLAC_STR "FLAC"
+
+#ifdef HAVE_FLAC
+
+#include "vdr_decoder.h"
+#include <FLAC++/decoder.h>
+
+// ----------------------------------------------------------------
+
+class mgFlacDecoder : public mgDecoder,
+ public FLAC::Decoder::File
+{
+ private:
+
+ struct mgDecode m_ds;
+ struct mad_pcm *m_pcm;
+ unsigned long long m_index, m_current_time_ms, m_current_samples;
+ unsigned int m_reservoir_count, m_len_decoded, m_samplerate;
+
+ long m_nCurrentSampleRate, m_nCurrentChannels, m_nCurrentBitsPerSample;
+ long m_nLengthMS, m_nBlockAlign, m_nAverageBitRate, m_nCurrentBitrate;
+ long m_nTotalSamples, m_nBitsPerSample, m_nFrameSize, m_nBlockSize;
+
+ bool m_state, m_first, m_continue;
+ std::string m_error;
+
+ FLAC__int32 **m_reservoir;
+
+ bool initialize();
+ bool clean();
+
+ struct mgDecode* done(eDecodeStatus status);
+ eDecodeStatus m_decode_status;
+
+ protected:
+
+ /*! \brief FLAC decoder callback routines */
+ //@{
+ virtual ::FLAC__StreamDecoderWriteStatus write_callback(const ::FLAC__Frame *frame,
+ const FLAC__int32 * const buffer[]);
+ virtual void metadata_callback(const ::FLAC__StreamMetadata *metadata);
+ virtual void error_callback(::FLAC__StreamDecoderErrorStatus status);
+ //@}
+
+ public:
+
+ mgFlacDecoder( mgContentItem *item );
+ ~mgFlacDecoder();
+
+ virtual mgPlayInfo *playInfo();
+
+ virtual bool valid();
+
+ virtual bool start();
+
+ virtual struct mgDecode *decode(void);
+
+ virtual bool skip(int Seconds, int Avail, int Rate);
+
+ virtual bool stop();
+};
+
+// ----------------------------------------------------------------
+
+#endif //HAVE_FLAC
+#endif //___DECODER_FLAC_H
diff --git a/muggle-plugin/vdr_decoder_mp3.c b/muggle-plugin/vdr_decoder_mp3.c
new file mode 100644
index 0000000..6569760
--- /dev/null
+++ b/muggle-plugin/vdr_decoder_mp3.c
@@ -0,0 +1,464 @@
+/*!
+ * \file vdr_decoder_mp3.h
+ * \brief An mp3 decoder for a VDR media plugin (muggle)
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <cmath>
+#include <iostream>
+
+#include "vdr_config.h"
+#include "vdr_decoder_mp3.h"
+#include "vdr_stream.h"
+#include "vdr_setup.h"
+
+#include "mg_tools.h"
+#include "mg_content.h"
+
+#define d(x) x
+
+// ----------------------------------------------------------------
+
+int
+mgMadStream (struct mad_stream *stream, mgStream * str)
+{
+ unsigned char *data;
+ unsigned long len;
+ if (str->stream (data, len, stream->next_frame))
+ {
+ if (len > 0)
+ {
+ mad_stream_buffer (stream, data, len);
+ }
+ return len;
+ }
+ return -1;
+}
+
+
+// --- mgMP3Decoder -------------------------------------------------------------
+
+mgMP3Decoder::mgMP3Decoder (mgContentItem * item, bool preinit):mgDecoder
+(item)
+{
+ m_stream = 0;
+ m_isStream = false;
+
+ m_filename = item->getSourceFile ();
+
+ if (preinit)
+ {
+ m_stream = new mgStream (m_filename);
+ printf ("m_stream for %s\n", m_filename.c_str ());
+
+ }
+ m_madframe = 0;
+ m_madsynth = 0;
+
+ memset (&m_madstream, 0, sizeof (m_madstream));
+
+ init ();
+}
+
+
+mgMP3Decoder::~mgMP3Decoder ()
+{
+ clean ();
+ delete m_stream;
+}
+
+
+void
+mgMP3Decoder::init ()
+{
+ clean ();
+ mad_stream_init (&m_madstream);
+
+ m_madframe = new mad_frame;
+ mad_frame_init (m_madframe);
+
+ m_madsynth = new mad_synth;
+ mad_synth_init (m_madsynth);
+
+ mad_stream_options (&m_madstream, MAD_OPTION_IGNORECRC);
+
+ m_playtime = mad_timer_zero;
+ m_skiptime = mad_timer_zero;
+ m_framenum = m_framemax = 0;
+ m_frameinfo = 0;
+ m_mute = m_errcount = 0;
+}
+
+
+void
+mgMP3Decoder::clean ()
+{
+ m_playing = false;
+ if (m_madsynth)
+ {
+ mad_synth_finish (m_madsynth);
+ delete m_madsynth;
+ m_madsynth = 0;
+ }
+
+ if (m_madframe)
+ {
+ mad_frame_finish (m_madframe);
+ delete m_madframe;
+ m_madframe = 0;
+ }
+ mad_stream_finish (&m_madstream);
+}
+
+
+bool mgMP3Decoder::valid (void)
+{
+ bool
+ res = false;
+ if (tryLock ())
+ {
+ if (start ())
+ {
+ struct mgDecode *
+ dd;
+ int
+ count = 10;
+ do
+ {
+ dd = decode ();
+ if (dd->status == dsEof)
+ {
+ count = 0;
+ }
+ if (dd->status != dsPlay)
+ {
+ break;
+ }
+ }
+ while (--count);
+ if (!count)
+ {
+ res = true;
+ }
+ stop ();
+ }
+ unlock ();
+ }
+ return res;
+}
+
+
+mgPlayInfo *
+mgMP3Decoder::playInfo ()
+{
+ if (m_playing)
+ {
+ m_playinfo.m_index = mad_timer_count (m_playtime, MAD_UNITS_SECONDS);
+
+ return &m_playinfo;
+ }
+ return 0;
+}
+
+
+bool mgMP3Decoder::start ()
+{
+ lock (true);
+ init ();
+ m_playing = true;
+
+ if (m_stream->open (true))
+ {
+ if (!m_isStream)
+ {
+ m_stream->seek ();
+
+ printf
+ ("mgMP3Decoder::start: m_framemax not determined, rewinding disabled!!!\n");
+
+/* m_framemax = scan->Frames+20; // TODO
+
+ m_frameinfo = new struct FrameInfo[m_framemax];
+ if(!m_frameinfo)
+ {
+ printf( "mgMP3Decoder::start: no memory for frame index, rewinding disabled" );
+ }
+ */
+ }
+ unlock ();
+
+ printf ("mgMP3Decoder::start: true\n");
+ return true;
+ }
+ m_stream->close ();
+ clean ();
+ unlock ();
+ printf ("mgMP3Decoder::start: false");
+ return false;
+}
+
+
+bool mgMP3Decoder::stop (void)
+{
+ lock ();
+
+ if (m_playing)
+ {
+ m_stream->close ();
+ clean ();
+ }
+ unlock ();
+ return true;
+}
+
+
+struct mgDecode *
+mgMP3Decoder::done (eDecodeStatus status)
+{
+ m_ds.status = status;
+ m_ds.index = mad_timer_count (m_playtime, MAD_UNITS_MILLISECONDS);
+ m_ds.pcm = &m_madsynth->pcm;
+ unlock (); // release the lock from Decode()
+
+ return &m_ds;
+}
+
+
+eDecodeStatus mgMP3Decoder::decodeError (bool hdr)
+{
+ if (m_madstream.error == MAD_ERROR_BUFLEN
+ || m_madstream.error == MAD_ERROR_BUFPTR)
+ {
+ int
+ s = mgMadStream (&m_madstream, m_stream);
+ if (s < 0)
+ {
+ printf ("mgMP3Decoder::decodeError: dsError returned\n");
+ return dsError;
+ }
+ if (s == 0)
+ {
+ printf ("mgMP3Decoder::decodeError: dsEof returned\n");
+ return dsEof;
+ }
+ }
+ else if (!MAD_RECOVERABLE (m_madstream.error))
+ {
+ printf
+ ("mgMP3Decoder::decodeError: mad decode %sfailed, frame=%d: %s. Returning dsError\n",
+ hdr ? "hdr " : "", m_framenum, mad_stream_errorstr (&m_madstream));
+ return dsError;
+ }
+ else
+ {
+ m_errcount += hdr ? 1 : 100;
+ printf
+ ("mgMP3Decoder::decodeError: mad decode %s error, frame=%d count=%d: %s. Returning dsOK\n",
+ hdr ? "hdr " : "", m_framenum, m_errcount,
+ mad_stream_errorstr (&m_madstream));
+ }
+ return dsOK;
+}
+
+
+struct mgDecode *
+mgMP3Decoder::decode ()
+{
+ lock (); // this is released in Done()
+ eDecodeStatus r;
+
+ while (m_playing)
+ {
+ if (m_errcount >= MAX_FRAME_ERR * 100)
+ {
+ printf ("mgMP3Decoder::decode: excessive decoding errors,"
+ " aborting file %s\n", m_filename.c_str ());
+ return done (dsError);
+ }
+
+ if (mad_header_decode (&m_madframe->header, &m_madstream) == -1)
+ {
+ if ((r = decodeError (true)))
+ {
+ return done (r);
+ }
+ }
+ else
+ {
+ if (!m_isStream)
+ {
+#ifdef DEBUG2
+ if (m_framenum >= m_framemax)
+ {
+ printf ("mgMP3Decoder::start: framenum >= framemax!!!!\n");
+ }
+#endif
+ if (m_frameinfo && m_framenum < m_framemax)
+ {
+ m_frameinfo[m_framenum].Pos =
+ m_stream->bufferPos () +
+ (m_madstream.this_frame - m_madstream.buffer);
+ m_frameinfo[m_framenum].Time = m_playtime;
+ }
+ }
+
+ mad_timer_add (&m_playtime, m_madframe->header.duration);
+ m_framenum++;
+
+ if (mad_timer_compare (m_playtime, m_skiptime) >= 0)
+ {
+ m_skiptime = mad_timer_zero;
+ }
+ else
+ {
+ return done (dsSkip); // skipping, decode next header
+ }
+
+ if (mad_frame_decode (m_madframe, &m_madstream) == -1)
+ {
+ if ((r = decodeError (false)))
+ {
+ return done (r);
+ }
+ }
+ else
+ {
+ m_errcount = 0;
+
+// TODO: // m_scan->InfoHook( &frame->header );
+
+ mad_synth_frame (m_madsynth, m_madframe);
+
+ if (m_mute)
+ {
+ m_mute--;
+ return done (dsSkip);
+ }
+ return done (dsPlay);
+ }
+ }
+ }
+ return done (dsError);
+}
+
+
+void
+mgMP3Decoder::makeSkipTime (mad_timer_t * skiptime,
+mad_timer_t playtime,
+int secs, int avail, int dvbrate)
+{
+ mad_timer_t time;
+
+ *skiptime = playtime;
+
+ mad_timer_set (&time, abs (secs), 0, 0);
+
+ if (secs < 0)
+ {
+ mad_timer_negate (&time);
+ }
+
+ mad_timer_add (skiptime, time);
+
+ // Byte/s = samplerate * 16 bit * 2 chan
+ float bufsecs = (float) avail / (float) (dvbrate * (16 / 8 * 2));
+
+ printf ("mgMP3Decoder::makeSkipTime: skip: avail=%d bufsecs=%f\n", avail,
+ bufsecs);
+
+ int full = (int) bufsecs;
+ bufsecs -= (float) full;
+
+ mad_timer_set (&time, full, (int) (bufsecs * 1000.0), 1000);
+
+ mad_timer_negate (&time);
+
+ mad_timer_add (skiptime, time);
+
+ printf
+ ("mgMP3Decoder::makeSkipTime: skip: playtime=%ld secs=%d full=%d bufsecs=%f skiptime=%ld\n",
+ mad_timer_count (playtime, MAD_UNITS_MILLISECONDS), secs, full, bufsecs,
+ mad_timer_count (*skiptime, MAD_UNITS_MILLISECONDS));
+}
+
+
+bool mgMP3Decoder::skip (int seconds, int avail, int rate)
+{
+ lock ();
+
+ bool
+ res = false;
+ if (m_playing && !m_isStream)
+ {
+ if (!mad_timer_compare (m_skiptime, mad_timer_zero))
+ { // allow only one skip at any time
+ mad_timer_t
+ time;
+ makeSkipTime (&time, m_playtime, seconds, avail, rate);
+
+ if (mad_timer_compare (m_playtime, time) <= 0)
+ { // forward skip
+#ifdef DEBUG
+ int
+ i = mad_timer_count (time, MAD_UNITS_SECONDS);
+ printf ("mgMP3Decoder::skip: forward skipping to %02d:%02d\n",
+ i / 60, i % 60);
+#endif
+ m_skiptime = time;
+ m_mute = 1;
+ res = true;
+ }
+ else
+ { // backward skip
+ if (m_frameinfo)
+ {
+#ifdef DEBUG
+ int
+ i = mad_timer_count (time, MAD_UNITS_SECONDS);
+ printf ("mgMP3Decoder::skip: rewinding to %02d:%02d\n",
+ i / 60, i % 60);
+#endif
+ while (m_framenum
+ && mad_timer_compare (time,
+ m_frameinfo[--m_framenum].
+ Time) < 0);
+ m_mute = 2;
+ if (m_framenum >= 2)
+ {
+ m_framenum -= 2;
+ }
+ m_playtime = m_frameinfo[m_framenum].Time;
+ m_stream->seek (m_frameinfo[m_framenum].Pos);
+ // reset stream buffer
+ mad_stream_finish (&m_madstream);
+ mad_stream_init (&m_madstream);
+#ifdef DEBUG
+ i = mad_timer_count (m_playtime, MAD_UNITS_MILLISECONDS);
+ printf
+ ("mgMP3Decoder::skip: new playtime=%d framenum=%d filepos=%lld\n",
+ i, m_framenum, m_frameinfo[m_framenum].Pos);
+#endif
+ res = true;
+ }
+ }
+ }
+ }
+ unlock ();
+ return res;
+}
diff --git a/muggle-plugin/vdr_decoder_mp3.h b/muggle-plugin/vdr_decoder_mp3.h
new file mode 100644
index 0000000..47d6eb2
--- /dev/null
+++ b/muggle-plugin/vdr_decoder_mp3.h
@@ -0,0 +1,114 @@
+/*!
+ * \file vdr_decoder_mp3.h
+ * \brief An mp3 decoder for a VDR media plugin (muggle)
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___DECODER_MP3_H
+#define ___DECODER_MP3_H
+
+#define DEC_MP3 DEC_ID('M','P','3',' ')
+#define DEC_MP3_STR "MP3"
+
+#include <mad.h>
+#include <string>
+
+#include "vdr_decoder.h"
+
+#if MAD_F_FRACBITS != 28
+#warning libmad with MAD_F_FRACBITS != 28 not tested
+#endif
+
+class mgStream;
+class mgContentItem;
+
+// ----------------------------------------------------------------
+
+/*!
+ * \brief A class to decode mp3 songs into PCM using libmad
+ */
+class mgMP3Decoder:public mgDecoder
+{
+ private:
+ struct mgDecode m_ds;
+
+//
+ struct mad_stream m_madstream;
+ struct mad_frame *m_madframe;
+ struct mad_synth *m_madsynth;
+ mad_timer_t m_playtime, m_skiptime;
+
+//
+ struct FrameInfo
+ {
+ unsigned long long Pos;
+ mad_timer_t Time;
+ } *m_frameinfo;
+
+ int m_framenum, m_framemax, m_errcount, m_mute;
+
+ void init ();
+
+ void clean ();
+
+ struct mgDecode *done (eDecodeStatus status);
+
+ virtual mgPlayInfo *playInfo ();
+
+ eDecodeStatus decodeError (bool hdr);
+
+ void makeSkipTime (mad_timer_t * skiptime, mad_timer_t playtime,
+ int secs, int avail, int dvbrate);
+
+ protected:
+ mgStream * m_stream;
+ bool m_isStream;
+
+ public:
+
+/*!
+ * \brief construct a decoder from a filename
+ */
+ mgMP3Decoder (mgContentItem * item, bool preinit = true);
+
+/*!
+ * \brief the destructor
+ */
+ virtual ~ mgMP3Decoder ();
+
+/*!
+ * \brief check, whether the file contains useable MP3 content
+ */
+ virtual bool valid ();
+
+/*!
+ * \brief start the decoding process
+ */
+ virtual bool start ();
+
+/*!
+ * \brief stop the decoding process
+ */
+ virtual bool stop ();
+
+/*!
+ * \brief skip an amount of seconds
+ */
+ virtual bool skip (int seconds, int avail, int rate);
+
+/*!
+ * \brief the actual decoding function (uses libmad)
+ */
+ virtual struct mgDecode *decode ();
+};
+#endif //___DECODER_MP3_H
diff --git a/muggle-plugin/vdr_decoder_ogg.c b/muggle-plugin/vdr_decoder_ogg.c
new file mode 100644
index 0000000..47011eb
--- /dev/null
+++ b/muggle-plugin/vdr_decoder_ogg.c
@@ -0,0 +1,461 @@
+/*! \file vdr_decoder_ogg.c
+ * \ingroup vdr
+ *
+ * The file implements a decoder which is used by the player to decode ogg vorbis audio files.
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001-2003 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifdef HAVE_VORBISFILE
+
+#include "vdr_decoder_ogg.h"
+
+#include <mad.h>
+#include <vorbis/vorbisfile.h>
+
+#include <string>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "vdr_setup.h"
+
+#include "mg_content.h"
+
+// --- mgOggFile ----------------------------------------------------------------
+
+class mgOggFile // : public mgFileInfo
+{
+ private:
+
+ bool m_opened, m_canSeek;
+
+ OggVorbis_File vf;
+
+ void error (const char *action, const int err);
+
+ std::string m_filename;
+
+ public:
+
+ mgOggFile (std::string filename);
+ ~mgOggFile ();
+
+ bool open (bool log = true);
+
+ void close (void);
+
+ long long seek (long long posMs = 0, bool relativ = false);
+
+ int stream (short *buffer, int samples);
+
+ bool canSeek ()
+ {
+ return m_canSeek;
+ }
+
+ long long indexMs (void);
+};
+
+mgOggFile::mgOggFile (std::string filename):
+m_filename (filename)
+{
+ m_canSeek = false;
+ m_opened = false;
+}
+
+
+mgOggFile::~mgOggFile ()
+{
+ close ();
+}
+
+
+bool mgOggFile::open (bool log)
+{
+ if (m_opened)
+ {
+ if (m_canSeek)
+ {
+ return (seek () >= 0);
+ }
+ return true;
+ }
+
+ FILE *
+ f = fopen (m_filename.c_str (), "r");
+ if (f)
+ {
+ int
+ r = ov_open (f, &vf, 0, 0);
+ if (!r)
+ {
+ m_canSeek = (ov_seekable (&vf) != 0);
+ m_opened = true;
+ }
+ else
+ {
+ fclose (f);
+ if (log)
+ {
+ error ("open", r);
+ }
+ }
+ }
+ else
+ {
+ if (log)
+ {
+// esyslog("ERROR: failed to open file %s: %s", m_filename.c_str(), strerror(errno) );
+ }
+ }
+ return m_opened;
+}
+
+
+void
+mgOggFile::close ()
+{
+ if (m_opened)
+ {
+ ov_clear (&vf);
+ m_opened = false;
+ }
+}
+
+
+void
+mgOggFile::error (const char *action, const int err)
+{
+ char *errstr;
+ switch (err)
+ {
+ case OV_FALSE:
+ errstr = "false/no data available";
+ break;
+ case OV_EOF:
+ errstr = "EOF";
+ break;
+ case OV_HOLE:
+ errstr = "missing or corrupted data";
+ break;
+ case OV_EREAD:
+ errstr = "read error";
+ break;
+ case OV_EFAULT:
+ errstr = "internal error";
+ break;
+ case OV_EIMPL:
+ errstr = "unimplemented feature";
+ break;
+ case OV_EINVAL:
+ errstr = "invalid argument";
+ break;
+ case OV_ENOTVORBIS:
+ errstr = "no Ogg Vorbis stream";
+ break;
+ case OV_EBADHEADER:
+ errstr = "corrupted Ogg Vorbis stream";
+ break;
+ case OV_EVERSION:
+ errstr = "unsupported bitstream version";
+ break;
+ case OV_ENOTAUDIO:
+ errstr = "ENOTAUDIO";
+ break;
+ case OV_EBADPACKET:
+ errstr = "EBADPACKET";
+ break;
+ case OV_EBADLINK:
+ errstr = "corrupted link";
+ break;
+ case OV_ENOSEEK:
+ errstr = "stream not seekable";
+ break;
+ default:
+ errstr = "unspecified error";
+ break;
+ }
+// esyslog( "ERROR: vorbisfile %s failed on %s: %s", action, m_filename.c_str(), errstr );
+}
+
+
+long long
+mgOggFile::indexMs (void)
+{
+ double p = ov_time_tell (&vf);
+ if (p < 0.0)
+ {
+ p = 0.0;
+ }
+
+ return (long long) (p * 1000.0);
+}
+
+
+long long
+mgOggFile::seek (long long posMs, bool relativ)
+{
+ if (relativ)
+ {
+ posMs += indexMs ();
+ }
+
+ int r = ov_time_seek (&vf, (double) posMs / 1000.0);
+
+ if (r)
+ {
+ error ("seek", r);
+ return -1;
+ }
+
+ posMs = indexMs ();
+ return posMs;
+}
+
+
+int
+mgOggFile::stream (short *buffer, int samples)
+{
+ int n;
+ do
+ {
+ int stream;
+ n = ov_read (&vf, (char *) buffer, samples * 2, 0, 2, 1, &stream);
+ }
+ while (n == OV_HOLE);
+
+ if (n < 0)
+ {
+ error ("read", n);
+ }
+
+ return (n / 2);
+}
+
+
+// --- mgOggDecoder -------------------------------------------------------------
+
+mgOggDecoder::mgOggDecoder (mgContentItem * item):mgDecoder (item)
+{
+ m_filename = item->getSourceFile ();
+ m_file = new mgOggFile (m_filename);
+ m_pcm = 0;
+ init ();
+}
+
+
+mgOggDecoder::~mgOggDecoder ()
+{
+ clean ();
+ delete m_file;
+}
+
+
+bool mgOggDecoder::valid ()
+{
+ bool
+ res = false;
+ if (tryLock ())
+ {
+ if (m_file->open (false))
+ {
+ res = true;
+ }
+ unlock ();
+ }
+ return res;
+}
+
+
+mgPlayInfo *
+mgOggDecoder::playInfo (void)
+{
+ if (m_playing)
+ {
+// m_playinfo.m_index = index/1000;
+// m_playinfo.m_total = info.Total;
+
+ return &m_playinfo;
+ }
+
+ return 0;
+}
+
+
+void
+mgOggDecoder::init ()
+{
+ clean ();
+ m_pcm = new struct mad_pcm;
+ m_index = 0;
+}
+
+
+bool mgOggDecoder::clean ()
+{
+ m_playing = false;
+
+ delete
+ m_pcm;
+ m_pcm = 0;
+
+ m_file->close ();
+ return false;
+}
+
+
+#define SF_SAMPLES (sizeof(m_pcm->samples[0])/sizeof(mad_fixed_t))
+
+bool mgOggDecoder::start ()
+{
+ lock (true);
+ init ();
+ m_playing = true;
+
+ if (m_file->open () /*&& info.DoScan(true) */ )
+ {
+// obtain from database: rate, channels
+/* d(printf("ogg: open rate=%d channels=%d seek=%d\n",
+ info.SampleFreq,info.Channels,file.CanSeek()))
+ */
+ if (m_item->getChannels () <= 2)
+ {
+ unlock ();
+ return true;
+ }
+ else
+ {
+// esyslog( "ERROR: cannot play ogg file %s: more than 2 channels", m_filename.c_str() );
+ }
+ }
+
+ clean ();
+ unlock ();
+
+ return false;
+}
+
+
+bool mgOggDecoder::stop (void)
+{
+ lock ();
+
+ if (m_playing)
+ {
+ clean ();
+ }
+ unlock ();
+
+ return true;
+}
+
+
+struct mgDecode *
+mgOggDecoder::done (eDecodeStatus status)
+{
+ m_ds.status = status;
+ m_ds.index = m_index;
+ m_ds.pcm = m_pcm;
+
+ unlock (); // release the lock from decode()
+
+ return &m_ds;
+}
+
+
+struct mgDecode *
+mgOggDecoder::decode (void)
+{
+ lock (); // this is released in Done()
+
+ if (m_playing)
+ {
+ short framebuff[2 * SF_SAMPLES];
+ int n = m_file->stream (framebuff, SF_SAMPLES);
+
+ if (n < 0)
+ {
+ return done (dsError);
+ }
+
+ if (n == 0)
+ {
+ return done (dsEof);
+ }
+
+// should be done during initialization
+ // from database
+ m_pcm->samplerate = m_item->getSampleRate ();
+ m_pcm->channels = m_item->getChannels (); // from database
+
+ n /= m_pcm->channels;
+ m_pcm->length = n;
+ m_index = m_file->indexMs ();
+
+ short *data = framebuff;
+ mad_fixed_t *sam0 = m_pcm->samples[0], *sam1 = m_pcm->samples[1];
+
+ // shift value for mad_fixed conversion
+ const int s = MAD_F_FRACBITS + 1 - (sizeof (short) * 8);
+
+ if (m_pcm->channels > 1)
+ {
+ for (; n > 0; n--)
+ {
+ *sam0++ = (*data++) << s;
+ *sam1++ = (*data++) << s;
+ }
+ }
+ else
+ {
+ for (; n > 0; n--)
+ {
+ *sam0++ = (*data++) << s;
+ }
+ }
+ return done (dsPlay);
+ }
+ return done (dsError);
+}
+
+
+bool mgOggDecoder::skip (int Seconds, int Avail, int Rate)
+{
+ lock ();
+ bool
+ res = false;
+
+ if (m_playing && m_file->canSeek ())
+ {
+ float
+ fsecs =
+ (float) Seconds - ((float) Avail / (float) (Rate * (16 / 8 * 2)));
+// Byte/s = samplerate * 16 bit * 2 chan
+
+ long long
+ newpos = m_file->indexMs () + (long long) (fsecs * 1000.0);
+
+ if (newpos < 0)
+ {
+ newpos = 0;
+ }
+
+ newpos = m_file->seek (newpos, false);
+
+ if (newpos >= 0)
+ {
+ m_index = m_file->indexMs ();
+#ifdef xDEBUG
+ int
+ i = index / 1000;
+ printf ("ogg: skipping to %02d:%02d\n", i / 60, i % 60);
+#endif
+ res = true;
+ }
+ }
+ unlock ();
+ return res;
+}
+#endif //HAVE_VORBISFILE
diff --git a/muggle-plugin/vdr_decoder_ogg.h b/muggle-plugin/vdr_decoder_ogg.h
new file mode 100644
index 0000000..1f5fcfc
--- /dev/null
+++ b/muggle-plugin/vdr_decoder_ogg.h
@@ -0,0 +1,63 @@
+/*! \file vdr_decoder_ogg.h
+ * \ingroup vdr
+ *
+ * The file contains a decoder which is used by the player to decode ogg vorbis audio files.
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001-2003 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___DECODER_OGG_H
+#define ___DECODER_OGG_H
+
+#define DEC_OGG DEC_ID('O','G','G',' ')
+#define DEC_OGG_STR "OGG"
+
+#ifdef HAVE_VORBISFILE
+
+#include "vdr_decoder.h"
+
+// ----------------------------------------------------------------
+
+class mgOggFile;
+
+/*!
+ * \brief A decoder for Ogg Vorbis files
+ *
+ */
+class mgOggDecoder:public mgDecoder
+{
+ private:
+
+ mgOggFile * m_file;
+ struct mgDecode m_ds;
+ struct mad_pcm *m_pcm;
+ unsigned long long m_index;
+
+//
+ void init (void);
+ bool clean (void);
+ struct mgDecode *done (eDecodeStatus status);
+
+ public:
+
+ mgOggDecoder (mgContentItem * item);
+ ~mgOggDecoder ();
+
+ virtual mgPlayInfo *playInfo ();
+
+ virtual bool valid ();
+
+ virtual bool start ();
+
+ virtual bool stop ();
+
+ virtual bool skip (int Seconds, int Avail, int Rate);
+
+ virtual struct mgDecode *decode (void);
+};
+
+// ----------------------------------------------------------------
+#endif //HAVE_VORBISFILE
+#endif //___DECODER_OGG_H
diff --git a/muggle-plugin/vdr_menu.c b/muggle-plugin/vdr_menu.c
new file mode 100644
index 0000000..615d77e
--- /dev/null
+++ b/muggle-plugin/vdr_menu.c
@@ -0,0 +1,1053 @@
+/*!
+ * \file vdr_menu.c
+ * \brief Implements menu handling for browsing media libraries within VDR
+ *
+ * \version $Revision: 1.27 $ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner, Wolfgang Rohdewald
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ */
+
+#include <stdio.h>
+#include <assert.h>
+
+#include <typeinfo>
+#include <string>
+#include <vector>
+
+#include <menuitems.h>
+#include <tools.h>
+#include <config.h>
+#include <plugin.h>
+
+#if VDRVERSNUM >= 10307
+#include <vdr/interface.h>
+#include <vdr/skins.h>
+#endif
+
+#include "vdr_setup.h"
+#include "vdr_menu.h"
+#include "vdr_player.h"
+#include "i18n.h"
+
+#define DEBUG
+#include "mg_tools.h"
+
+#include <config.h>
+#if VDRVERSNUM >= 10307
+#include <vdr/interface.h>
+#include <vdr/skins.h>
+#endif
+
+void
+mgStatus::OsdCurrentItem(const char* Text)
+{
+ cOsdItem* i = main->Get(main->Current());
+ if (!i) return;
+ mgAction * a = dynamic_cast<mgAction *>(i);
+ if (!a)
+ mgError("mgStatus::OsdCurrentItem expected an mgAction*");
+ if (a)
+ a->TryNotify();
+}
+
+void Play(mgSelection *sel,const bool select) {
+ mgSelection *s = new mgSelection(sel);
+ if (select) s->select();
+ if (s->empty())
+ {
+ delete s;
+ return;
+ }
+ mgPlayerControl *c = PlayerControl ();
+ if (c)
+ c->NewPlaylist (s);
+ else
+ cControl::Launch (new mgPlayerControl (s));
+}
+
+
+//! \brief queue the selection for playing, abort ongoing instant play
+void
+mgMainMenu::PlayQueue()
+{
+ queue_playing=true;
+ instant_playing=false;
+ Play(playselection());
+}
+
+//! \brief queue the selection for playing, abort ongoing queue playing
+void
+mgMainMenu::PlayInstant(const bool select)
+{
+ instant_playing=true;
+ Play(selection(),select);
+}
+
+void
+mgMainMenu::setOrder(mgSelection *sel,unsigned int idx)
+{
+ mgOrder* o = getOrder(idx);
+ if (o->size()>0)
+ {
+ m_current_order = idx;
+ sel->setOrder(o);
+ }
+ else
+ mgWarning("mgMainMenu::setOrder: orders[%u] is empty",idx);
+}
+
+mgOrder* mgMainMenu::getOrder(unsigned int idx)
+{
+ if (idx>=orders.size())
+ mgError("mgMainMenu::getOrder(%u): orders.size() is %d",
+ idx,orders.size());
+ return orders[idx];
+}
+
+void
+mgMainMenu::CollectionChanged(string name)
+{
+ delete moveselection;
+ moveselection = NULL;
+ forcerefresh = true; // TODO brauchen wir das?
+ if (name == play_collection)
+ {
+ playselection()->clearCache();
+ mgPlayerControl *c = PlayerControl();
+ if (c)
+ c->ReloadPlaylist();
+ else
+ PlayQueue();
+ }
+ if (CollectionEntered(name) || selection()->isCollectionlist())
+ selection()->clearCache();
+}
+
+bool
+mgMainMenu::ShowingCollections()
+{
+ return (UsingCollection && selection ()->level () == 0);
+}
+
+
+bool
+mgMainMenu::DefaultCollectionSelected()
+{
+ string this_sel = trim(selection ()->getCurrentValue());
+ return (ShowingCollections () && this_sel == default_collection);
+}
+
+bool
+mgMainMenu::CollectionEntered(string name)
+{
+ if (!UsingCollection) return false;
+ if (selection()->level()==0) return false;
+ return trim(selection ()->getKeyItem(0).value()) == name;
+}
+
+
+mgMenu *
+mgMainMenu::Parent ()
+{
+ if (Menus.size () < 2)
+ return NULL;
+ return Menus[Menus.size () - 2];
+}
+
+
+mgAction*
+mgMenu::GenerateAction(const mgActions action,mgActions on)
+{
+ mgAction *result = actGenerate(action);
+ if (result)
+ {
+ result->SetMenu(this);
+ if (!result->Enabled(on))
+ {
+ delete result;
+ result=NULL;
+ }
+ }
+ return result;
+}
+
+eOSState
+mgMenu::ExecuteAction(const mgActions action,mgActions on)
+{
+ mgAction *a = GenerateAction (action,on);
+ if (a)
+ {
+ a->Execute ();
+ delete a;
+ return osContinue;
+ }
+ return osUnknown;
+}
+
+
+mgPlayerControl *
+PlayerControl ()
+{
+ mgPlayerControl *result = NULL;
+ cControl *control = cControl::Control ();
+ if (control && typeid (*control) == typeid (mgPlayerControl))
+// is there a running MP3 player?
+ result = static_cast < mgPlayerControl * >(control);
+ return result;
+}
+
+
+mgMenu::mgMenu ()
+{
+ m_osd = NULL;
+ m_parent_index=-1;
+ TreeRedAction = actNone;
+ TreeGreenAction = actNone;
+ TreeYellowAction = actNone;
+ TreeBlueAction = actNone;
+ CollRedAction = actNone;
+ CollGreenAction = actNone;
+ CollYellowAction = actNone;
+ CollBlueAction = actNone;
+}
+
+
+// ----------------------- mgMainMenu ----------------------
+
+
+void
+mgMainMenu::DumpOrders(mgValmap& nv)
+{
+ for (unsigned int idx=0;idx<orders.size();idx++)
+ {
+ mgOrder *o = orders[idx];
+ if (!o)
+ mgError("DumpOrders:order[%u] is 0",idx);
+ char prefix[20];
+ sprintf(prefix,"order%u",idx);
+ o->DumpState(nv,prefix);
+ }
+}
+
+void
+mgMainMenu::SaveState()
+{
+ char *b;
+ asprintf(&b,"%s/muggle.state",cPlugin::ConfigDirectory ("muggle"));
+ FILE *f = fopen(b,"w");
+ if (!f)
+ {
+ if (!m_save_warned)
+ mgWarning("Cannot write %s",b);
+ m_save_warned=true;
+ free(b);
+ return;
+ }
+ free(b);
+ mgValmap nmain("MainMenu");
+ nmain.put("DefaultCollection",default_collection);
+ nmain.put("UsingCollection",UsingCollection);
+ nmain.put("TreeRedAction",int(Menus.front()->TreeRedAction));
+ nmain.put("TreeGreenAction",int(Menus.front()->TreeGreenAction));
+ nmain.put("TreeYellowAction",int(Menus.front()->TreeYellowAction));
+ nmain.put("CollRedAction",int(Menus.front()->CollRedAction));
+ nmain.put("CollGreenAction",int(Menus.front()->CollGreenAction));
+ nmain.put("CollYellowAction",int(Menus.front()->CollYellowAction));
+ nmain.put("CurrentOrder",m_current_order);
+ DumpOrders(nmain);
+ mgValmap nsel("tree");
+ m_treesel->DumpState(nsel);
+ mgValmap ncol("collection");
+ m_collectionsel->DumpState(ncol);
+ nmain.Write(f);
+ nsel.Write(f);
+ ncol.Write(f);
+ fclose(f);
+}
+
+mgMainMenu::mgMainMenu ():cOsdMenu ("",25)
+{
+ m_Status = new mgStatus(this);
+ m_message = 0;
+ moveselection = 0;
+ m_root = 0;
+ external_commands = 0;
+ queue_playing=false;
+ instant_playing=false;
+ m_save_warned=false;
+ play_collection = tr("play");
+ mgValmap nsel("tree");
+ mgValmap ncol("collection");
+ mgValmap nmain("MainMenu");
+
+ // define defaults for values missing in state file:
+ nsel.put("FallThrough",true);
+ nmain.put("DefaultCollection",play_collection);
+ nmain.put("UsingCollection","false");
+ nmain.put("TreeRedAction",int(actAddThisToCollection));
+ nmain.put("TreeGreenAction",int(actInstantPlay));
+ nmain.put("TreeYellowAction",int(actToggleSelection));
+ nmain.put("CollRedAction",int(actAddThisToCollection));
+ nmain.put("CollGreenAction",int(actInstantPlay));
+ nmain.put("CollYellowAction",int(actToggleSelection));
+
+ // load values from state file
+ char *b;
+ asprintf(&b,"%s/muggle.state",cPlugin::ConfigDirectory ("muggle"));
+ FILE *f = fopen(b,"r");
+ free(b);
+ if (f) {
+ nsel.Read(f);
+ ncol.Read(f);
+ nmain.Read(f);
+ fclose(f);
+ }
+
+ // get values from mgValmaps
+ LoadOrders(nmain);
+ default_collection = nmain.getstr("DefaultCollection");
+ UsingCollection = nmain.getbool("UsingCollection");
+ InitMapFromSetup(nsel);
+ InitMapFromSetup(ncol);
+ m_treesel = new mgSelection;
+ m_treesel->setOrder(getOrder(m_current_order));
+ m_treesel->InitFrom (nsel);
+ m_treesel->CreateCollection(default_collection);
+ if (default_collection!=play_collection)
+ m_treesel->CreateCollection(play_collection);
+ vector<mgKeyTypes> kt;
+ kt.push_back(keyCollection);
+ kt.push_back(keyCollectionItem);
+ mgOrder o;
+ o.setKeys(kt);
+ m_collectionsel = new mgSelection;
+ m_collectionsel->setOrder(&o);
+ m_collectionsel->InitFrom (ncol);
+ m_playsel = new mgSelection;
+ m_playsel->setOrder(&o);
+ m_playsel->InitFrom(ncol);
+
+ // initialize
+ if (m_playsel->level()!=1)
+ {
+ m_playsel->leave_all();
+ m_playsel->enter(play_collection);
+ }
+ UseNormalSelection ();
+ unsigned int posi = selection()->gotoPosition();
+ LoadExternalCommands(); // before AddMenu()
+ m_root = new mgTree;
+ m_root->TreeRedAction = mgActions(nmain.getuint("TreeRedAction"));
+ m_root->TreeGreenAction = mgActions(nmain.getuint("TreeGreenAction"));
+ m_root->TreeYellowAction = mgActions(nmain.getuint("TreeYellowAction"));
+ m_root->CollRedAction = mgActions(nmain.getuint("CollRedAction"));
+ m_root->CollGreenAction = mgActions(nmain.getuint("CollGreenAction"));
+ m_root->CollYellowAction = mgActions(nmain.getuint("CollYellowAction"));
+ AddMenu (m_root,posi);
+
+ //SetCurrent (Get (posi));
+
+ forcerefresh = false;
+}
+
+void
+mgMainMenu::AddOrder()
+{
+ orders.push_back(new mgOrder);
+}
+
+void
+mgMainMenu::DeleteOrder()
+{
+ mgOrder *o = orders[Current()];
+ delete o;
+ orders.erase(orders.begin()+Current());
+}
+
+void
+mgMainMenu::LoadOrders(mgValmap& nv)
+{
+ for (unsigned int idx=0;idx<1000;idx++)
+ {
+ char b[10];
+ sprintf(b,"order%u",idx);
+ mgOrder *o = new mgOrder(nv,b);
+ if (o->size()==0)
+ {
+ delete o;
+ break;
+ }
+ orders.push_back(o);
+ }
+ m_current_order = nv.getuint("CurrentOrder");
+ if (m_current_order >= orders.size())
+ m_current_order=0;
+ if (orders.size()>0) return;
+
+ nv.put("order0.Keys.0.Type",keyArtist);
+ nv.put("order0.Keys.1.Type",keyAlbum);
+ nv.put("order0.Keys.2.Type",keyTrack);
+
+ nv.put("order1.Keys.0.Type",keyAlbum);
+ nv.put("order1.Keys.1.Type",keyTrack);
+
+ nv.put("order2.Keys.0.Type",keyGenres);
+ nv.put("order2.Keys.1.Type",keyArtist);
+ nv.put("order2.Keys.2.Type",keyAlbum);
+ nv.put("order2.Keys.3.Type",keyTrack);
+
+ nv.put("order3.Keys.0.Type",keyArtist);
+ nv.put("order3.Keys.1.Type",keyTrack);
+
+ nv.put("CurrentOrder",0);
+ LoadOrders(nv);
+}
+
+void
+mgMainMenu::LoadExternalCommands()
+{
+// Read commands for collections in etc. /video/muggle/playlist_commands.conf
+ external_commands = new cCommands ();
+
+#if VDRVERSNUM >= 10318
+ cString cmd_file = AddDirectory (cPlugin::ConfigDirectory ("muggle"),
+ "playlist_commands.conf");
+ mgDebug (1, "mgMuggle::Start: 10318 Looking for file %s", *cmd_file);
+ bool have_cmd_file = external_commands->Load (*cmd_file);
+#else
+ const char *
+ cmd_file = (const char *) AddDirectory (cPlugin::ConfigDirectory ("muggle"),
+ "playlist_commands.conf");
+ mgDebug (1, "mgMuggle::Start: 10317 Looking for file %s", cmd_file);
+ bool have_cmd_file = external_commands->Load ((const char *) cmd_file);
+#endif
+
+ if (!have_cmd_file)
+ {
+ delete external_commands;
+ external_commands = NULL;
+ }
+}
+
+mgMainMenu::~mgMainMenu()
+{
+ delete m_treesel;
+ delete m_collectionsel;
+ delete m_playsel;
+ delete m_Status;
+ delete moveselection;
+ delete m_root;
+ delete external_commands;
+ for (unsigned int i=0;i<orders.size();i++)
+ delete orders[i];
+}
+
+void
+mgMainMenu::InitMapFromSetup (mgValmap& nv)
+{
+ // values from setup override saved values
+ nv["Directory"] = cPlugin::ConfigDirectory ("muggle");
+}
+
+void
+mgMenu::AddAction (const mgActions action, mgActions on,const bool hotkey)
+{
+ mgAction *a = GenerateAction(action,on);
+ if (!a) return;
+ const char *mn = a->MenuName();
+ if (strlen(mn)==0)
+ mgError("AddAction(%d):MenuName is empty",int(action));
+ if (hotkey)
+ a->SetText(osd()->hk(mn));
+ else
+ a->SetText(mn);
+ free(const_cast<char*>(mn));
+ osd()->AddItem(a);
+}
+
+
+void
+mgMenu::AddExternalAction(const mgActions action, const char *title)
+{
+ mgAction *a = GenerateAction(action,actNone);
+ if (!a) return;
+ a->SetText(osd()->hk(title));
+ osd()->AddItem(a);
+}
+
+void
+mgMainMenu::AddOrderActions(mgMenu* m)
+{
+ for (unsigned int idx=0;idx<orders.size();idx++)
+ {
+ mgOrder *o = orders[idx];
+ if (!o)
+ mgError("AddOrderAction:orders[%u] is 0",idx);
+ mgAction *a = m->GenerateAction(actOrder,actNone);
+ assert(a);
+ a->SetText(hk(o->Name().c_str()));
+ AddItem(a);
+ }
+}
+
+void
+mgMenu::AddSelectionItems (mgSelection *sel,mgActions act)
+{
+ for (unsigned int i = 0; i < sel->items.size (); i++)
+ {
+ mgAction *a = GenerateAction(act, actEntry);
+ if (!a) continue;
+ const char *name = a->MenuName(i+1,sel->items[i]);
+ // add incremental filter here
+#if 0
+ // example:
+ if (name[0]!='C')
+ continue;
+#endif
+ // adapt newposition since it refers to position in mgSelection:
+ if ((signed int)i==osd()->newposition)
+ osd()->newposition = osd()->Count();
+ a->SetText(name,false);
+ a->setHandle(i);
+ osd()->AddItem(a);
+ }
+ if (osd()->ShowingCollections ())
+ {
+ mgAction *a = GenerateAction(actCreateCollection,actNone);
+ if (a)
+ {
+ a->SetText(a->MenuName(),false);
+ osd()->AddItem(a);
+ }
+ }
+}
+
+
+const char*
+mgMenu::HKey(const mgActions act,mgActions on)
+{
+ const char* result = NULL;
+ mgAction *a = GenerateAction(act,on);
+ if (a)
+ {
+ result = a->ButtonName();
+ delete a;
+ }
+ return result;
+}
+
+void
+mgMenu::SetHelpKeys(mgActions on)
+{
+ mgActions r,g,y,b;
+ if (osd()->UsingCollection)
+ {
+ r = CollRedAction;
+ g = CollGreenAction;
+ y = CollYellowAction;
+ b = CollBlueAction;
+ }
+ else
+ {
+ r = TreeRedAction;
+ g = TreeGreenAction;
+ y = TreeYellowAction;
+ b = TreeBlueAction;
+ }
+ osd()->SetHelpKeys(HKey(r,on),
+ HKey(g,on),
+ HKey(y,on),
+ HKey(b,on));
+}
+
+
+void
+mgMenu::InitOsd (const char *title,const bool hashotkeys)
+{
+ osd ()->InitOsd (title,hashotkeys);
+ SetHelpKeys(); // Default, will be overridden by the single items
+}
+
+
+void
+mgMainMenu::InitOsd (const char *title,const bool hashotkeys)
+{
+ Clear ();
+ SetTitle (title);
+ if (hashotkeys) SetHasHotkeys ();
+}
+
+void
+mgMainMenu::AddItem(mgAction *a)
+{
+ cOsdItem *c = dynamic_cast<cOsdItem*>(a);
+ if (!c)
+ mgError("AddItem with non cOsdItem");
+ Add(c);
+}
+
+void
+mgSubmenu::BuildOsd ()
+{
+ static char b[100];
+ snprintf(b,99,tr("Commands:%s"),trim(osd()->selection()->getCurrentValue()).c_str());
+ mgActions on = osd()->CurrentType();
+ InitOsd (b);
+ if (!osd ()->Parent ())
+ return;
+ AddAction(actInstantPlay,on);
+ AddAction(actAddThisToCollection,on);
+ AddAction(actAddThisToDefaultCollection,on);
+ AddAction(actSetDefaultCollection,on);
+ AddAction(actRemoveThisFromCollection,on);
+ AddAction(actToggleSelection,on);
+ AddAction(actDeleteCollection,on);
+ AddAction(actClearCollection,on);
+ AddAction(actChooseOrder,on);
+ AddAction(actExportTracklist,on);
+ cCommand *command;
+ if (osd()->external_commands)
+ {
+ int idx=0;
+ while ((command = osd ()->external_commands->Get (idx)) != NULL)
+ {
+ if (idx>actExternalHigh-actExternal0)
+ {
+ mgWarning("Too many external commands");
+ break;
+ }
+ AddExternalAction (mgActions(idx+int(actExternal0)),command->Title());
+ idx++;
+ }
+ }
+ TreeRedAction = actSetButton;
+ TreeGreenAction = actSetButton;
+ TreeYellowAction = actSetButton;
+ CollRedAction = actSetButton;
+ CollGreenAction = actSetButton;
+ CollYellowAction = actSetButton;
+}
+
+mgActions
+mgMainMenu::CurrentType()
+{
+ mgActions result = actNone;
+ cOsdItem* c = Get(Current());
+ if (c)
+ {
+ mgAction *a = dynamic_cast<mgAction*>(c);
+ if (!a)
+ mgError("Found an OSD item which is not mgAction:%s",c->Text());
+ result = a->Type();
+ }
+ return result;
+}
+
+eOSState
+mgMenu::ExecuteButton(eKeys key)
+{
+ mgActions on = osd()->CurrentType();
+ mgActions action = actNone;
+ if (osd()->UsingCollection)
+ switch (key)
+ {
+ case kRed: action = CollRedAction; break;
+ case kGreen: action = CollGreenAction; break;
+ case kYellow: action = CollYellowAction; break;
+ case kBlue: action = CollBlueAction; break;
+ default: break;
+ }
+ else
+ switch (key)
+ {
+ case kRed: action = TreeRedAction; break;
+ case kGreen: action = TreeGreenAction; break;
+ case kYellow: action = TreeYellowAction; break;
+ case kBlue: action = TreeBlueAction; break;
+ default: break;
+ }
+ return ExecuteAction(action,on);
+}
+
+mgTree::mgTree()
+{
+ TreeBlueAction = actShowCommands;
+ CollBlueAction = actShowCommands;
+}
+
+eOSState
+mgMenu::Process (eKeys key)
+{
+ return ExecuteButton(key);
+}
+
+eOSState
+mgTree::Process (eKeys key)
+{
+ eOSState result = osUnknown;
+ if (key!=kNone)
+ mgDebug(1,"mgTree::Process(%d)",key);
+ switch (key)
+ {
+ case k0:mgDebug(1,"ich bin k0");break;
+ default: result = mgMenu::Process(key);
+ }
+ return result;
+}
+
+void
+mgTree::BuildOsd ()
+{
+ InitOsd (selection ()->getListname ().c_str (), false);
+ AddSelectionItems (selection());
+}
+
+void
+mgMainMenu::Message1(const char *msg, const char *arg1)
+{
+ if (strlen(msg)==0) return;
+ asprintf (&m_message, tr (msg), arg1);
+}
+
+
+eOSState mgMainMenu::ProcessKey (eKeys key)
+{
+ eOSState result = osContinue;
+
+ if (Menus.size()<1)
+ mgError("mgMainMenu::ProcessKey: Menus is empty");
+
+ mgPlayerControl * c = PlayerControl ();
+ if (c)
+ {
+ if (!c->Active ())
+ {
+ c->Shutdown ();
+ if (instant_playing && queue_playing) {
+ PlayQueue();
+ }
+ else
+ {
+ instant_playing = false;
+ queue_playing = false;
+ }
+ }
+ else
+ {
+ switch (key)
+ {
+ case kPause:
+ c->Pause ();
+ break;
+ case kStop:
+ if (instant_playing && queue_playing) {
+ PlayQueue();
+ }
+ else
+ {
+ queue_playing = false;
+ c->Stop ();
+ }
+ break;
+ case kChanUp:
+ c->Forward ();
+ break;
+ case kChanDn:
+ c->Backward ();
+ break;
+ default:
+ goto otherkeys;
+ }
+ goto pr_exit;
+ }
+ }
+ else
+ if (key==kPlay) {
+ PlayQueue();
+ goto pr_exit;
+ }
+otherkeys:
+ newmenu = Menus.back(); // Default: Stay in current menu
+ newposition = -1;
+
+ {
+ mgMenu * oldmenu = newmenu;
+
+// item specific key logic:
+ result = cOsdMenu::ProcessKey (key);
+
+// mgMenu specific key logic:
+ if (result == osUnknown)
+ result = oldmenu->Process (key);
+ }
+// catch osBack for empty OSD lists . This should only happen for playlistitems
+// (because if the list was empty, no mgActions::ProcessKey was ever called)
+ if (result == osBack)
+ {
+ // do as if there was an entry
+ mgAction *a = Menus.back()->GenerateAction(actEntry,actEntry);
+ if (a)
+ {
+ result = a->Back();
+ delete a;
+ }
+ }
+
+// do nothing for unknown keys:
+ if (result == osUnknown)
+ goto pr_exit;
+
+// change OSD menu as requested:
+ if (newmenu == NULL)
+ {
+ if (Menus.size () > 1)
+ {
+ CloseMenu();
+ forcerefresh = true;
+ }
+ else
+ {
+ result = osBack; // game over
+ goto pr_exit;
+ }
+ }
+ else if (newmenu != Menus.back ())
+ AddMenu (newmenu,newposition);
+
+ if (UsingCollection)
+ forcerefresh |= m_collectionsel->cacheIsEmpty();
+ else
+ forcerefresh |= m_treesel->cacheIsEmpty();
+
+ forcerefresh |= (newposition>=0);
+
+ if (forcerefresh)
+ {
+ forcerefresh = false;
+ if (newposition<0)
+ newposition = selection()->gotoPosition();
+ Menus.back ()->Display ();
+ }
+pr_exit:
+ showMessage();
+ return result;
+}
+
+void
+mgMainMenu::CloseMenu()
+{
+ mgMenu* m = Menus.back();
+ if (newposition==-1) newposition = m->getParentIndex();
+ Menus.pop_back ();
+ delete m;
+}
+
+void
+mgMainMenu::showMessage()
+{
+ if (m_message)
+ {
+ showmessage(m_message);
+ free(m_message);
+ m_message = NULL;
+ }
+}
+
+void
+showmessage(const char * msg,int duration)
+{
+#if VDRVERSNUM >= 10307
+ Skins.Message (mtInfo, msg,duration);
+ Skins.Flush ();
+#else
+ Interface->Status (msg);
+ Interface->Flush ();
+#endif
+}
+
+void
+showimportcount(unsigned int count,bool final=false)
+{
+ char b[100];
+ if (final)
+ {
+ sprintf(b,tr("Import done:Imported %d tracks"),count);
+ assert(strlen(b)<100);
+ showmessage(b,1);
+ }
+ else
+ {
+ sprintf(b,tr("Imported %d tracks..."),count);
+ assert(strlen(b)<100);
+ showmessage(b);
+ }
+}
+
+void
+mgMainMenu::AddMenu (mgMenu * m,unsigned int position)
+{
+ Menus.push_back (m);
+ m->setosd (this);
+ m->setParentIndex(Current());
+ if (Get(Current()))
+ m->setParentName(Get(Current())->Text());
+ newposition = position;
+ m->Display ();
+}
+
+void
+mgMenu::setosd(mgMainMenu *osd)
+{
+ m_osd = osd;
+ m_prevUsingCollection = osd->UsingCollection;
+}
+
+mgSubmenu::mgSubmenu()
+{
+ TreeBlueAction = actShowList;
+ CollBlueAction = actShowList;
+}
+
+
+void
+mgMenuOrders::BuildOsd ()
+{
+ TreeRedAction = actEditOrder;
+ TreeGreenAction = actCreateOrder;
+ TreeYellowAction = actDeleteOrder;
+ InitOsd (tr ("Select an order"));
+ osd()->AddOrderActions(this);
+}
+
+mgMenuOrder::mgMenuOrder()
+{
+ m_order=0;
+}
+
+mgMenuOrder::~mgMenuOrder()
+{
+ if (m_order)
+ delete m_order;
+}
+
+void
+mgMenuOrder::BuildOsd ()
+{
+ if (!m_order)
+ {
+ m_order = new mgOrder;
+ *m_order = *(osd()->getOrder(getParentIndex()));
+ }
+ InitOsd (m_order->Name().c_str());
+ m_keytypes.clear();
+ m_keytypes.reserve(mgKeyTypesNr+1);
+ m_keynames.clear();
+ m_keynames.reserve(50);
+ m_orderbycount = m_order->getOrderByCount();
+ mgDebug(1,"m_orderbycount wird %d",m_orderbycount);
+ for (unsigned int i=0;i<m_order->size();i++)
+ {
+ unsigned int kt;
+ m_keynames.push_back(selection()->choices(m_order,i,&kt));
+ m_keytypes.push_back(kt);
+ char buf[20];
+ sprintf(buf,tr("Key %d"),i+1);
+ mgAction *a = actGenerateKeyItem(buf,(int*)&m_keytypes[i],m_keynames[i].size(),&m_keynames[i][0]);
+ a->SetMenu(this);
+ osd()->AddItem(a);
+ }
+ mgAction *a = actGenerateBoolItem(tr("Sort by count"),&m_orderbycount);
+ a->SetMenu(this);
+ osd()->AddItem(a);
+}
+
+bool
+mgMenuOrder::ChangeOrder(eKeys key)
+{
+ vector <mgKeyTypes> newtypes;
+ newtypes.clear();
+ for (unsigned int i=0; i<m_keytypes.size();i++)
+ newtypes.push_back(ktValue(m_keynames[i][m_keytypes[i]]));
+ mgOrder n = mgOrder(newtypes);
+ n.setOrderByCount(m_orderbycount);
+ mgDebug(1,"m_orderbycount %d nach n",m_orderbycount);
+ bool result = !(n == *m_order);
+ *m_order = n;
+ if (result)
+ {
+ osd()->forcerefresh = true;
+ int np = osd()->Current();
+ if (key==kUp && np) np--;
+ if (key==kDown) np++;
+ osd()->newposition = np;
+ }
+ return result;
+}
+
+void
+mgMenuOrder::SaveOrder()
+{
+ *(osd()->getOrder(getParentIndex())) = *m_order;
+ osd()->SaveState();
+}
+
+
+mgTreeCollSelector::mgTreeCollSelector()
+{
+ TreeBlueAction = actShowList;
+ CollBlueAction = actShowList;
+}
+
+mgTreeCollSelector::~mgTreeCollSelector()
+{
+ osd()->UsingCollection = m_prevUsingCollection;
+}
+
+void
+mgTreeCollSelector::BuildOsd ()
+{
+ osd()->UsingCollection = true;
+ mgSelection *coll = osd()->collselection();
+ InitOsd (m_title.c_str());
+ coll->leave_all();
+ coll->setPosition(osd()->default_collection);
+ AddSelectionItems (coll,coll_action());
+ osd()->newposition = coll->gotoPosition();
+ cOsdItem *c = osd()->Get(osd()->newposition);
+ mgAction *a = dynamic_cast<mgAction *>(c);
+ a->IgnoreNextEvent = true;
+}
+
+mgTreeAddToCollSelector::mgTreeAddToCollSelector(string title)
+{
+ m_title = title;
+}
+
+mgTreeRemoveFromCollSelector::mgTreeRemoveFromCollSelector(string title)
+{
+ m_title = title;
+}
+
+void
+mgMainMenu::DisplayGoto ()
+{
+ if (newposition >= 0)
+ {
+ if ((int)newposition>=Count())
+ newposition = Count() -1;
+ SetCurrent (Get (newposition));
+ RefreshCurrent ();
+ }
+ Display ();
+}
+
+
+void
+mgMenu::Display ()
+{
+ BuildOsd ();
+ osd ()->DisplayGoto ();
+}
+
diff --git a/muggle-plugin/vdr_menu.h b/muggle-plugin/vdr_menu.h
new file mode 100644
index 0000000..ede022b
--- /dev/null
+++ b/muggle-plugin/vdr_menu.h
@@ -0,0 +1,413 @@
+/*!
+ * \file vdr_menu.h
+ * \brief Implements menu handling for broswing media libraries within VDR
+ *
+ * \version $Revision: 1.13 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner, Wolfgang Rohdewald
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ */
+
+#ifndef _VDR_MENU_H
+#define _VDR_MENU_H
+
+#include <string>
+#include <list>
+#include <vector>
+
+#include <osd.h>
+#include <plugin.h>
+#include <status.h>
+#include "vdr_actions.h"
+
+#include "vdr_player.h"
+
+using namespace std;
+
+//! \brief play a selection, aborting what is currently played
+//! \param select if true, play only what the current position selects
+void Play(mgSelection *sel,const bool select=false);
+
+void showmessage(const char *msg,int duration=2);
+void showimportcount(unsigned int count);
+
+class cCommands;
+
+class mgSelection;
+class mgMenu;
+class mgMainMenu;
+
+//! \brief if a player is running, return it
+mgPlayerControl * PlayerControl ();
+
+//! \brief callback class, monitors state changes in vdr
+class mgStatus : public cStatus
+{
+ private:
+ //! \brief the mgMainMenu that wants to be notified
+ mgMainMenu *main;
+ public:
+ //! \brief default constructor
+ mgStatus(mgMainMenu* m) { main = m;}
+ protected:
+ //! \brief the event we want to know about
+ virtual void OsdCurrentItem(const char *Text);
+};
+
+
+/*!
+ * \brief the muggle main OSD
+ */
+
+class mgMainMenu:public cOsdMenu
+{
+ private:
+ mgSelection *m_treesel;
+ mgSelection *m_playsel;
+ mgSelection *m_collectionsel;
+ char *m_message;
+ void showMessage();
+ void LoadExternalCommands();
+ vector<mgOrder*> orders;
+ unsigned int m_current_order;
+ void DumpOrders(mgValmap& nv);
+ void LoadOrders(mgValmap& nv);
+ mgMenu *m_root;
+ bool m_save_warned;
+ public:
+ void AddOrder();
+ void DeleteOrder();
+ void AddOrderActions(mgMenu *m);
+ unsigned int getCurrentOrder() { return m_current_order; }
+ mgOrder* getOrder(unsigned int idx);
+ void setOrder(mgSelection *sel, unsigned int idx);
+
+ mgSelection *moveselection;
+ mgActions CurrentType();
+
+ //! \brief syntactic sugar: expose the protected cOsdMenu::SetHelp
+ void SetHelpKeys(const char *Red,const char *Green, const char *Yellow, const char *Blue) { SetHelp(Red,Green,Yellow,Blue); }
+
+ //! \brief callback object, lets vdr notify us about OSD changes
+ mgStatus *m_Status;
+
+ //! \brief play the play selection, abort ongoing instant play
+ void PlayQueue();
+
+ //! \brief instant play the selection, abort ongoing queue playing
+ void PlayInstant(const bool select=false);
+
+ //! \brief true if we are browsing m_collectionsel
+ bool UsingCollection;
+
+ //! \brief the different menus, the last one is active
+ vector < mgMenu * >Menus;
+
+ //! \brief true if an item from the "playing" selection is being played
+ bool queue_playing;
+
+ //! \brief true if an item is being instant played
+ bool instant_playing;
+
+ //! \brief parent menu if any
+ mgMenu * Parent ();
+
+ //! \brief default constructor
+ mgMainMenu ();
+
+ //! \brief default destructor
+ ~mgMainMenu ();
+
+ //! \brief save the entire muggle state
+ void SaveState();
+
+ //! \brief adds a new mgMenu to the stack
+ void AddMenu (mgMenu * m,unsigned int position=0);
+
+ //! \brief initializes using values from nv
+ void InitMapFromSetup (mgValmap& nv);
+
+ //! \brief main entry point, called from vdr
+ eOSState ProcessKey (eKeys Key);
+
+ //! \brief from now on use the normal selection
+ void UseNormalSelection ()
+ {
+ UsingCollection= false;
+ }
+
+ //! \brief from now on use the collection selection
+ void UseCollectionSelection ()
+ {
+ UsingCollection= true;
+ }
+
+ //! \brief this is the collection things will be added to
+ string default_collection;
+
+/*! \brief this is the "now playing" collection translated in
+ * the current language. When changing the OSD language, this
+ * collection will NOT be renamed in the data base, but a new
+ * empty collection will be started. The collection for the
+ * previous language will stay, so the user can copy from the
+ * old one to the new one.
+ */
+ string play_collection;
+
+/*! \brief selects a certain line on the OSD and displays the OSD
+ */
+ void DisplayGoto ();
+
+ //! \brief external commands
+ cCommands *external_commands;
+
+ //! \brief Actions can set newmenu which will then be displayed next
+ mgMenu *newmenu;
+
+ //! \brief Actions can set newstate which will then be returned to main vdr
+ eOSState newstate;
+
+ //! \brief Actions can set forcerefresh. This will force a redisplay of the OSD
+ bool forcerefresh;
+
+ //! \brief show a message. Can be called by actions. It will
+ // only be shown at the end of the next mgMainMenu::ProcessKey
+ // because that might do forcerefresh which overwrites the message
+ void Message (const char *msg) { m_message = strdup(msg); }
+ void Message1 (const char *msg, const char *arg1);
+ void Message1 (const char *msg, string arg1) { Message1(msg,arg1.c_str()); }
+
+ //! \brief Actions can request a new position. -1 means none wanted
+ int newposition;
+
+ //! \brief clears the screen, sets a title and the hotkey flag
+ void InitOsd (const char *title,const bool hashotkeys);
+
+#if VDRVERSNUM >= 10307
+ //! \brief expose the protected DisplayMenu() from cOsdMenu
+ cSkinDisplayMenu *DisplayMenu(void)
+ {
+ return cOsdMenu::DisplayMenu();
+ }
+#endif
+
+ //! \brief expose the protected cOsdMenu::hk()
+ const char *hk (const char *s)
+ {
+ return cOsdMenu::hk (s);
+ }
+
+ //! \brief the current selection
+ mgSelection* selection ()
+ {
+ if (UsingCollection)
+ return m_collectionsel;
+ else
+ return m_treesel;
+ }
+
+ //! \brief the collection selection
+ mgSelection* collselection()
+ {
+ return m_collectionsel;
+ }
+
+//! \brief the "now playing" selection
+ mgSelection* playselection ()
+ {
+ return m_playsel;
+ }
+
+//! \brief true if the cursor is placed in the collection list
+ bool ShowingCollections();
+
+//! \brief true if the cursor is placed on the default collection
+ bool DefaultCollectionSelected();
+
+//! \brief true if the cursor is placed in the default collection
+ bool CollectionEntered(string name);
+
+ void AddItem(mgAction *a);
+
+ void CollectionChanged(string name);
+
+ void CloseMenu();
+};
+
+//! \brief a generic muggle menu
+class mgMenu
+{
+ private:
+ mgMainMenu* m_osd;
+ const char *HKey(const mgActions act,mgActions on);
+ protected:
+ bool m_prevUsingCollection;
+ eOSState ExecuteButton(eKeys key);
+//! \brief adds the wanted action to the OSD menu
+// \param hotkey if true, add this as a hotkey
+ void AddAction(const mgActions action, mgActions on = mgActions(0),const bool hotkey=true);
+
+ //! \brief add an external action, always with hotkey
+ void AddExternalAction(const mgActions action, const char *title);
+
+//! \brief adds entries for all selected data base items to the OSD menu.
+// If this is the list of collections, appends a command for collection
+// creation.
+ void AddSelectionItems (mgSelection *sel,mgActions act = actEntry);
+ //! \brief the name of the blue button depends of where we are
+ int m_parent_index;
+ string m_parent_name;
+ public:
+ /*! sets the correct help keys.
+ * \todo without data from mysql, no key is shown,
+ * not even yellow or blue
+ */
+ void SetHelpKeys(mgActions on = mgActions(0));
+//! \brief generates an object for the wanted action
+ mgAction* GenerateAction(const mgActions action,mgActions on);
+
+//! \brief executes the wanted action
+ eOSState ExecuteAction (const mgActions action, const mgActions on);
+
+//! \brief sets the pointer to the owning mgMainMenu
+ void setosd (mgMainMenu* osd);
+
+ void setParentIndex(int idx) { m_parent_index = idx; }
+ int getParentIndex() { return m_parent_index; }
+ void setParentName(string name) { m_parent_name = name; }
+ string getParentName() { return m_parent_name; }
+
+//! \brief the pointer to the owning mgMainMenu
+ mgMainMenu* osd ()
+ {
+ return m_osd;
+ }
+
+//! \brief the currently active selection of the owning mgMainMenu
+ mgSelection* selection ()
+ {
+ return osd ()->selection ();
+ }
+//! \brief the playselection of the owning mgMainMenu
+ mgSelection* playselection ()
+ {
+ return osd ()->playselection ();
+ }
+
+ mgMenu ();
+
+ virtual ~ mgMenu ()
+ {
+ }
+
+//! \brief clears the screen, sets a title and the hotkey flag
+ void InitOsd (const char *title,const bool hashotkeys=true);
+
+//! \brief display OSD and go to osd()->newposition
+ void Display ();
+
+//! \brief BuildOsd() should be abstract but then we cannot compile
+ virtual void BuildOsd ()
+ {
+ }
+
+/*! \brief Process() should be abstract but then we cannot compile.
+ * \return Process may decide that we want another screen to be displayed.
+ * If the mgMenu* returned is not "this", the caller will use the return
+ * value for a new display. If NULL is returned, the caller will display
+ * the previous menu.
+ */
+ virtual eOSState Process (eKeys Key);
+
+//! \brief the ID of the action defined by the red button.
+ mgActions TreeRedAction;
+ mgActions CollRedAction;
+
+//! \brief the ID of the action defined by the green button.
+ mgActions TreeGreenAction;
+ mgActions CollGreenAction;
+
+//! \brief the action defined by the yellow button.
+ mgActions TreeYellowAction;
+ mgActions CollYellowAction;
+
+//! \brief the action defined by the blue button.
+ mgActions TreeBlueAction;
+ mgActions CollBlueAction;
+};
+
+//! \brief an mgMenu class for navigating through the data base
+class mgTree:public mgMenu
+{
+ public:
+ mgTree();
+ virtual eOSState Process (eKeys Key);
+ protected:
+ void BuildOsd ();
+};
+
+//! \brief an mgMenu class for submenus
+class mgSubmenu:public mgMenu
+{
+ public:
+ mgSubmenu::mgSubmenu();
+ protected:
+ void BuildOsd ();
+};
+
+//! \brief an mgMenu class for selecting an order
+class mgMenuOrders:public mgMenu
+{
+ protected:
+ void BuildOsd ();
+};
+
+class mgMenuOrder : public mgMenu
+{
+ public:
+ mgMenuOrder();
+ ~mgMenuOrder();
+ bool ChangeOrder(eKeys key);
+ void SaveOrder();
+ protected:
+ void BuildOsd ();
+ private:
+ void AddKeyActions(mgMenu *m,mgOrder *o);
+ mgOrder * m_order;
+ int m_orderbycount;
+ vector<int> m_keytypes;
+ vector < vector <const char*> > m_keynames;
+};
+
+//! \brief an mgMenu class for selecting a collection
+class mgTreeCollSelector:public mgMenu
+{
+ public:
+ mgTreeCollSelector();
+ ~mgTreeCollSelector();
+ protected:
+ void BuildOsd ();
+ virtual mgActions coll_action() = 0;
+ string m_title;
+};
+
+class mgTreeAddToCollSelector:public mgTreeCollSelector
+{
+ public:
+ mgTreeAddToCollSelector(string title);
+ protected:
+ virtual mgActions coll_action() { return actAddCollEntry; }
+};
+
+//! \brief an mgMenu class for selecting a collection
+class mgTreeRemoveFromCollSelector:public mgTreeCollSelector
+{
+ public:
+ mgTreeRemoveFromCollSelector(string title);
+ protected:
+ virtual mgActions coll_action() { return actRemoveCollEntry; }
+};
+
+#endif
diff --git a/muggle-plugin/vdr_network.h b/muggle-plugin/vdr_network.h
new file mode 100644
index 0000000..4725d2a
--- /dev/null
+++ b/muggle-plugin/vdr_network.h
@@ -0,0 +1,47 @@
+/*
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___NETWORK_H
+#define ___NETWORK_H
+
+#include <thread.h>
+#include <ringbuffer.h>
+#include <config.h>
+
+class cRingBufferLinear;
+
+// ----------------------------------------------------------------
+
+class mgNet:public cRingBufferLinear, cThread
+{
+ private:
+ int m_fd;
+ bool m_connected, m_netup;
+ int m_deferedErrno;
+ int m_rwTimeout, m_conTimeout;
+ unsigned char m_lineBuff[4096];
+ int m_count;
+//
+ void close (void);
+ int ringRead (unsigned char *dest, int len);
+ void copyFromBuff (unsigned char *dest, int n);
+ protected:
+ virtual void action (void);
+ public:
+ mgNet (int size, int ConTimeoutMs, int RwTimeoutMs);
+ ~mgNet ();
+ bool connect (const char *hostname, const int port);
+ void disconnect (void);
+ bool connected (void)
+ {
+ return m_connected;
+ }
+ int gets (char *dest, int len);
+ int puts (char *dest);
+ int read (unsigned char *dest, int len);
+ int write (unsigned char *dest, int len);
+};
+#endif //___NETWORK_H
diff --git a/muggle-plugin/vdr_player.c b/muggle-plugin/vdr_player.c
new file mode 100644
index 0000000..01e7337
--- /dev/null
+++ b/muggle-plugin/vdr_player.c
@@ -0,0 +1,1808 @@
+/*!
+ * \file vdr_player.c
+ * \brief A generic PCM player for a VDR media plugin (muggle)
+ *
+ * \version $Revision: 1.7 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner, Wolfgang Rohdewald
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <math.h>
+
+#include <string>
+#include <iostream>
+
+#include <mad.h>
+
+#include <player.h>
+#include <device.h>
+#include <thread.h>
+#include <ringbuffer.h>
+#include <tools.h>
+#include <recording.h>
+#include <status.h>
+
+#include "vdr_player.h"
+#include "vdr_decoder.h"
+#include "vdr_config.h"
+#include "vdr_setup.h"
+#include "i18n.h"
+
+#include "mg_tools.h"
+
+// ----------------------------------------------------------------
+
+// TODO: check for use of constants
+#define OUT_BITS 16 // output 16 bit samples to DVB driver
+#define OUT_FACT (OUT_BITS/8*2) // output factor is 16 bit & 2 channels -> 4 bytes
+
+// cResample
+#define MAX_NSAMPLES (1152*7) // max. buffer for resampled frame
+
+// cNormalize
+#define MAX_GAIN 3.0 // max. allowed gain
+#define LIM_ACC 12 // bit, accuracy for lookup table
+ // max. value covered by lookup table
+#define F_LIM_MAX (mad_fixed_t)((1<<(MAD_F_FRACBITS+2))-1)
+#define LIM_SHIFT (MAD_F_FRACBITS-LIM_ACC) // shift value for table lookup
+#define F_LIM_JMP (mad_fixed_t)(1<<LIM_SHIFT) // lookup table jump between values
+
+// cLevel
+#define POW_WIN 100 // window width for smoothing power values
+#define EPSILON 0.00000000001 // anything less than EPSILON is considered zero
+
+// cMP3Player
+#define MAX_FRAMESIZE 2048 // max. frame size allowed for DVB driver
+#define HDR_SIZE 9
+#define LPCM_SIZE 7
+#define LEN_CORR 3
+ // play time to fill buffers before speed check starts (ms)
+#define SPEEDCHECKSTART ((MP3BUFSIZE*1000/32000/2/2)+1000)
+#define SPEEDCHECKTIME 3000 // play time for speed check (ms)
+
+/*
+struct LPCMHeader { int id:8; // id
+ int frame_count:8; // number of frames
+ int access_ptr:16; // first acces unit pointer, i.e. start of audio frame
+ bool emphasis:1; // audio emphasis on-off
+ bool mute:1; // audio mute on-off
+ bool reserved:1; // reserved
+ int frame_number:5; // audio frame number
+ int quant_wlen:2; // quantization word length
+ int sample_freq:2; // audio sampling frequency (48khz=0, 96khz=1, 44,1khz=2, 32khz=3)
+ bool reserved2:1; // reserved
+int chan_count:3; // number of audio channels - 1 (e.g. stereo = 1)
+int dyn_range_ctrl:8; // dynamic range control (0x80 if off)
+};
+*/
+
+struct LPCMFrame
+{
+ unsigned char PES[HDR_SIZE];
+ unsigned char LPCM[LPCM_SIZE];
+ unsigned char Data[MAX_FRAMESIZE - HDR_SIZE - LPCM_SIZE];
+};
+
+#include "vdr_sound.c"
+
+// --- mgPCMPlayer ----------------------------------------------------------
+
+/*!
+ * \brief a generic PCM player class
+ *
+ * this class implements a state machine that obtains decoded data from a generic data
+ * and moves it to the DVB device. It inherits from cPlayer in order to be hooked into
+ * VDR as a player and inherits from cThread in order to implement a separate thread
+ * for the decoding process.
+ */
+class mgPCMPlayer:public cPlayer, cThread
+{
+ private:
+
+//! \brief indicates, whether the player is currently active
+ bool m_active;
+
+//! \brief indicates, whether the player has been started
+ bool m_started;
+
+//! \brief a buffer for decoded sound
+ cRingBufferFrame *m_ringbuffer;
+
+//! \brief a mutex for the playmode
+ cMutex m_playmode_mutex;
+
+//! \brief a condition to signal playmode changes
+ cCondVar m_playmode_cond;
+
+//! \brief the current playlist
+ mgSelection *m_playlist;
+
+//! \brief the currently played or to be played item
+ mgContentItem *m_current;
+
+//! \brief the currently playing item
+ mgContentItem *m_playing;
+
+//! \brief the decoder responsible for the currently playing item
+ mgDecoder *m_decoder;
+
+ cFrame *m_rframe, *m_pframe;
+
+ enum ePlayMode
+ {
+ pmPlay,
+ pmStopped,
+ pmPaused,
+ pmStartup
+ };
+ ePlayMode m_playmode;
+
+ enum eState
+ {
+ msStart, msStop,
+ msDecode, msNormalize,
+ msResample, msOutput,
+ msError, msEof, msWait
+ };
+ eState m_state;
+
+ bool levelgood;
+ unsigned int dvbSampleRate;
+
+//
+ int m_index;
+
+ void Empty ();
+ bool SkipFile (int step=0);
+ void PlayTrack();
+ void StopPlay ();
+
+ void SetPlayMode (ePlayMode mode);
+ void WaitPlayMode (ePlayMode mode, bool inv);
+
+ int skip_direction;
+
+ protected:
+ virtual void Activate (bool On);
+ virtual void Action (void);
+
+ public:
+ mgPCMPlayer (mgSelection * plist);
+ virtual ~ mgPCMPlayer ();
+
+ bool Active ()
+ {
+ return m_active;
+ }
+
+ void Pause ();
+ void Play ();
+ void Forward ();
+ void Backward ();
+
+ void Goto (int Index, bool Still = false);
+ void SkipSeconds (int secs);
+ void ToggleShuffle (void);
+ void ToggleLoop (void);
+
+ virtual bool GetIndex (int &Current, int &Total, bool SnapToIFrame = false);
+// bool GetPlayInfo(cMP3PlayInfo *rm); // LVW
+
+ void ReloadPlaylist ();
+ void NewPlaylist (mgSelection * plist);
+ mgContentItem *getCurrent ()
+ {
+ return m_current;
+ }
+ mgSelection *getPlaylist ()
+ {
+ return m_playlist;
+ }
+};
+
+mgPCMPlayer::mgPCMPlayer (mgSelection * plist):cPlayer (the_setup.
+BackgrMode ? pmAudioOnly :
+pmAudioOnlyBlack)
+{
+ m_playlist = plist;
+
+ m_active = true;
+ m_started = false;
+
+ m_ringbuffer = new cRingBufferFrame (MP3BUFSIZE);
+
+ m_rframe = 0;
+ m_pframe = 0;
+ m_decoder = 0;
+
+ m_playmode = pmStartup;
+ m_state = msStop;
+
+ m_index = 0;
+ m_playing = 0;
+ m_current = 0;
+ skip_direction = 1;
+}
+
+
+mgPCMPlayer::~mgPCMPlayer ()
+{
+ Detach ();
+ delete m_playlist;
+ delete m_current;
+ delete m_ringbuffer;
+}
+
+void
+mgPCMPlayer::PlayTrack()
+{
+ mgContentItem * newcurr = m_playlist->getCurrentTrack ();
+ if (newcurr)
+ {
+ delete m_current;
+ m_current = new mgContentItem(newcurr);
+ }
+ Play ();
+}
+
+void
+mgPCMPlayer::Activate (bool on)
+{
+ MGLOG ("mgPCMPlayer::Activate");
+ if (on)
+ {
+ if (m_playlist && !m_started)
+ {
+ m_playmode = pmStartup;
+ Start ();
+
+ m_started = true;
+ delete m_current;
+ m_current = 0;
+
+ m_playmode_mutex.Lock ();
+ WaitPlayMode (pmStartup, true); // wait for the decoder to become ready
+ m_playmode_mutex.Unlock ();
+
+ Lock ();
+ PlayTrack();
+ Unlock ();
+ }
+ }
+ else if (m_started && m_active)
+ {
+ Lock ();
+ StopPlay ();
+ Unlock ();
+
+ m_active = false;
+ SetPlayMode (pmStartup);
+
+ Cancel (2);
+ }
+}
+
+void
+mgPCMPlayer::ReloadPlaylist()
+{
+ Lock ();
+ m_playlist->clearCache();
+ if (!m_playing)
+ {
+ SkipFile(1);
+ Play ();
+ }
+ Unlock ();
+}
+
+void
+mgPCMPlayer::NewPlaylist (mgSelection * plist)
+{
+ MGLOG ("mgPCMPlayer::NewPlaylist");
+
+ Lock ();
+ StopPlay ();
+
+ delete m_playlist;
+ m_playlist = plist;
+ PlayTrack();
+ Unlock ();
+}
+
+void
+mgPCMPlayer::SetPlayMode (ePlayMode mode)
+{
+ m_playmode_mutex.Lock ();
+ if (mode != m_playmode)
+ {
+ m_playmode = mode;
+ m_playmode_cond.Broadcast ();
+ }
+ m_playmode_mutex.Unlock ();
+}
+
+
+void
+mgPCMPlayer::WaitPlayMode (ePlayMode mode, bool inv)
+{
+// must be called with m_playmode_mutex LOCKED !!!
+
+ while (m_active
+ && ((!inv && mode != m_playmode) || (inv && mode == m_playmode)))
+ {
+ m_playmode_cond.Wait (m_playmode_mutex);
+ }
+}
+
+
+void
+mgPCMPlayer::Action (void)
+{
+ MGLOG ("mgPCMPlayer::Action");
+
+ struct mgDecode *ds = 0;
+ struct mad_pcm *pcm = 0;
+ cResample resample[2];
+ unsigned int nsamples[2];
+ const mad_fixed_t *data[2];
+ cScale scale;
+ cLevel level;
+ cNormalize norm;
+ bool haslevel = false;
+ struct LPCMFrame lpcmFrame;
+ const unsigned char *p = 0;
+ int pc = 0, only48khz = the_setup.Only48kHz;
+ cPoller poll;
+#ifdef DEBUG
+ int beat = 0;
+#endif
+
+ dsyslog ("muggle: player thread started (pid=%d)", getpid ());
+
+ memset (&lpcmFrame, 0, sizeof (lpcmFrame));
+ lpcmFrame.PES[2] = 0x01;
+ lpcmFrame.PES[3] = 0xbd;
+ lpcmFrame.PES[6] = 0x87;
+ lpcmFrame.LPCM[0] = 0xa0; // substream ID
+ lpcmFrame.LPCM[1] = 0xff;
+ lpcmFrame.LPCM[5] = 0x01;
+ lpcmFrame.LPCM[6] = 0x80;
+
+ dvbSampleRate = 48000;
+ m_state = msStop;
+ SetPlayMode (pmStopped);
+
+#if VDRVERSNUM >= 10318
+ cDevice::PrimaryDevice()->SetCurrentAudioTrack(ttAudio);
+#endif
+ while (m_active)
+ {
+#ifdef DEBUG
+ if (time (0) >= beat + 30)
+ {
+ std::
+ cout << "mgPCMPlayer::Action: heartbeat buffer=" << m_ringbuffer->
+ Available () << std::endl << std::flush;
+ scale.Stats ();
+ if (haslevel)
+ norm.Stats ();
+ beat = time (0);
+ }
+#endif
+
+ Lock ();
+
+ if (!m_rframe && m_playmode == pmPlay)
+ {
+ switch (m_state)
+ {
+ case msStart:
+ {
+ m_index = 0;
+ m_playing = m_current;
+
+ if (m_playing)
+ {
+ std::string filename = m_playing->getSourceFile ();
+ if ((m_decoder = mgDecoders::findDecoder (m_playing))
+ && m_decoder->start ())
+ {
+ levelgood = true;
+ haslevel = false;
+
+ level.Init ();
+
+ m_state = msDecode;
+ break;
+ }
+ else
+ {
+ mgWarning("found no decoder for %s",filename.c_str());
+ m_state=msStop; // if loop mode is on and no decoder
+ // for any track is found, we would
+ // otherwise get into an endless loop
+ // not stoppable with the remote.
+ break;
+ }
+ }
+ m_state = msEof;
+ }
+ break;
+ case msDecode:
+ {
+ ds = m_decoder->decode ();
+ switch (ds->status)
+ {
+ case dsPlay:
+ {
+ pcm = ds->pcm;
+ m_index = ds->index / 1000;
+ m_state = msNormalize;
+ }
+ break;
+ case dsSkip:
+ case dsSoftError:
+ {
+// skipping, state unchanged, next decode
+ }
+ break;
+ case dsEof:
+ {
+ m_state = msEof;
+ }
+ break;
+ case dsOK:
+ case dsError:
+ {
+ m_state = msError;
+ }
+ break;
+ }
+ }
+ break;
+ case msNormalize:
+ {
+ if (!haslevel)
+ {
+ if (levelgood)
+ {
+ level.GetPower (pcm);
+ }
+ }
+ else
+ {
+ norm.AddGain (pcm);
+ }
+ m_state = msResample;
+ }
+ break;
+ case msResample:
+ {
+#ifdef DEBUG
+ {
+ static unsigned int oldrate = 0;
+ if (oldrate != pcm->samplerate)
+ {
+ std::
+ cout << "mgPCMPlayer::Action: new input sample rate "
+ << pcm->samplerate << std::endl << std::flush;
+ oldrate = pcm->samplerate;
+ }
+ }
+#endif
+ nsamples[0] = nsamples[1] = pcm->length;
+ data[0] = pcm->samples[0];
+ data[1] = pcm->channels > 1 ? pcm->samples[1] : 0;
+
+ lpcmFrame.LPCM[5] &= 0xcf;
+ dvbSampleRate = 48000;
+ if (!only48khz)
+ {
+ switch (pcm->samplerate)
+ { // If one of the supported frequencies, do it without resampling.
+ case 96000:
+ { // Select a "even" upsampling frequency if possible, too.
+ lpcmFrame.LPCM[5] |= 1 << 4;
+ dvbSampleRate = 96000;
+ }
+ break;
+
+//case 48000: // this is already the default ...
+// lpcmFrame.LPCM[5]|=0<<4;
+// dvbSampleRate=48000;
+// break;
+ case 11025:
+ case 22050:
+ case 44100:
+ {
+ lpcmFrame.LPCM[5] |= 2 << 4;
+ dvbSampleRate = 44100;
+ }
+ break;
+ case 8000:
+ case 16000:
+ case 32000:
+ {
+ lpcmFrame.LPCM[5] |= 3 << 4;
+ dvbSampleRate = 32000;
+ }
+ break;
+ }
+ }
+
+ if (dvbSampleRate != pcm->samplerate)
+ {
+ if (resample[0].
+ SetInputRate (pcm->samplerate, dvbSampleRate))
+ {
+ nsamples[0] =
+ resample[0].ResampleBlock (nsamples[0], data[0]);
+ data[0] = resample[0].Resampled ();
+ }
+ if (data[1]
+ && resample[1].SetInputRate (pcm->samplerate,
+ dvbSampleRate))
+ {
+ nsamples[1] =
+ resample[1].ResampleBlock (nsamples[1], data[1]);
+ data[1] = resample[1].Resampled ();
+ }
+ }
+ m_state = msOutput;
+ }
+ break;
+ case msOutput:
+ {
+ if (nsamples[0] > 0)
+ {
+ unsigned int outlen = scale.ScaleBlock (lpcmFrame.Data,
+ sizeof (lpcmFrame.
+ Data),
+ nsamples[0],
+ data[0],
+ data[1],
+ the_setup.
+ AudioMode ?
+ amDither :
+ amRound);
+ if (outlen)
+ {
+ outlen += sizeof (lpcmFrame.LPCM) + LEN_CORR;
+ lpcmFrame.PES[4] = outlen >> 8;
+ lpcmFrame.PES[5] = outlen;
+ m_rframe = new cFrame ((unsigned char *) &lpcmFrame,
+ outlen +
+ sizeof (lpcmFrame.PES) -
+ LEN_CORR);
+ }
+ }
+ else
+ {
+ m_state = msDecode;
+ }
+ }
+ break;
+ case msError:
+ case msEof:
+ {
+ if (SkipFile ())
+ {
+ m_state = msStart;
+ }
+ else
+ {
+ m_state = msWait;
+ }
+ } // fall through
+ case msStop:
+ {
+ m_playing = 0;
+ if (m_decoder)
+ { // who deletes decoder?
+ m_decoder->stop ();
+ delete m_decoder;
+ m_decoder = 0;
+ }
+
+ levelgood = false;
+
+ scale.Stats ();
+ if (haslevel)
+ {
+ norm.Stats ();
+ }
+ if (m_state == msStop)
+ { // might be unequal in case of fall through from eof/error
+ SetPlayMode (pmStopped);
+ }
+ }
+ break;
+ case msWait:
+ {
+ if (m_ringbuffer->Available () == 0)
+ {
+ // m_active = false;
+ SetPlayMode (pmStopped);
+ }
+ }
+ break;
+ }
+ }
+
+ if (m_rframe && m_ringbuffer->Put (m_rframe))
+ {
+ m_rframe = 0;
+ }
+
+ if (!m_pframe && m_playmode == pmPlay)
+ {
+ m_pframe = m_ringbuffer->Get ();
+ if (m_pframe)
+ {
+ p = m_pframe->Data ();
+ pc = m_pframe->Count ();
+ }
+ }
+
+ if (m_pframe)
+ {
+#if VDRVERSNUM >= 10318
+ int w = PlayPes (p, pc);
+#else
+ int w = PlayVideo (p, pc);
+#endif
+ if (w > 0)
+ {
+ p += w;
+ pc -= w;
+
+ if (pc <= 0)
+ {
+ m_ringbuffer->Drop (m_pframe);
+ m_pframe = 0;
+ }
+ }
+ else if (w < 0 && FATALERRNO)
+ {
+ LOG_ERROR;
+ break;
+ }
+ }
+ eState curr_m_state=m_state; // avoid helgrind warning
+
+ Unlock ();
+
+ if ((m_rframe || curr_m_state == msWait) && m_pframe)
+ {
+// Wait for output to become ready
+ DevicePoll (poll, 500);
+ }
+ else
+ {
+ if (m_playmode != pmPlay)
+ {
+ m_playmode_mutex.Lock ();
+
+ if (m_playmode != pmPlay)
+ {
+ // Wait on playMode change
+ WaitPlayMode (m_playmode, true);
+ }
+ m_playmode_mutex.Unlock ();
+ }
+ }
+ }
+
+ Lock ();
+
+ if (m_rframe)
+ {
+ delete m_rframe;
+ m_rframe = 0;
+ }
+
+ if (m_decoder)
+ { // who deletes decoder?
+ m_decoder->stop ();
+ delete m_decoder;
+ m_decoder = 0;
+ }
+
+ m_playing = 0;
+
+ SetPlayMode (pmStopped);
+
+ Unlock ();
+
+ m_active = false;
+
+ dsyslog ("muggle: player thread ended (pid=%d)", getpid ());
+}
+
+
+void
+mgPCMPlayer::Empty (void)
+{
+ MGLOG ("mgPCMPlayer::Empty");
+
+ Lock ();
+
+ m_ringbuffer->Clear ();
+ DeviceClear ();
+
+ delete m_rframe;
+ m_rframe = 0;
+ m_pframe = 0;
+
+ Unlock ();
+}
+
+
+void
+mgPCMPlayer::StopPlay ()
+{ // StopPlay() must be called in locked state!!!
+ MGLOG ("mgPCMPlayer::StopPlay");
+ if (m_playmode != pmStopped)
+ {
+ Empty ();
+ m_state = msStop;
+ SetPlayMode (pmPlay);
+ Unlock (); // let the decode thread process the stop signal
+
+ m_playmode_mutex.Lock ();
+ WaitPlayMode (pmStopped, false);
+ m_playmode_mutex.Unlock ();
+
+ Lock ();
+ }
+}
+
+
+bool mgPCMPlayer::SkipFile (int step)
+{
+ MGLOG("mgPCMPlayer::SkipFile");
+ mgDebug(1,"SkipFile:step=%d, skip_direction=%d",step,skip_direction);
+ mgContentItem * newcurr = NULL;
+ if (step!=0)
+ skip_direction=step;
+ if (m_playlist->skipTracks (skip_direction))
+ {
+ newcurr = m_playlist->getCurrentTrack ();
+ if (newcurr) {
+ delete m_current;
+ m_current = new mgContentItem(newcurr);
+ }
+ }
+ return (newcurr != NULL);
+}
+
+void
+mgPCMPlayer::ToggleShuffle ()
+{
+ m_playlist->toggleShuffleMode ();
+}
+
+
+void
+mgPCMPlayer::ToggleLoop (void)
+{
+ m_playlist->toggleLoopMode ();
+}
+
+
+void
+mgPCMPlayer::Pause (void)
+{
+ if (m_playmode == pmPaused)
+ {
+ Play ();
+ }
+ else
+ {
+ if (m_playmode == pmPlay)
+ {
+// DeviceFreeze();
+ SetPlayMode (pmPaused);
+ }
+ }
+}
+
+
+void
+mgPCMPlayer::Play (void)
+{
+ MGLOG ("mgPCMPlayer::Play");
+
+
+ if (m_playmode != pmPlay && m_current)
+ {
+ Lock ();
+ if (m_playmode == pmStopped)
+ {
+ m_state = msStart;
+ }
+// DevicePlay(); // TODO? Commented out in original code, too
+ SetPlayMode (pmPlay);
+ Unlock ();
+ }
+}
+
+
+void
+mgPCMPlayer::Forward ()
+{
+ MGLOG ("mgPCMPlayer::Forward");
+
+ Lock ();
+ if (SkipFile (1))
+ {
+ StopPlay ();
+ Play ();
+ }
+ Unlock ();
+}
+
+
+void
+mgPCMPlayer::Backward (void)
+{
+ MGLOG ("mgPCMPlayer::Backward");
+ Lock ();
+ if (SkipFile (-1))
+ {
+ StopPlay ();
+ Play ();
+ }
+ Unlock ();
+}
+
+
+void
+mgPCMPlayer::Goto (int index, bool still)
+{
+ m_playlist->setTrackPosition (index - 1);
+ mgContentItem *next = m_playlist->getCurrentTrack ();
+
+ if (next)
+ {
+ Lock ();
+ StopPlay ();
+ delete m_current;
+ m_current = new mgContentItem(next);
+ Play ();
+ Unlock ();
+ }
+}
+
+
+void
+mgPCMPlayer::SkipSeconds (int secs)
+{
+ if (m_playmode != pmStopped)
+ {
+ Lock ();
+ if (m_playmode == pmPaused)
+ {
+ SetPlayMode (pmPlay);
+ }
+ if (m_decoder
+ && m_decoder->skip (secs, m_ringbuffer->Available (),
+ dvbSampleRate))
+ {
+ levelgood = false;
+ }
+ Empty ();
+ Unlock ();
+ }
+}
+
+
+bool mgPCMPlayer::GetIndex (int &current, int &total, bool snaptoiframe)
+{
+ if (m_current)
+ {
+ current = SecondsToFrames (m_index);
+ total = SecondsToFrames (m_current->getDuration ());
+ return true;
+ }
+ return false;
+}
+
+
+/*
+string mgPCMPlayer::CheckImage( string fileName, size_t j )
+{
+static char tmpFile[1024];
+char *tmp[2048];
+char *result = NULL;
+FILE *fp;
+
+sprintf (tmpFile, "%s/%s", MP3Setup.ImageCacheDir, &fileName[j]); // ???
+strcpy (strrchr (tmpFile, '.'), ".mpg");
+d(printf("mp3[%d]: cache %s\n", getpid (), tmpFile))
+if ((fp = fopen (tmpFile, "rb")))
+{
+fclose (fp);
+result = tmpFile;
+}
+else
+{
+if ((fp = fopen (fileName, "rb")))
+{
+fclose (fp);
+d(printf("mp3[%d]: image %s found\n", getpid (), fileName))
+sprintf ((char *) tmp, "image_convert.sh \"%s\" \"%s\"", fileName, tmpFile);
+system ((const char*) tmp);
+result = tmpFile;
+}
+}
+fp = fopen ("/tmp/vdr_mp3_current_image.txt", "w");
+fprintf (fp, "%s\n", fileName);
+fclose (fp);
+return result;
+}
+
+char *cMP3Player::LoadImage(const char *fullname)
+{
+size_t i, j = strlen (MP3Sources.GetSource()->BaseDir()) + 1;
+char imageFile[1024];
+static char mpgFile[1024];
+char *p, *q = NULL;
+char *imageSuffixes[] = { "png", "gif", "jpg" };
+
+d(printf("mp3[%d]: checking %s for images\n", getpid (), fullname))
+strcpy (imageFile, fullname);
+strcpy (mpgFile, "");
+//
+// track specific image, e.g. <song>.jpg
+//
+p = strrchr (imageFile, '.');
+if (p)
+{
+for (i = 0; i < sizeof (imageSuffixes) / sizeof (imageSuffixes[0]); i++)
+{
+strcpy (p + 1, imageSuffixes[i]);
+d(printf("mp3[%d]: %s\n", getpid (), imageFile))
+q = CheckImage (imageFile, j);
+if (q)
+{
+strcpy (mpgFile, q);
+}
+}
+}
+//
+// album specific image, e.g. cover.jpg in song directory
+//
+if (!strlen (mpgFile))
+{
+p = strrchr (imageFile, '/');
+if (p)
+{
+strcpy (p + 1, "cover.");
+p += 6;
+for (i = 0; i < sizeof (imageSuffixes) / sizeof (imageSuffixes[0]); i++)
+{
+strcpy (p + 1, imageSuffixes[i]);
+d(printf("mp3[%d]: %s\n", getpid (), imageFile))
+q = CheckImage (imageFile, j);
+if (q)
+{
+strcpy (mpgFile, q);
+}
+}
+}
+}
+//
+// artist specific image, e.g. artist.jpg in artist directory
+//
+if (!strlen (mpgFile))
+{
+p = strrchr (imageFile, '/');
+if (p)
+{
+*p = '\0';
+p = strrchr (imageFile, '/');
+}
+if (p)
+{
+strcpy (p + 1, "artist.");
+p += 7;
+for (i = 0; i < sizeof (imageSuffixes) / sizeof (imageSuffixes[0]); i++)
+{
+strcpy (p + 1, imageSuffixes[i]);
+d(printf("mp3[%d]: %s\n", getpid (), imageFile))
+q = CheckImage (imageFile, j);
+if (q)
+{
+strcpy (mpgFile, q);
+}
+}
+}
+}
+//
+// default background image
+//
+if (!strlen (mpgFile))
+{
+for (i = 0; i < sizeof (imageSuffixes) / sizeof (imageSuffixes[0]); i++)
+{
+sprintf (imageFile, "%s/background.%s", MP3Setup.ImageCacheDir, imageSuffixes[i]);
+d(printf("mp3[%d]: %s\n", getpid (), imageFile))
+q = CheckImage (imageFile, strlen(MP3Setup.ImageCacheDir) + 1);
+if (q)
+{
+strcpy (mpgFile, q);
+}
+}
+}
+if (!strlen (mpgFile))
+{
+sprintf (mpgFile, "%s/background.mpg", MP3Setup.ImageCacheDir);
+}
+return mpgFile;
+}
+
+void mgPCMPlayer::ShowImage (char *file)
+{
+uchar *buffer;
+int fd;
+struct stat st;
+struct video_still_picture sp;
+
+if ((fd = open (file, O_RDONLY)) >= 0)
+{
+d(printf("mp3[%d]: cover still picture %s\n", getpid (), file))
+fstat (fd, &st);
+sp.iFrame = (char *) malloc (st.st_size);
+if (sp.iFrame)
+{
+sp.size = st.st_size;
+if (read (fd, sp.iFrame, sp.size) > 0)
+{
+buffer = (uchar *) sp.iFrame;
+d(printf("mp3[%d]: new image frame (size %d)\n", getpid(), sp.size))
+if(MP3Setup.UseDeviceStillPicture)
+DeviceStillPicture (buffer, sp.size);
+else
+{
+for (int i = 1; i <= 25; i++)
+{
+send_pes_packet (buffer, sp.size, i);
+}
+}
+}
+free (sp.iFrame);
+}
+else
+{
+esyslog ("mp3[%d]: cannot allocate memory (%d bytes) for still image",
+getpid(), (int) st.st_size);
+}
+close (fd);
+}
+else
+{
+esyslog ("mp3[%d]: cannot open image file '%s'",
+getpid(), file);
+}
+}
+
+void mgPCMPlayer::send_pes_packet(unsigned char *data, int len, int timestamp)
+{
+#define PES_MAX_SIZE 2048
+int ptslen = timestamp ? 5 : 1;
+static unsigned char pes_header[PES_MAX_SIZE];
+
+pes_header[0] = pes_header[1] = 0;
+pes_header[2] = 1;
+pes_header[3] = 0xe0;
+
+while(len > 0)
+{
+int payload_size = len;
+if(6 + ptslen + payload_size > PES_MAX_SIZE)
+payload_size = PES_MAX_SIZE - (6 + ptslen);
+
+pes_header[4] = (ptslen + payload_size) >> 8;
+pes_header[5] = (ptslen + payload_size) & 255;
+
+if(ptslen == 5)
+{
+int x;
+x = (0x02 << 4) | (((timestamp >> 30) & 0x07) << 1) | 1;
+pes_header[8] = x;
+x = ((((timestamp >> 15) & 0x7fff) << 1) | 1);
+pes_header[7] = x >> 8;
+pes_header[8] = x & 255;
+x = ((((timestamp) & 0x7fff) < 1) | 1);
+pes_header[9] = x >> 8;
+pes_header[10] = x & 255;
+} else
+{
+pes_header[6] = 0x0f;
+}
+
+memcpy(&pes_header[6 + ptslen], data, payload_size);
+#if VDRVERSNUM >= 10318
+PlayPes(pes_header, 6 + ptslen + payload_size);
+#else
+PlayVideo(pes_header, 6 + ptslen + payload_size);
+#endif
+
+len -= payload_size;
+data += payload_size;
+ptslen = 1;
+}
+}
+*/
+
+// --- mgPlayerControl -------------------------------------------------------
+
+mgPlayerControl::mgPlayerControl (mgSelection * plist):cControl (player =
+new
+mgPCMPlayer (plist))
+{
+#if VDRVERSNUM >= 10307
+ m_display = NULL;
+ m_menu = NULL;
+#endif
+ m_visible = false;
+ m_has_osd = false;
+ m_track_view = true;
+ m_progress_view = true;
+
+ m_szLastShowStatusMsg = NULL;
+
+// Notify all cStatusMonitor
+ StatusMsgReplaying ();
+}
+
+
+mgPlayerControl::~mgPlayerControl ()
+{
+// Stop();
+// Notify cleanup all cStatusMonitor
+ cStatus::MsgReplaying (this, NULL);
+ if (m_szLastShowStatusMsg)
+ {
+ free (m_szLastShowStatusMsg);
+ m_szLastShowStatusMsg = NULL;
+ }
+
+ InternalHide ();
+ Stop ();
+}
+
+
+bool mgPlayerControl::Active (void)
+{
+ return player && player->Active ();
+}
+
+
+void
+mgPlayerControl::Stop (void)
+{
+ if (player)
+ {
+ delete player;
+ player = 0;
+ }
+}
+
+
+void
+mgPlayerControl::Pause (void)
+{
+ if (player)
+ {
+ player->Pause ();
+ }
+}
+
+
+void
+mgPlayerControl::Play (void)
+{
+ if (player)
+ {
+ player->Play ();
+ }
+}
+
+
+void
+mgPlayerControl::Forward (void)
+{
+ if (player)
+ {
+ player->Forward ();
+ }
+}
+
+
+void
+mgPlayerControl::Backward (void)
+{
+ if (player)
+ {
+ player->Backward ();
+ }
+}
+
+
+void
+mgPlayerControl::SkipSeconds (int Seconds)
+{
+ if (player)
+ {
+ player->SkipSeconds (Seconds);
+ }
+}
+
+
+void
+mgPlayerControl::Goto (int Position, bool Still)
+{
+ if (player)
+ {
+ player->Goto (Position, Still);
+ }
+}
+
+
+void
+mgPlayerControl::ToggleShuffle (void)
+{
+ if (player)
+ {
+ player->ToggleShuffle ();
+ }
+}
+
+
+void
+mgPlayerControl::ToggleLoop (void)
+{
+ if (player)
+ {
+ player->ToggleLoop ();
+ }
+}
+
+void
+mgPlayerControl::ReloadPlaylist ()
+{
+ if (player)
+ {
+ player->ReloadPlaylist ();
+ }
+}
+
+void
+mgPlayerControl::NewPlaylist (mgSelection * plist)
+{
+ if (player)
+ {
+ player->NewPlaylist (plist);
+ }
+}
+
+void
+mgPlayerControl::ShowContents ()
+{
+#if VDRVERSNUM >= 10307
+ if (!m_menu)
+ {
+ m_menu = Skins.Current ()->DisplayMenu ();
+ }
+
+ if (player && m_menu)
+ {
+ int num_items = m_menu->MaxItems ();
+
+ if (m_track_view)
+ {
+ m_menu->Clear ();
+ m_menu->SetTitle ("Track info view");
+
+ m_menu->SetTabs (15);
+
+ char *buf;
+ if (num_items > 0)
+ {
+ asprintf (&buf, "Title:\t%s",
+ player->getCurrent ()->getTitle ().c_str ());
+ m_menu->SetItem (buf, 0, false, false);
+ free (buf);
+ }
+ if (num_items > 1)
+ {
+ asprintf (&buf, "Artist:\t%s",
+ player->getCurrent ()->getArtist ().c_str ());
+ m_menu->SetItem (buf, 1, false, false);
+ free (buf);
+ }
+ if (num_items > 2)
+ {
+ asprintf (&buf, "Album:\t%s",
+ player->getCurrent ()->getAlbum ().c_str ());
+ m_menu->SetItem (buf, 2, false, false);
+ free (buf);
+ }
+ if (num_items > 3)
+ {
+ asprintf (&buf, "Genre:\t%s",
+ player->getCurrent ()->getGenre ().c_str ());
+ m_menu->SetItem (buf, 3, false, false);
+ free (buf);
+ }
+ if (num_items > 4)
+ {
+ int len = player->getCurrent ()->getDuration ();
+ asprintf (&buf, "Length:\t%s",
+#if VDRVERSNUM >= 10318
+ *IndexToHMSF (SecondsToFrames (len)));
+#else
+ IndexToHMSF (SecondsToFrames (len)));
+#endif
+ m_menu->SetItem (buf, 4, false, false);
+ free (buf);
+ }
+ if (num_items > 5)
+ {
+ asprintf (&buf, "Bit rate:\t%s",
+ player->getCurrent ()->getBitrate ().c_str ());
+ m_menu->SetItem (buf, 5, false, false);
+ free (buf);
+ }
+ if (num_items > 6)
+ {
+ int sr = player->getCurrent ()->getSampleRate ();
+
+ asprintf (&buf, "Sampling rate:\t%d", sr);
+ m_menu->SetItem (buf, 6, false, false);
+ free (buf);
+ }
+ if (num_items > 6)
+ {
+ string sf = player->getCurrent ()->getSourceFile ();
+ char *p = strrchr(sf.c_str(),'/');
+ asprintf (&buf, "File name:\t%s", p+1);
+ m_menu->SetItem (buf, 7, false, false);
+ free (buf);
+ }
+ }
+ else
+ {
+ mgSelection *list = player->getPlaylist ();
+ if (list)
+ {
+// use items for playlist tag display
+ m_menu->Clear ();
+ m_menu->SetTitle ("Now playing");
+ m_menu->SetTabs (25);
+
+ int cur = list->getTrackPosition ();
+ for (int i = 0; i < num_items; i++)
+ {
+ mgContentItem *item = list->getTrack (cur - 3 + i);
+ if (item)
+ {
+ char *buf;
+ asprintf (&buf, "%s\t%s", item->getTitle ().c_str (),
+ item->getArtist ().c_str ());
+ m_menu->SetItem (buf, i, i == 3, i > 3);
+ free (buf);
+ }
+ }
+ }
+ }
+ }
+#endif
+}
+
+
+void
+mgPlayerControl::ShowProgress ()
+{
+ if (player)
+ {
+ char *buf;
+ bool play = true, forward = true;
+ int speed = -1;
+
+ int current_frame, total_frames;
+ player->GetIndex (current_frame, total_frames);
+
+ if (!m_track_view)
+ { // playlist stuff
+ mgSelection *list = player->getPlaylist ();
+ if (list)
+ {
+ total_frames = SecondsToFrames (list->getLength ());
+ current_frame += SecondsToFrames (list->getCompletedLength ());
+ asprintf (&buf, "%s (%d/%d)", list->getListname ().c_str (),
+ list->getTrackPosition () + 1, list->getNumTracks ());
+ }
+ }
+ else
+ { // track view
+ asprintf (&buf, "%s: %s",
+ player->getCurrent ()->getArtist ().c_str (),
+ player->getCurrent ()->getTitle ().c_str ());
+ }
+
+#if VDRVERSNUM >= 10307
+ if (!m_display)
+ {
+ m_display = Skins.Current ()->DisplayReplay (false);
+ }
+ if (m_display)
+ {
+ m_display->SetProgress (current_frame, total_frames);
+ m_display->SetCurrent (IndexToHMSF (current_frame));
+ m_display->SetTotal (IndexToHMSF (total_frames));
+ m_display->SetTitle (buf);
+ m_display->SetMode (play, forward, speed);
+ m_display->Flush ();
+ }
+#else
+ int w = Interface->Width ();
+ int h = Interface->Height ();
+
+ Interface->WriteText (w / 2, h / 2, "Muggle is active!");
+ Interface->Flush ();
+#endif
+ free (buf);
+ }
+}
+
+
+void
+mgPlayerControl::Display ()
+{
+ if (m_visible)
+ {
+ if (!m_has_osd)
+ {
+// open the osd if its not already there...
+#if VDRVERSNUM >= 10307
+#else
+ Interface->Open ();
+#endif
+ m_has_osd = true;
+ }
+
+// now an osd is open, go on
+ if (m_progress_view)
+ {
+#if VDRVERSNUM >= 10307
+ if (m_menu)
+ {
+ delete m_menu;
+ m_menu = NULL;
+ }
+#endif
+ ShowProgress ();
+ }
+ else
+ {
+#if VDRVERSNUM >= 10307
+ if (m_display)
+ {
+ delete m_display;
+ m_display = NULL;
+ }
+#endif
+ ShowContents ();
+ }
+ }
+ else
+ {
+ InternalHide ();
+ }
+}
+
+
+void
+mgPlayerControl::Hide ()
+{
+ m_visible = false;
+
+ InternalHide ();
+}
+
+
+void
+mgPlayerControl::InternalHide ()
+{
+ if (m_has_osd)
+ {
+#if VDRVERSNUM >= 10307
+ if (m_display)
+ {
+ delete m_display;
+ m_display = NULL;
+ }
+ if (m_menu)
+ {
+ delete m_menu;
+ m_menu = NULL;
+ }
+#else
+ Interface->Close ();
+#endif
+ m_has_osd = false;
+ }
+}
+
+
+eOSState mgPlayerControl::ProcessKey (eKeys key)
+{
+ if (key!=kNone)
+ mgDebug (1,"mgPlayerControl::ProcessKey(%u)",key);
+ if (!Active ())
+ {
+ return osEnd;
+ }
+
+ StatusMsgReplaying ();
+
+ Display ();
+
+ eOSState
+ state = cControl::ProcessKey (key);
+
+ if (state == osUnknown)
+ {
+ switch (key)
+ {
+ case kUp:
+ {
+ if (m_visible && !m_progress_view && !m_track_view)
+ Backward();
+ else
+ Forward ();
+ }
+ break;
+ case kDown:
+ {
+ if (m_visible && !m_progress_view && !m_track_view)
+ Forward ();
+ else
+ Backward();
+ }
+ break;
+ case kRed:
+ {
+ if (!m_visible && player)
+ {
+ mgSelection *
+ pl = player->getPlaylist ();
+
+ std::string s;
+ switch (pl->toggleLoopMode ())
+ {
+ case mgSelection::LM_NONE:
+ {
+ s = tr ("Loop mode off");
+ }
+ break;
+ case mgSelection::LM_SINGLE:
+ {
+ s = tr ("Loop mode single");
+ }
+ break;
+ case mgSelection::LM_FULL:
+ {
+ s = tr ("Loop mode full");
+ }
+ break;
+ default:
+ {
+ s = tr ("Unknown loop mode");
+ }
+ }
+#if VDRVERSNUM >= 10307
+ Skins.Message (mtInfo, s.c_str ());
+ Skins.Flush ();
+#else
+ Interface->Status (s.c_str ());
+ Interface->Flush ();
+#endif
+ }
+ else
+ {
+// toggle progress display between simple and detail
+ m_progress_view = !m_progress_view;
+ Display ();
+ }
+ }
+ break;
+ case kGreen:
+ {
+ if (!m_visible && player)
+ {
+ mgSelection *
+ pl = player->getPlaylist ();
+
+ std::string s;
+ switch (pl->toggleShuffleMode ())
+ {
+ case mgSelection::SM_NONE:
+ {
+ s = tr ("Shuffle mode off");
+ }
+ break;
+ case mgSelection::SM_NORMAL:
+ {
+ s = tr ("Shuffle mode normal");
+ }
+ break;
+ case mgSelection::SM_PARTY:
+ {
+ s = tr ("Shuffle mode party");
+ }
+ break;
+ default:
+ {
+ s = tr ("Unknown shuffle mode");
+ }
+ }
+#if VDRVERSNUM >= 10307
+ Skins.Message (mtInfo, s.c_str ());
+ Skins.Flush ();
+#else
+ Interface->Status (s.c_str ());
+ Interface->Flush ();
+#endif
+ }
+ else
+ {
+// toggle progress display between playlist and track
+ m_track_view = !m_track_view;
+ Display ();
+ }
+ }
+ break;
+ case kPause:
+ case kYellow:
+ {
+ Pause ();
+ }
+ break;
+ case kStop:
+ case kBlue:
+ {
+ InternalHide ();
+ Stop ();
+
+ return osEnd;
+ }
+ break;
+ case kOk:
+ {
+ m_visible = !m_visible;
+ Display ();
+
+ return osContinue;
+ }
+ break;
+ case kBack:
+ {
+ InternalHide ();
+ Stop ();
+
+ return osEnd;
+ }
+ break;
+ default:
+ {
+ return osUnknown;
+ }
+ }
+ }
+ return osContinue;
+}
+
+
+void
+mgPlayerControl::StatusMsgReplaying ()
+{
+ MGLOG ("mgPlayerControl::StatusMsgReplaying()");
+ char *szBuf = NULL;
+ if (player && player->getCurrent () && player->getPlaylist ())
+ {
+ char cLoopMode;
+ char cShuffle;
+
+ switch (player->getPlaylist ()->getLoopMode ())
+ {
+ default:
+ case mgSelection::LM_NONE:
+ cLoopMode = '.'; // Loop mode off
+ break;
+ case mgSelection::LM_SINGLE:
+ cLoopMode = 'S'; // Loop mode single
+ break;
+ case mgSelection::LM_FULL:
+ cLoopMode = 'P'; // Loop mode fuel
+ break;
+ }
+
+ switch (player->getPlaylist ()->getShuffleMode ())
+ {
+ default:
+ case mgSelection::SM_NONE:
+ cShuffle = '.'; // Shuffle mode off
+ break;
+ case mgSelection::SM_NORMAL:
+ cShuffle = 'S'; // Shuffle mode normal
+ break;
+ case mgSelection::SM_PARTY:
+ cShuffle = 'P'; // Shuffle mode party
+ break;
+ }
+
+ mgContentItem *tmp = player->getCurrent ();
+ if (tmp == NULL)
+ mgError("mgPlayerControl::StatusMsgReplaying: getCurrent() is NULL");
+ if (tmp->getArtist ().length () > 0)
+ {
+ asprintf (&szBuf, "[%c%c] (%d/%d) %s - %s",
+ cLoopMode,
+ cShuffle,
+ player->getPlaylist ()->getTrackPosition () + 1,
+ player->getPlaylist ()->getNumTracks (),
+ player->getCurrent ()->getArtist ().c_str (),
+ player->getCurrent ()->getTitle ().c_str ());
+ }
+ else
+ {
+ asprintf (&szBuf, "[%c%c] (%d/%d) %s",
+ cLoopMode,
+ cShuffle,
+ player->getPlaylist ()->getTrackPosition () + 1,
+ player->getPlaylist ()->getNumTracks (),
+ player->getCurrent ()->getTitle ().c_str ());
+ }
+ }
+ else
+ {
+ asprintf (&szBuf, "[muggle]");
+ }
+
+//fprintf(stderr,"StatusMsgReplaying(%s)\n",szBuf);
+ if (szBuf)
+ {
+ if (m_szLastShowStatusMsg == NULL
+ || 0 != strcmp (szBuf, m_szLastShowStatusMsg))
+ {
+ if (m_szLastShowStatusMsg)
+ {
+ free (m_szLastShowStatusMsg);
+ }
+ m_szLastShowStatusMsg = szBuf;
+ cStatus::MsgReplaying (this, m_szLastShowStatusMsg);
+ }
+ else
+ {
+ free (szBuf);
+ }
+ }
+}
diff --git a/muggle-plugin/vdr_player.h b/muggle-plugin/vdr_player.h
new file mode 100644
index 0000000..5d60344
--- /dev/null
+++ b/muggle-plugin/vdr_player.h
@@ -0,0 +1,149 @@
+/*!
+ * \file vdr_player.h
+ * \brief A player/control combination to let VDR play music
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___VDR_PLAYER_H
+#define ___VDR_PLAYER_H
+
+#include <player.h>
+#include "mg_selection.h"
+#if VDRVERSNUM >= 10307
+class cOsd;
+#endif
+
+// -------------------------------------------------------------------
+
+class mgPCMPlayer;
+
+// -------------------------------------------------------------------
+
+/*!
+ * \brief exerts control over the player itself
+ *
+ * This control is launched from the main menu and manages a link
+ * to the player. Key events are caught and signaled to the player.
+ */
+class mgPlayerControl:public cControl
+{
+ private:
+
+//! \brief the reference to the player
+ mgPCMPlayer * player;
+
+//! \brief indicates, whether the osd should be visible
+ bool m_visible;
+
+//! \brief indicates, whether an osd is currently displayed
+ bool m_has_osd;
+
+ bool m_track_view;
+ bool m_progress_view;
+
+#if VDRVERSNUM >= 10307
+//! \brief a replay display to show the progress during playback
+ cSkinDisplayReplay *m_display;
+ cSkinDisplayMenu *m_menu;
+
+ cOsd *osd;
+ const cFont *font;
+#endif
+
+//! \brief Last Message for Statusmonitor
+ char *m_szLastShowStatusMsg;
+
+ public:
+
+/*! \brief construct a control with a playlist
+ *
+ * \param plist - the playlist to be played
+ */
+ mgPlayerControl (mgSelection * plist);
+
+/*! \brief destructor
+ */
+ virtual ~ mgPlayerControl ();
+
+//! \brief indicate whether the corresponding player is active
+ bool Active ();
+
+//! \brief stop the corresponding player
+ void Stop ();
+
+//! \brief toggle the pause mode of the corresponding player
+ void Pause ();
+
+//! \brief start playing
+ void Play ();
+
+//! \brief skip to the next song
+ void Forward ();
+
+//! \brief skip to the previous song
+ void Backward ();
+
+/*! \brief skip a specified number of seconds
+ *
+ * \param seconds - the number of seconds to skip
+ */
+ void SkipSeconds (int seconds);
+
+/*! \brief goto a certain position in the playlist
+ *
+ * \param index - the position in the playlist to skip to
+ * \param still - currently unused
+ */
+ void Goto (int index, bool still = false);
+
+//! \brief toggle the shuffle mode of the corresponding player
+ void ToggleShuffle ();
+
+//! \brief toggle the loop mode of the corresponding player
+ void ToggleLoop ();
+
+ /*! \brief tell the player to reload the play list.
+ * This is needed if we play a collection
+ * and the user changed the collection while playing it
+ */
+ void ReloadPlaylist();
+
+/*! \brief signal a new playlist
+ *
+ * The caller has to take care of deallocating the previous list
+ *
+ * \param plist - the new playlist to be played
+ */
+ void NewPlaylist (mgSelection * plist);
+
+//! \brief a progress display
+ void ShowProgress ();
+
+ void Display ();
+
+ void ShowContents ();
+
+//! \brief hide the osd, if present
+ void Hide ();
+
+//! \brief hide the osd, if present
+ void InternalHide ();
+
+//! \brief process key events
+ eOSState ProcessKey (eKeys key);
+
+ protected:
+//! \brief signal a played file to any cStatusMonitor inside vdr
+ void StatusMsgReplaying ();
+};
+#endif //___VDR_PLAYER_H
diff --git a/muggle-plugin/vdr_setup.c b/muggle-plugin/vdr_setup.c
new file mode 100644
index 0000000..df10c0d
--- /dev/null
+++ b/muggle-plugin/vdr_setup.c
@@ -0,0 +1,87 @@
+/*!
+ * \file vdr_setup.c
+ * \brief A setup class for a VDR media plugin (muggle)
+ *
+ * \version $Revision: 1.3 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Partially adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#include <iostream>
+#include <stdlib.h>
+#include <stdio.h>
+#include <cstring>
+
+#include "vdr_setup.h"
+#include "vdr_actions.h"
+#include "i18n.h"
+
+
+// --- mgMenuSetup -----------------------------------------------------------
+
+mgMenuSetup::mgMenuSetup ()
+{
+ m_data = the_setup;
+
+ SetSection (tr ("Muggle"));
+
+ Add (new
+ cMenuEditBoolItem (tr ("Setup.Muggle$Initial loop mode"),
+ &m_data.InitLoopMode));
+ Add (new
+ cMenuEditBoolItem (tr ("Setup.Muggle$Initial shuffle mode"),
+ &m_data.InitShuffleMode));
+ Add (new
+ cMenuEditBoolItem (tr ("Setup.Muggle$Audio mode"), &m_data.AudioMode,
+ tr ("Round"), tr ("Dither")));
+ Add (new
+ cMenuEditBoolItem (tr ("Setup.Muggle$Use 48kHz mode only"),
+ &m_data.Only48kHz));
+ Add (new
+ cMenuEditIntItem (tr ("Setup.Muggle$Display mode"),
+ &m_data.DisplayMode, 1, 3));
+ Add (new
+ cMenuEditBoolItem (tr ("Setup.Muggle$Background mode"),
+ &m_data.BackgrMode, tr ("Black"), tr ("Live")));
+ Add (new
+ cMenuEditIntItem (tr ("Setup.Muggle$Normalizer level"),
+ &m_data.TargetLevel, 0, MAX_TARGET_LEVEL));
+ Add (new
+ cMenuEditIntItem (tr ("Setup.Muggle$Limiter level"),
+ &m_data.LimiterLevel, MIN_LIMITER_LEVEL, 100));
+ Add (new
+ cMenuEditBoolItem (tr ("Setup.Muggle$Delete stale references"),
+ &m_data.DeleteStaleReferences));
+
+
+ mgAction *a = actGenerate(actSync);
+ const char *mn = a->MenuName();
+ a->SetText(mn);
+ free(const_cast<char*>(mn));
+ Add(dynamic_cast<cOsdItem*>(a));
+}
+
+
+void
+mgMenuSetup::Store (void)
+{
+ the_setup = m_data;
+
+ SetupStore ("InitLoopMode", the_setup.InitLoopMode);
+ SetupStore ("InitShuffleMode", the_setup.InitShuffleMode);
+ SetupStore ("AudioMode", the_setup.AudioMode);
+ SetupStore ("DisplayMode", the_setup.DisplayMode);
+ SetupStore ("BackgrMode", the_setup.BackgrMode);
+ SetupStore ("TargetLevel", the_setup.TargetLevel);
+ SetupStore ("LimiterLevel", the_setup.LimiterLevel);
+ SetupStore ("Only48kHz", the_setup.Only48kHz);
+ SetupStore ("DeleteStaleReferences", the_setup.DeleteStaleReferences);
+}
+
diff --git a/muggle-plugin/vdr_setup.h b/muggle-plugin/vdr_setup.h
new file mode 100644
index 0000000..6a8ebdb
--- /dev/null
+++ b/muggle-plugin/vdr_setup.h
@@ -0,0 +1,38 @@
+/*!
+ * \file vdr_setup.h
+ * \brief A setup class for a VDR media plugin (muggle)
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___VDR_SETUP_MG_H
+#define ___VDR_SETUP_MG_H
+
+// #include <osd.h>
+#include <menuitems.h>
+#include <string>
+
+#include "mg_setup.h"
+
+/*!
+ * \brief allow user to modify setup on OSD
+ */
+class mgMenuSetup : public cMenuSetupPage
+{
+ private:
+ mgSetup m_data;
+ protected:
+ virtual void Store ();
+ public:
+ mgMenuSetup ();
+};
+#endif
diff --git a/muggle-plugin/vdr_sound.c b/muggle-plugin/vdr_sound.c
new file mode 100644
index 0000000..7c4304f
--- /dev/null
+++ b/muggle-plugin/vdr_sound.c
@@ -0,0 +1,753 @@
+/*!
+ * \file vdr_setup.c
+ * \brief Sound manipulation classes for a VDR media plugin (muggle)
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001,2002 Stefan Huelswitt <huels@iname.com>
+ */
+
+// --- cResample ------------------------------------------------------------
+
+// The resample code has been adapted from the madplay project
+// (resample.c) found in the libmad distribution
+
+class cResample
+{
+ private:
+ mad_fixed_t ratio;
+ mad_fixed_t step;
+ mad_fixed_t last;
+ mad_fixed_t resampled[MAX_NSAMPLES];
+ public:
+ bool SetInputRate (unsigned int oldrate, unsigned int newrate);
+ unsigned int ResampleBlock (unsigned int nsamples, const mad_fixed_t * old);
+ const mad_fixed_t *Resampled (void)
+ {
+ return resampled;
+ }
+};
+
+bool cResample::SetInputRate (unsigned int oldrate, unsigned int newrate)
+{
+ if (oldrate < 8000 || oldrate > newrate * 6)
+ { // out of range
+ esyslog ("WARNING: samplerate %d out of range 8000-%d\n", oldrate,
+ newrate * 6);
+ return 0;
+ }
+ ratio = mad_f_tofixed ((double) oldrate / (double) newrate);
+ step = 0;
+ last = 0;
+#ifdef DEBUG
+ static mad_fixed_t
+ oldratio = 0;
+ if (oldratio != ratio)
+ {
+ printf ("mad: new resample ratio %f (from %d kHz to %d kHz)\n",
+ mad_f_todouble (ratio), oldrate, newrate);
+ oldratio = ratio;
+ }
+#endif
+ return ratio != MAD_F_ONE;
+}
+
+
+unsigned int
+cResample::ResampleBlock (unsigned int nsamples, const mad_fixed_t * old)
+{
+// This resampling algorithm is based on a linear interpolation, which is
+// not at all the best sounding but is relatively fast and efficient.
+//
+// A better algorithm would be one that implements a bandlimited
+// interpolation.
+
+ mad_fixed_t *nsam = resampled;
+ const mad_fixed_t *end = old + nsamples;
+ const mad_fixed_t *begin = nsam;
+
+ if (step < 0)
+ {
+ step = mad_f_fracpart (-step);
+
+ while (step < MAD_F_ONE)
+ {
+ *nsam++ = step ? last + mad_f_mul (*old - last, step) : last;
+ step += ratio;
+ if (((step + 0x00000080L) & 0x0fffff00L) == 0)
+ step = (step + 0x00000080L) & ~0x0fffffffL;
+ }
+ step -= MAD_F_ONE;
+ }
+
+ while (end - old > 1 + mad_f_intpart (step))
+ {
+ old += mad_f_intpart (step);
+ step = mad_f_fracpart (step);
+ *nsam++ = step ? *old + mad_f_mul (old[1] - old[0], step) : *old;
+ step += ratio;
+ if (((step + 0x00000080L) & 0x0fffff00L) == 0)
+ step = (step + 0x00000080L) & ~0x0fffffffL;
+ }
+
+ if (end - old == 1 + mad_f_intpart (step))
+ {
+ last = end[-1];
+ step = -step;
+ }
+ else
+ step -= mad_f_fromint (end - old);
+
+ return nsam - begin;
+}
+
+
+// --- cLevel ----------------------------------------------------------------
+
+// The normalize algorithm and parts of the code has been adapted from the
+// Normalize 0.7 project. (C) 1999-2002, Chris Vaill <cvaill@cs.columbia.edu>
+
+// A little background on how normalize computes the volume
+// of a wav file, in case you want to know just how your
+// files are being munged:
+//
+// The volumes calculated are RMS amplitudes, which corre­
+// spond (roughly) to perceived volume. Taking the RMS ampli­
+// tude of an entire file would not give us quite the measure
+// we want, though, because a quiet song punctuated by short
+// loud parts would average out to a quiet song, and the
+// adjustment we would compute would make the loud parts
+// excessively loud.
+//
+// What we want is to consider the maximum volume of the
+// file, and normalize according to that. We break up the
+// signal into 100 chunks per second, and get the signal
+// power of each chunk, in order to get an estimation of
+// "instantaneous power" over time. This "instantaneous
+// power" signal varies too much to get a good measure of the
+// original signal's maximum sustained power, so we run a
+// smoothing algorithm over the power signal (specifically, a
+// mean filter with a window width of 100 elements). The max­
+// imum point of the smoothed power signal turns out to be a
+// good measure of the maximum sustained power of the file.
+// We can then take the square root of the power to get maxi­
+// mum sustained RMS amplitude.
+
+class cLevel
+{
+ private:
+ double maxpow;
+ mad_fixed_t peak;
+ struct Power
+ {
+// smooth
+ int npow, wpow;
+ double powsum, pows[POW_WIN];
+// sum
+ unsigned int nsum;
+ double sum;
+ } power[2];
+//
+ inline void AddPower (struct Power *p, double pow);
+ public:
+ void Init (void);
+ void GetPower (struct mad_pcm *pcm);
+ double GetLevel (void);
+ double GetPeak (void);
+};
+
+void
+cLevel::Init (void)
+{
+ for (int l = 0; l < 2; l++)
+ {
+ struct Power *p = &power[l];
+ p->sum = p->powsum = 0.0;
+ p->wpow = p->npow = p->nsum = 0;
+ for (int i = POW_WIN - 1; i >= 0; i--)
+ p->pows[i] = 0.0;
+ }
+ maxpow = 0.0;
+ peak = 0;
+}
+
+
+void
+cLevel::GetPower (struct mad_pcm *pcm)
+{
+ for (int i = 0; i < pcm->channels; i++)
+ {
+ struct Power *p = &power[i];
+ mad_fixed_t *data = pcm->samples[i];
+ for (int n = pcm->length; n > 0; n--)
+ {
+ if (*data < -peak)
+ peak = -*data;
+ if (*data > peak)
+ peak = *data;
+ double s = mad_f_todouble (*data++);
+ p->sum += (s * s);
+ if (++(p->nsum) >= pcm->samplerate / 100)
+ {
+ AddPower (p, p->sum / (double) p->nsum);
+ p->sum = 0.0;
+ p->nsum = 0;
+ }
+ }
+ }
+}
+
+
+void
+cLevel::AddPower (struct Power *p, double pow)
+{
+ p->powsum += pow;
+ if (p->npow >= POW_WIN)
+ {
+ if (p->powsum > maxpow)
+ maxpow = p->powsum;
+ p->powsum -= p->pows[p->wpow];
+ }
+ else
+ p->npow++;
+ p->pows[p->wpow] = pow;
+ p->wpow = (p->wpow + 1) % POW_WIN;
+}
+
+
+double
+cLevel::GetLevel (void)
+{
+ if (maxpow < EPSILON)
+ {
+// Either this whole file has zero power, or was too short to ever
+// fill the smoothing buffer. In the latter case, we need to just
+// get maxpow from whatever data we did collect.
+
+ if (power[0].powsum > maxpow)
+ maxpow = power[0].powsum;
+ if (power[1].powsum > maxpow)
+ maxpow = power[1].powsum;
+ }
+ // adjust for the smoothing window size and root
+ double level = sqrt (maxpow / (double) POW_WIN);
+ printf ("norm: new volumen level=%f peak=%f\n", level,
+ mad_f_todouble (peak));
+ return level;
+}
+
+
+double
+cLevel::GetPeak (void)
+{
+ return mad_f_todouble (peak);
+}
+
+
+// --- cNormalize ------------------------------------------------------------
+
+class cNormalize
+{
+ private:
+ mad_fixed_t gain;
+ double d_limlvl, one_limlvl;
+ mad_fixed_t limlvl;
+ bool dogain, dolimit;
+#ifdef DEBUG
+// stats
+ unsigned long limited, clipped, total;
+ mad_fixed_t peak;
+#endif
+// limiter
+#ifdef USE_FAST_LIMITER
+ mad_fixed_t *table, tablestart;
+ int tablesize;
+ inline mad_fixed_t FastLimiter (mad_fixed_t x);
+#endif
+ inline mad_fixed_t Limiter (mad_fixed_t x);
+ public:
+ cNormalize (void);
+ ~cNormalize ();
+ void Init (double Level, double Peak);
+ void Stats (void);
+ void AddGain (struct mad_pcm *pcm);
+};
+
+cNormalize::cNormalize (void)
+{
+ d_limlvl = (double) the_setup.LimiterLevel / 100.0;
+ one_limlvl = 1 - d_limlvl;
+ limlvl = mad_f_tofixed (d_limlvl);
+ printf ("norm: lim_lev=%f lim_acc=%d\n", d_limlvl, LIM_ACC);
+
+#ifdef USE_FAST_LIMITER
+ mad_fixed_t start = limlvl & ~(F_LIM_JMP - 1);
+ tablestart = start;
+ tablesize = (unsigned int) (F_LIM_MAX - start) / F_LIM_JMP + 2;
+ table = new mad_fixed_t[tablesize];
+ if (table)
+ {
+ printf ("norm: table size=%d start=%08x jump=%08x\n", tablesize, start,
+ F_LIM_JMP);
+ for (int i = 0; i < tablesize; i++)
+ {
+ table[i] = Limiter (start);
+ start += F_LIM_JMP;
+ }
+ tablesize--; // avoid a -1 in FastLimiter()
+
+// do a quick accuracy check, just to be sure that FastLimiter() is working
+// as expected :-)
+#ifdef ACC_DUMP
+ FILE *out = fopen ("/tmp/limiter", "w");
+#endif
+ mad_fixed_t maxdiff = 0;
+ for (mad_fixed_t x = F_LIM_MAX; x >= limlvl; x -= mad_f_tofixed (1e-4))
+ {
+ mad_fixed_t diff = mad_f_abs (Limiter (x) - FastLimiter (x));
+ if (diff > maxdiff)
+ maxdiff = diff;
+#ifdef ACC_DUMP
+ fprintf (out, "%0.10f\t%0.10f\t%0.10f\t%0.10f\t%0.10f\n",
+ mad_f_todouble (x), mad_f_todouble (Limiter (x)),
+ mad_f_todouble (FastLimiter (x)), mad_f_todouble (diff),
+ mad_f_todouble (maxdiff));
+ if (ferror (out))
+ break;
+#endif
+ }
+#ifdef ACC_DUMP
+ fclose (out);
+#endif
+ printf ("norm: accuracy %.12f\n", mad_f_todouble (maxdiff));
+ if (mad_f_todouble (maxdiff) > 1e-6)
+ {
+ esyslog ("ERROR: accuracy check failed, normalizer disabled");
+ delete table;
+ table = 0;
+ }
+ }
+ else
+ esyslog ("ERROR: no memory for lookup table, normalizer disabled");
+#endif // USE_FAST_LIMITER
+}
+
+
+cNormalize::~cNormalize ()
+{
+#ifdef USE_FAST_LIMITER
+ delete[] table;
+#endif
+}
+
+
+void
+cNormalize::Init (double Level, double Peak)
+{
+ double Target = (double) the_setup.TargetLevel / 100.0;
+ double dgain = Target / Level;
+ if (dgain > MAX_GAIN)
+ dgain = MAX_GAIN;
+ gain = mad_f_tofixed (dgain);
+// Check if we actually need to apply a gain
+ dogain = (Target > 0.0 && fabs (1 - dgain) > MIN_GAIN);
+#ifdef USE_FAST_LIMITER
+ if (!table)
+ dogain = false;
+#endif
+// Check if we actually need to do limiting:
+// we have to if limiter is enabled, if gain>1 and if the peaks will clip.
+ dolimit = (d_limlvl < 1.0 && dgain > 1.0 && Peak * dgain > 1.0);
+#ifdef DEBUG
+ printf ("norm: gain=%f dogain=%d dolimit=%d (target=%f level=%f peak=%f)\n",
+ dgain, dogain, dolimit, Target, Level, Peak);
+ limited = clipped = total = 0;
+ peak = 0;
+#endif
+}
+
+
+void
+cNormalize::Stats (void)
+{
+#ifdef DEBUG
+ if (total)
+ printf ("norm: stats tot=%ld lim=%ld/%.3f%% clip=%ld/%.3f%% peak=%.3f\n",
+ total, limited, (double) limited / total * 100.0, clipped,
+ (double) clipped / total * 100.0, mad_f_todouble (peak));
+#endif
+}
+
+
+mad_fixed_t cNormalize::Limiter (mad_fixed_t x)
+{
+// Limiter function:
+//
+// / x (for x <= lev)
+// x' = |
+// \ tanh((x - lev) / (1-lev)) * (1-lev) + lev (for x > lev)
+//
+// call only with x>=0. For negative samples, preserve sign outside this function
+//
+// With limiter level = 0, this is equivalent to a tanh() function;
+// with limiter level = 1, this is equivalent to clipping.
+
+ if (x > limlvl)
+ {
+#ifdef DEBUG
+ if (x > MAD_F_ONE)
+ clipped++;
+ limited++;
+#endif
+ x =
+ mad_f_tofixed (tanh ((mad_f_todouble (x) - d_limlvl) / one_limlvl) *
+ one_limlvl + d_limlvl);
+ }
+ return x;
+}
+
+
+#ifdef USE_FAST_LIMITER
+mad_fixed_t cNormalize::FastLimiter (mad_fixed_t x)
+{
+// The fast algorithm is based on a linear interpolation between the
+// the values in the lookup table. Relays heavly on libmads fixed point format.
+
+ if (x > limlvl)
+ {
+ int
+ i = (unsigned int) (x - tablestart) / F_LIM_JMP;
+#ifdef DEBUG
+ if (x > MAD_F_ONE)
+ clipped++;
+ limited++;
+ if (i >= tablesize)
+ printf ("norm: overflow x=%f x-ts=%f i=%d tsize=%d\n",
+ mad_f_todouble (x), mad_f_todouble (x - tablestart), i,
+ tablesize);
+#endif
+ mad_fixed_t
+ r = x & (F_LIM_JMP - 1);
+ x = MAD_F_ONE;
+ if (i < tablesize)
+ {
+ mad_fixed_t *
+ ptr = &table[i];
+ x = *ptr;
+ mad_fixed_t
+ d = *(ptr + 1) - x;
+//x+=mad_f_mul(d,r)<<LIM_ACC; // this is not accurate as mad_f_mul() does >>MAD_F_FRACBITS
+// which is senseless in the case of following <<LIM_ACC.
+ // better, don't know if works on all machines
+ x += ((long long) d * (long long) r) >> LIM_SHIFT;
+ }
+ }
+ return x;
+}
+#endif
+
+#ifdef USE_FAST_LIMITER
+#define LIMITER_FUNC FastLimiter
+#else
+#define LIMITER_FUNC Limiter
+#endif
+
+void
+cNormalize::AddGain (struct mad_pcm *pcm)
+{
+ if (dogain)
+ {
+ for (int i = 0; i < pcm->channels; i++)
+ {
+ mad_fixed_t *data = pcm->samples[i];
+#ifdef DEBUG
+ total += pcm->length;
+#endif
+ if (dolimit)
+ {
+ for (int n = pcm->length; n > 0; n--)
+ {
+ mad_fixed_t s = mad_f_mul (*data, gain);
+ if (s < 0)
+ {
+ s = -s;
+#ifdef DEBUG
+ if (s > peak)
+ peak = s;
+#endif
+ s = LIMITER_FUNC (s);
+ s = -s;
+ }
+ else
+ {
+#ifdef DEBUG
+ if (s > peak)
+ peak = s;
+#endif
+ s = LIMITER_FUNC (s);
+ }
+ *data++ = s;
+ }
+ }
+ else
+ {
+ for (int n = pcm->length; n > 0; n--)
+ {
+ mad_fixed_t s = mad_f_mul (*data, gain);
+#ifdef DEBUG
+ if (s > peak)
+ peak = s;
+ else if (-s > peak)
+ peak = -s;
+#endif
+ if (s > MAD_F_ONE)
+ s = MAD_F_ONE; // do clipping
+ if (s < -MAD_F_ONE)
+ s = -MAD_F_ONE;
+ *data++ = s;
+ }
+ }
+ }
+ }
+}
+
+
+// --- cScale ----------------------------------------------------------------
+
+// The dither code has been adapted from the madplay project
+// (audio.c) found in the libmad distribution
+
+enum eAudioMode
+{ amRound, amDither };
+
+class cScale
+{
+ private:
+ enum
+ { MIN = -MAD_F_ONE, MAX = MAD_F_ONE - 1 };
+#ifdef DEBUG
+// audio stats
+ unsigned long clipped_samples;
+ mad_fixed_t peak_clipping;
+ mad_fixed_t peak_sample;
+#endif
+// dither
+ struct dither
+ {
+ mad_fixed_t error[3];
+ mad_fixed_t random;
+ } leftD, rightD;
+//
+ inline mad_fixed_t Clip (mad_fixed_t sample, bool stats = true);
+ inline signed long LinearRound (mad_fixed_t sample);
+ inline unsigned long Prng (unsigned long state);
+ inline signed long LinearDither (mad_fixed_t sample, struct dither *dither);
+ public:
+ cScale();
+ void Stats (void);
+ unsigned int ScaleBlock (unsigned char *data, unsigned int size,
+ unsigned int &nsamples, const mad_fixed_t * &left,
+ const mad_fixed_t * &right, eAudioMode mode);
+};
+
+cScale::cScale()
+{
+#ifdef DEBUG
+ clipped_samples = 0;
+ peak_clipping = peak_sample = 0;
+#endif
+ memset (&leftD, 0, sizeof (leftD));
+ memset (&rightD, 0, sizeof (rightD));
+}
+
+
+void
+cScale::Stats (void)
+{
+#ifdef DEBUG
+ printf ("mp3: scale stats clipped=%ld peak_clip=%f peak=%f\n",
+ clipped_samples, mad_f_todouble (peak_clipping),
+ mad_f_todouble (peak_sample));
+#endif
+}
+
+
+// gather signal statistics while clipping
+mad_fixed_t cScale::Clip (mad_fixed_t sample, bool stats)
+{
+#ifndef DEBUG
+ if (sample > MAX)
+ sample = MAX;
+ if (sample < MIN)
+ sample = MIN;
+#else
+ if (!stats)
+ {
+ if (sample > MAX)
+ sample = MAX;
+ if (sample < MIN)
+ sample = MIN;
+ }
+ else
+ {
+ if (sample >= peak_sample)
+ {
+ if (sample > MAX)
+ {
+ ++clipped_samples;
+ if (sample - MAX > peak_clipping)
+ peak_clipping = sample - MAX;
+ sample = MAX;
+ }
+ peak_sample = sample;
+ }
+ else if (sample < -peak_sample)
+ {
+ if (sample < MIN)
+ {
+ ++clipped_samples;
+ if (MIN - sample > peak_clipping)
+ peak_clipping = MIN - sample;
+ sample = MIN;
+ }
+ peak_sample = -sample;
+ }
+ }
+#endif
+ return sample;
+}
+
+
+// generic linear sample quantize routine
+signed long
+cScale::LinearRound (mad_fixed_t sample)
+{
+// round
+ sample += (1L << (MAD_F_FRACBITS - OUT_BITS));
+// clip
+ sample = Clip (sample);
+// quantize and scale
+ return sample >> (MAD_F_FRACBITS + 1 - OUT_BITS);
+}
+
+
+// 32-bit pseudo-random number generator
+unsigned long
+cScale::Prng (unsigned long state)
+{
+ return (state * 0x0019660dL + 0x3c6ef35fL) & 0xffffffffL;
+}
+
+
+// generic linear sample quantize and dither routine
+signed long
+cScale::LinearDither (mad_fixed_t sample, struct dither *dither)
+{
+ unsigned int scalebits;
+ mad_fixed_t output, mask, random;
+
+// noise shape
+ sample += dither->error[0] - dither->error[1] + dither->error[2];
+ dither->error[2] = dither->error[1];
+ dither->error[1] = dither->error[0] / 2;
+// bias
+ output = sample + (1L << (MAD_F_FRACBITS + 1 - OUT_BITS - 1));
+ scalebits = MAD_F_FRACBITS + 1 - OUT_BITS;
+ mask = (1L << scalebits) - 1;
+// dither
+ random = Prng (dither->random);
+ output += (random & mask) - (dither->random & mask);
+ dither->random = random;
+// clip
+ output = Clip (output);
+ sample = Clip (sample, false);
+// quantize
+ output &= ~mask;
+// error feedback
+ dither->error[0] = sample - output;
+// scale
+ return output >> scalebits;
+}
+
+
+// write a block of signed 16-bit big-endian PCM samples
+unsigned int
+cScale::ScaleBlock (unsigned char *data, unsigned int size,
+unsigned int &nsamples, const mad_fixed_t * &left,
+const mad_fixed_t * &right, eAudioMode mode)
+{
+ signed int sample;
+ unsigned int len, res;
+
+ len = size / OUT_FACT;
+ res = size;
+ if (len > nsamples)
+ {
+ len = nsamples;
+ res = len * OUT_FACT;
+ }
+ nsamples -= len;
+
+ if (right)
+ { // stereo
+ switch (mode)
+ {
+ case amRound:
+ while (len--)
+ {
+ sample = LinearRound (*left++);
+ *data++ = sample >> 8;
+ *data++ = sample >> 0;
+ sample = LinearRound (*right++);
+ *data++ = sample >> 8;
+ *data++ = sample >> 0;
+ }
+ break;
+ case amDither:
+ while (len--)
+ {
+ sample = LinearDither (*left++, &leftD);
+ *data++ = sample >> 8;
+ *data++ = sample >> 0;
+ sample = LinearDither (*right++, &rightD);
+ *data++ = sample >> 8;
+ *data++ = sample >> 0;
+ }
+ break;
+ }
+ }
+ else
+ { // mono, duplicate left channel
+ switch (mode)
+ {
+ case amRound:
+ while (len--)
+ {
+ sample = LinearRound (*left++);
+ *data++ = sample >> 8;
+ *data++ = sample >> 0;
+ *data++ = sample >> 8;
+ *data++ = sample >> 0;
+ }
+ break;
+ case amDither:
+ while (len--)
+ {
+ sample = LinearDither (*left++, &leftD);
+ *data++ = sample >> 8;
+ *data++ = sample >> 0;
+ *data++ = sample >> 8;
+ *data++ = sample >> 0;
+ }
+ break;
+ }
+ }
+ return res;
+}
diff --git a/muggle-plugin/vdr_stream.c b/muggle-plugin/vdr_stream.c
new file mode 100644
index 0000000..872ac4b
--- /dev/null
+++ b/muggle-plugin/vdr_stream.c
@@ -0,0 +1,351 @@
+/*!
+ * \file vdr_stream.c
+ * \brief Implementation of media stream classes
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001-2003 Stefan Huelswitt <huels@iname.com>
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/statfs.h>
+#include <iostream>
+
+#include <interface.h>
+
+#include "mg_tools.h"
+
+// #include "setup-mp3.h"
+#include "vdr_stream.h"
+#include "vdr_network.h"
+#include "vdr_config.h"
+// #include "i18n.h"
+// #include "version.h"
+
+//#define tr(x) x
+
+#ifdef USE_MMAP
+#include <sys/mman.h>
+#endif
+
+#define DEFAULT_PORT 80 // default port for streaming (HTTP)
+
+// --- mgStream -----------------------------------------------------------------
+
+mgStream::mgStream (std::string filename):m_filename (filename)
+{
+ m_fd = -1;
+ m_ismmap = false;
+ m_buffer = 0;
+}
+
+
+mgStream::~mgStream ()
+{
+ close ();
+}
+
+
+bool mgStream::open (bool log)
+{
+ if (m_fd >= 0)
+ {
+ return seek ();
+ }
+
+// just check, whether file exists?
+ if (fileinfo (log))
+ {
+// printf ("mgStream::open: fileinfo == true\n");
+
+ if ((m_fd =::open (m_filename.c_str (), O_RDONLY)) >= 0)
+ {
+ //printf ("mgStream::open: file opened\n");
+
+ m_buffpos = m_readpos = 0;
+ m_fill = 0;
+
+ //printf ("mgStream::open: buffpos, readpos, fill set\n");
+
+/*
+ #ifdef USE_MMAP
+ if( m_filesize <= MAX_MMAP_SIZE )
+ {
+ m_buffer = (unsigned char*)mmap( 0, m_filesize, PROT_READ,
+ MAP_SHARED, m_fd, 0 );
+ if( m_buffer != MAP_FAILED )
+ {
+ m_ismmap = true;
+ return true;
+ }
+else
+{
+dsyslog("mmap() failed for %s: %s", m_filename.c_str(), strerror(errno) );
+}
+}
+#endif
+*/
+ //printf ("mgStream::open: allocating buffer: %d\n", MP3FILE_BUFSIZE);
+ m_buffer = new unsigned char[MP3FILE_BUFSIZE];
+ //printf ("mgStream::open: buffer allocated\n");
+
+ if (m_buffer)
+ {
+ //printf ("mgStream::open: buffer allocated, returning true\n");
+
+ return true;
+ }
+ else
+ {
+ esyslog ("ERROR: not enough memory for buffer: %s",
+ m_filename.c_str ());
+ }
+ }
+ else
+ {
+ if (log)
+ {
+ esyslog ("ERROR: failed to open file %s: %s",
+ m_filename.c_str (), strerror (errno));
+ }
+ }
+ }
+
+ close ();
+ //printf ("mgStream::open: returning false\n");
+ return false;
+}
+
+
+void
+mgStream::close (void)
+{
+#ifdef USE_MMAP
+ if (m_ismmap)
+ {
+ munmap (m_buffer, m_filesize);
+ m_buffer = 0;
+ m_ismmap = false;
+ }
+ else
+ {
+#endif
+ delete[] m_buffer;
+ m_buffer = 0;
+#ifdef USE_MMAP
+ }
+#endif
+ if (m_fd >= 0)
+ {
+ ::close (m_fd);
+ m_fd = -1;
+ }
+}
+
+
+bool mgStream::seek (unsigned long long pos)
+{
+ //printf ("mgStream::seek\n");
+ if (m_fd >= 0 && pos >= 0 && pos <= m_filesize)
+ {
+ //printf ("mgStream::seek valid file and position detected\n");
+
+ m_buffpos = 0;
+ m_fill = 0;
+
+ if (m_ismmap)
+ {
+ m_readpos = pos;
+
+ //printf ("mgStream::seek: returning true\n");
+ return true;
+ }
+ else
+ {
+ if ((m_readpos = lseek64 (m_fd, pos, SEEK_SET)) >= 0)
+ {
+ if (m_readpos != pos)
+ {
+ dsyslog ("seek mismatch in %s, wanted %lld, got %lld",
+ m_filename.c_str (), pos, m_readpos);
+ }
+ //printf ("mgStream::seek: returning true\n");
+ return true;
+ }
+ else
+ {
+ esyslog ("ERROR: seeking failed in %s: %d,%s",
+ m_filename.c_str (), errno, strerror (errno));
+ }
+ }
+ }
+ else
+ {
+ //printf ("mp3: bad seek call fd=%d pos=%lld name=%s\n", m_fd, pos,
+ //m_filename.c_str ());
+ }
+
+ //printf ("mgStream::seek: returning false\n");
+ return false;
+}
+
+
+bool
+mgStream::stream (unsigned char *&data,
+unsigned long &len, const unsigned char *rest)
+{
+ if (m_fd >= 0)
+ {
+ if (m_readpos < m_filesize)
+ {
+ if (m_ismmap)
+ {
+ if (rest && m_fill)
+ {
+ m_readpos = (rest - m_buffer);// take care of remaining data
+ }
+ m_fill = m_filesize - m_readpos;
+ data = m_buffer + m_readpos;
+ len = m_fill;
+ m_buffpos = m_readpos;
+ m_readpos += m_fill;
+
+ return true;
+ }
+ else
+ {
+ if (rest && m_fill)
+ { // copy remaining data to start of buffer
+ m_fill -= (rest - m_buffer); // remaing bytes
+ memmove (m_buffer, rest, m_fill);
+ }
+ else
+ {
+ m_fill = 0;
+ }
+
+ int r;
+ do
+ {
+ r = read (m_fd, m_buffer + m_fill,
+ MP3FILE_BUFSIZE - m_fill);
+ }
+ while (r == -1 && errno == EINTR);
+
+ if (r >= 0)
+ {
+ m_buffpos = m_readpos - m_fill;
+ m_readpos += r;
+ m_fill += r;
+ data = m_buffer;
+ len = m_fill;
+
+ return true;
+ }
+ else
+ {
+ esyslog ("ERROR: read failed in %s: %d,%s",
+ m_filename.c_str (), errno, strerror (errno));
+ }
+ }
+ }
+ else
+ {
+ len = 0;
+ return true;
+ }
+ }
+ return false;
+}
+
+
+bool mgStream::removable ()
+{
+// we do not handle removable media at this time
+ return false;
+}
+
+
+bool mgStream::fileinfo (bool log)
+{
+ struct stat64
+ ds;
+
+ if (!stat64 (m_filename.c_str (), &ds))
+ {
+ //printf ("mgStream::fileinfo: stat64 == 0\n");
+
+ if (S_ISREG (ds.st_mode))
+ {
+ m_fsID = "";
+ m_fsType = 0;
+
+ struct statfs64
+ sfs;
+
+ if (!statfs64 (m_filename.c_str (), &sfs))
+ {
+ if (removable ())
+ {
+ char *
+ tmpbuf;
+ asprintf (&tmpbuf, "%llx:%llx", sfs.f_blocks, sfs.f_files);
+ m_fsID = tmpbuf;
+ free (tmpbuf);
+ }
+ m_fsType = sfs.f_type;
+ }
+ else
+ {
+ if (errno != ENOSYS && log)
+ {
+ esyslog ("ERROR: can't statfs %s: %s", m_filename.c_str (),
+ strerror (errno));
+ }
+ }
+
+ m_filesize = ds.st_size;
+ m_ctime = ds.st_ctime;
+
+#ifdef CDFS_MAGIC
+ if (m_fsType == CDFS_MAGIC)
+ {
+ m_ctime = 0; // CDFS returns mount time as ctime
+ }
+#endif
+// infodone tells that info has been read, like a cache flag
+// InfoDone();
+ return true;
+ }
+ else
+ {
+ if (log)
+ {
+ esyslog ("ERROR: %s is not a regular file",
+ m_filename.c_str ());
+ }
+ }
+ }
+ else
+ {
+ if (log)
+ {
+ esyslog ("ERROR: can't stat %s: %s", m_filename.c_str (),
+ strerror (errno));
+ }
+
+ //printf ("mgStream::fileinfo: stat64 != 0 for %s\n",
+ // m_filename.c_str ());
+ }
+
+ return false;
+}
diff --git a/muggle-plugin/vdr_stream.h b/muggle-plugin/vdr_stream.h
new file mode 100644
index 0000000..6b1769c
--- /dev/null
+++ b/muggle-plugin/vdr_stream.h
@@ -0,0 +1,60 @@
+/*!
+ * \file vdr_stream.h
+ * \brief Definitions of media streams
+ *
+ * \version $Revision: 1.2 $
+ * \date $Date$
+ * \author Ralf Klueber, Lars von Wedel, Andreas Kellner
+ * \author Responsible author: $Author$
+ *
+ * $Id$
+ *
+ * Adapted from
+ * MP3/MPlayer plugin to VDR (C++)
+ * (C) 2001-2003 Stefan Huelswitt <huels@iname.com>
+ */
+
+#ifndef ___STREAM_H
+#define ___STREAM_H
+
+#include <string>
+
+#include "vdr_decoder.h"
+
+class cNet;
+
+// ----------------------------------------------------------------
+
+class mgStream // : public mgFileInfo
+{
+ private:
+ int m_fd;
+ bool m_ismmap;
+
+// from cFileInfo
+ std::string m_filename, m_fsID;
+ unsigned long long m_filesize;
+ time_t m_ctime;
+ long m_fsType;
+
+ bool fileinfo (bool log);
+ bool removable ();
+
+ protected:
+ unsigned char *m_buffer;
+ unsigned long long m_readpos, m_buffpos;
+ unsigned long m_fill;
+ public:
+ mgStream (std::string filename);
+ virtual ~ mgStream ();
+ virtual bool open (bool log = true);
+ virtual void close ();
+ virtual bool stream (unsigned char *&data, unsigned long &len,
+ const unsigned char *rest = NULL);
+ virtual bool seek (unsigned long long pos = 0);
+ virtual unsigned long long bufferPos ()
+ {
+ return m_buffpos;
+ }
+};
+#endif //___STREAM_H