diff options
Diffstat (limited to 'muggle-plugin')
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 ¤t, 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 |