From d45bc4bed686d1d0c2daff22a66dfa0394fde4fb Mon Sep 17 00:00:00 2001 From: Joerg Bornkessel Date: Sat, 26 Apr 2014 19:15:40 +0200 Subject: initial commit --- COPYING | 340 ++++++++++++++++++++++++ HISTORY | 90 +++++++ METAGS.TAG | 0 Makefile.obsolete | 136 ++++++++++ README | 58 +++++ a-tools.h | 22 ++ audioreceiver.c | 259 ++++++++++++++++++ audioreceiver.h | 63 +++++ audiorecorder.c | 345 ++++++++++++++++++++++++ audiorecorder.h | 63 +++++ browse-item.c | 69 +++++ browse-item.h | 44 ++++ browse.c | 638 +++++++++++++++++++++++++++++++++++++++++++++ browse.h | 71 +++++ cache.c | 232 +++++++++++++++++ cache.h | 55 ++++ column.c | 29 +++ column.h | 58 +++++ contrib/audiorecorder.conf | 12 + contrib/postproc.pl | 144 ++++++++++ contrib/set_path | 35 +++ convert.c | 154 +++++++++++ convert.h | 37 +++ dispatcher.c | 435 +++++++++++++++++++++++++++++++ dispatcher.h | 93 +++++++ external-player.h | 15 ++ mainmenu.c | 90 +++++++ mainmenu.h | 29 +++ mpa-frame.c | 55 ++++ mpa-frame.h | 24 ++ po/de_DE.po | 240 +++++++++++++++++ postdata.c | 135 ++++++++++ postdata.h | 62 +++++ postproc.c | 380 +++++++++++++++++++++++++++ postproc.h | 48 ++++ push-test | 0 rds.c | 386 +++++++++++++++++++++++++++ rds.h | 60 +++++ recstat.h | 17 ++ service.h | 27 ++ setup.c | 104 ++++++++ setup.h | 56 ++++ trackinfo.c | 360 +++++++++++++++++++++++++ trackinfo.h | 69 +++++ xml-base.c | 104 ++++++++ xml-base.h | 37 +++ xml-cache.c | 280 ++++++++++++++++++++ xml-cache.h | 33 +++ 48 files changed, 6093 insertions(+) create mode 100644 COPYING create mode 100644 HISTORY create mode 100644 METAGS.TAG create mode 100644 Makefile.obsolete create mode 100644 README create mode 100644 a-tools.h create mode 100644 audioreceiver.c create mode 100644 audioreceiver.h create mode 100644 audiorecorder.c create mode 100644 audiorecorder.h create mode 100644 browse-item.c create mode 100644 browse-item.h create mode 100644 browse.c create mode 100644 browse.h create mode 100644 cache.c create mode 100644 cache.h create mode 100644 column.c create mode 100644 column.h create mode 100644 contrib/audiorecorder.conf create mode 100755 contrib/postproc.pl create mode 100755 contrib/set_path create mode 100644 convert.c create mode 100644 convert.h create mode 100644 dispatcher.c create mode 100644 dispatcher.h create mode 100644 external-player.h create mode 100644 mainmenu.c create mode 100644 mainmenu.h create mode 100644 mpa-frame.c create mode 100644 mpa-frame.h create mode 100644 po/de_DE.po create mode 100644 postdata.c create mode 100644 postdata.h create mode 100644 postproc.c create mode 100644 postproc.h delete mode 100644 push-test create mode 100644 rds.c create mode 100644 rds.h create mode 100644 recstat.h create mode 100644 service.h create mode 100644 setup.c create mode 100644 setup.h create mode 100644 trackinfo.c create mode 100644 trackinfo.h create mode 100644 xml-base.c create mode 100644 xml-base.h create mode 100644 xml-cache.c create mode 100644 xml-cache.h diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..5b6e7c6 --- /dev/null +++ b/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. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 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. + + , 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/HISTORY b/HISTORY new file mode 100644 index 0000000..34f28ca --- /dev/null +++ b/HISTORY @@ -0,0 +1,90 @@ +VDR Plugin 'audiorecorder' Revision History +------------------------------------------- + +2006-06-16: Version 0.1.0-pre1 + +- Initial revision. + +2006-06-26: Version 0.1.0-pre2 + +- replaced localtime with thread-safe localtime_r (cAudioreceiver::get_path()). +- try to catch wrong rtp-tags (cRds::correct_rtp_tags()). +- add support for reencoding into mp3 format. + +2006-06-26: Version 0.1.0-pre3 + +- a small bugfix in cRds::decode_rtp(). + +2006-07-17: Version 0.1.0-pre4 + +- complete rewrite of the recording stuff. now there are up to !!! 12 !!! + parallel recordings possible (cDispatcher()). +- better/more checks of the rtp_tags (cRds::correct_rtp_tag()). +- fixed a bug in cRds::set_next_frame(). +- some cosmetic changes. +- introduced service-interface for communication with other plugins + (you can use the header file service.h). +- reduced the verbosity of the plugin. debugging could be enabled on stdout with + the commandline argument -d or --debug=. + +2006-09-01: Version 0.1.0-pre5 + +- added casts to uint64_t to avoid an integer overflow (suggested by egal & + lordjaxom on www.vdrportal.de, cDispatcher::check_free_disc_space()). +- added "min. free disc space (in mb)" to setup menu. the recording is stopped + if the free space on the recording-directory is smaller than this value + (suggested by egal on www.vdrportal.de). +- added "max. tracks in queue" to setup menu. the recording is stopped if this + value is achived. recording is startet again at 25 % of this value + (suggested by egal on www.vdrportal.de). +- now artist and title are converted to capital letters (for better detecting + of doubled recordings). +- after switching a channel, the next receiver is attached after at minimum + 30 seconds (thx to andipiel from www.vdrportal.de, cDispatcher()). +- removed track.c and track.h +- introduced caching (xml-base.[c,h], xml-cache.[c,h] and cache.[c,h]). + tinyxml is used for the xml-handling, so thx a lot to the authors ... + ... look at http://www.grinninglizard.com/tinyxml/ for more informations. +- introduced menu and browser (menu.[c,h], browse[c,h], browse-item[c,h]) +- code cleanup, replaced most c-functions with the according c++/stl function. +- add "pause if osd is open" to setup menu. +- moved hr3, hr1 and swr3 to the end of the channels-list (dispatcher.c) +- added possible album, track or year-values (trackinfo.h) + +2006-09-05: Version 0.1.0-pre6 + +- fixed a possible crash in cXmlCache::copy_to_objects() +- added the namespace "a-land" to tinyxml to avoid a segfault when used together + with the setup plugin/path + ++HISTORY.magicamun + +2007-03-08: Version 0.1.0-pre6a + +- adjustable setup for "UpperCase" or not in the naming of the song. +- 3 possible namings of songs introduced, subdirectories are possible too. +- adjustable number of copies of songs on disk. +- preparation for setting of channels via name/provider. + +2007-03-25: Version 0.1.0-pre6b +- config file "audiorecorder.conf" in the plugin-directory of your + VDR added, to setup the used channels. + +2007-03-25: Version 0.1.0-pre6c +- multilanguage support (at the moment only english and german) +- Adjusting/correcting the cache-functions to allow subdirectories +- Reading of the xml-cache reprogrammed as a thread - to speed up + the start of the VDR. + +2007-05-26: Version 0.1.0-pre7 (for 1.4.x) +2007-05-26: Version 0.1.0-pre8 (für 1.5.x) +- optional naming of the songs via external script. + +2007-06-08: Version 0.1.0-pre9 +- the plugin is now acting better if the audiorecorder.conf is missing. +- caches only the defined codecs (mp2, mp3). +- relative pathes in the xml-cache. +- crash fixed in the script post processing if there is no string returned. +- channel (#5) and event (#6) added to the parameter list of the + external script, bitrates are now given correctly. + diff --git a/METAGS.TAG b/METAGS.TAG new file mode 100644 index 0000000..e69de29 diff --git a/Makefile.obsolete b/Makefile.obsolete new file mode 100644 index 0000000..16c6eac --- /dev/null +++ b/Makefile.obsolete @@ -0,0 +1,136 @@ +# +# 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 = audiorecorder + +### The version number of this plugin (taken from the main source file): + +VERSION = $(shell grep 'const char \*cPluginAudiorecorder::VERSION *=' $(PLUGIN).c | awk '{ print $$5 }' | sed -e 's/[";]//g') + +### The C++ compiler and options: + +CXX ?= g++ +CXXFLAGS ?= -fPIC -g -O2 -Wall -Woverloaded-virtual + +### The directory environment: + +VDRDIR = ../../.. +LIBDIR = ../../lib +TMPDIR = /tmp + +FFMDIR = /usr/include + +### Allow user defined options to overwrite defaults: + +-include $(VDRDIR)/Make.config + +### The version number of VDR's plugin API (taken from VDR's "config.h"): + +APIVERSION = $(shell sed -ne '/define APIVERSION/s/^.*"\(.*\)".*$$/\1/p' $(VDRDIR)/config.h) + +### The name of the distribution archive: + +ARCHIVE = $(PLUGIN)-$(VERSION) +PACKAGE = vdr-$(ARCHIVE) + +### Includes, Libs and Defines (add further entries here): + +INCLUDES += -I$(VDRDIR)/include -I$(DVBDIR)/include $(shell taglib-config --cflags) + +LIBS = $(shell taglib-config --libs) + +DEFINES += -D_GNU_SOURCE -DPLUGIN_NAME_I18N='"$(PLUGIN)"' -DTIXML_USE_STL + +ifdef AUDIORECORDER_DEVEL + DEFINES += -DAUDIORECORDER_DEVEL +endif + +# ffmpeg +AVCODEC_INC = $(shell pkg-config libavcodec --cflags | sed -e 's/ //g') +ifeq ($(strip $(AVCODEC_INC)),) + INCLUDES += -I$(FFMDIR) -I$(FFMDIR)/libavcodec +else + INCLUDES += $(AVCODEC_INC) +endif +AVCODEC_LIBS = $(shell pkg-config libavcodec --libs) +ifeq ($(strip $(AVCODEC_LIBS)),) + LIBS += -lavcodec +else + LIBS += $(AVCODEC_LIBS) +endif + +### The object files (add further files here): + +OBJS = $(PLUGIN).o mainmenu.o browse.o browse-item.o column.o dispatcher.o audioreceiver.o postdata.o trackinfo.o postproc.o rds.o convert.o cache.o xml-cache.o xml-base.o setup.o mpa-frame.o tinyxml/tinyxml.o tinyxml/tinyxmlerror.o tinyxml/tinyxmlparser.o audiorecorder_i18n.o + +### Implicit rules: + +%.o: %.c + $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) -o $@ $< + +# Dependencies: + +MAKEDEP = $(CXX) -MM -MG +DEPFILE = .dependencies +$(DEPFILE): Makefile + @$(MAKEDEP) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@ + +-include $(DEPFILE) + +### Internationalization (I18N): + +PODIR = po +LOCALEDIR = $(VDRDIR)/locale +I18Npo = $(wildcard $(PODIR)/*.po) +I18Nmo = $(addsuffix .mo, $(foreach file, $(I18Npo), $(basename $(file)))) +I18Ndirs = $(notdir $(foreach file, $(I18Npo), $(basename $(file)))) +I18Npot = $(PODIR)/$(PLUGIN).pot +I18Nvdrmo = vdr-$(PLUGIN).mo +ifeq ($(strip $(APIVERSION)),1.5.7) + I18Nvdrmo = $(PLUGIN).mo +endif + +%.mo: %.po + msgfmt -c -o $@ $< + +$(I18Npot): $(OBJS:%.o=%.c) + xgettext -C -cTRANSLATORS --no-wrap -F -k -ktr -ktrNOOP --msgid-bugs-address='' -o $@ $(OBJS:%.o=%.c) + +$(I18Npo): $(I18Npot) + msgmerge -U --no-wrap -F --backup=none -q $@ $< + +i18n: $(I18Nmo) + @mkdir -p $(LOCALEDIR) + for i in $(I18Ndirs); do\ + mkdir -p $(LOCALEDIR)/$$i/LC_MESSAGES;\ + cp $(PODIR)/$$i.mo $(LOCALEDIR)/$$i/LC_MESSAGES/$(I18Nvdrmo);\ + done + +generate-i18n: i18n-template.h $(I18Npot) $(I18Npo) buildutil/pot2i18n.pl + buildutil/pot2i18n.pl $(I18Npot) i18n-template.h > i18n-generated.h + +### Targets: + +all: libvdr-$(PLUGIN).so i18n + +libvdr-$(PLUGIN).so: $(OBJS) + $(CXX) $(CXXFLAGS) -shared $(OBJS) $(LIBS) -o $@ + @cp $@ $(LIBDIR)/$@.$(APIVERSION) + +dist: clean + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a * $(TMPDIR)/$(ARCHIVE) + @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(PACKAGE).tgz + +clean: + @-rm -f $(PODIR)/*.mo $(PODIR)/*.pot + @-rm -f $(OBJS) $(DEPFILE) *.so *.tgz core* *~ diff --git a/README b/README new file mode 100644 index 0000000..cddbd9c --- /dev/null +++ b/README @@ -0,0 +1,58 @@ +This is a "plugin" for the Video Disk Recorder (VDR). + +Written by: herbert attenberger + +Project's homepage: + +Latest version available at: + +See the file COPYING for license information. + +Description: +Floods your disc with music! This plugin records songs with artist, title and ID3-Tag. The naming and cutting of the songs uses the "Radiotext plus" feature of the ARD channels "DAS MODUL", Eins_Live, YOU_FM, WDR_2, SWR3 etc., as long as the vdr is tuned to the transponder or if there is a unused DVB-card. + +As codec you can choose between the native format mp2 MPEG-1_Audio_Layer_2 and mp3 (mit adjustable bitrate). More codecs are planned for the future. + +Additionally you can add a fade-in and/or fade-out effect, to reduce unwanted noises at the beginning or the end of the song. + +TODO: Add other recording mechanisms, e.g. PayRadio channels with title/artist in the EPG as Premiere, DMX, .... + +Parameters: +-r dir, --recdir=dir set recording-directory to 'dir'. +-d level, --debug=level set debugging level (default: 0), + all stuff is written to stdout. + 0 = off + 1 = only errors + 2 = errors and further infos + ++ Patches to the original plugin + +Written by: magicamun + +Description: +Additions to the original plugin are: + +- adjustable setup for "UpperCase" or not in the naming of the song. +- 3 possible namings of songs introduced, subdirectories are possible too. +- adjustable number of copies of songs on disk. +- config file "audiorecorder.conf" in the plugin-directory of your vdr added, to setup the used channels. +- multilanguage support (at the moment only english and german) +- Reading of the xml-cache reprogrammed as a thread - to speed up the start of the VDR. +- optional naming of the songs via external script. +- relative pathes in the xml-cache. + +Parameters: +-r dir, --recdir=dir set recording-directory to 'dir'. +-d level, --debug=level set debugging level (default: 0), + all stuff is written to stdout. + 0 = off + 1 = only errors + 2 = errors and further infos +-p script, --postproc=script set script (default: none) + for postprocessing. + +Installation: +Additional to the installation hints of the pre5, copy the "audiorecorder.conf" in the plugin-directory of your VDR. + +Others: +Other recording mechanism via title/artist in the EPG for Premiere programmed by Frankman (search for P-Rex) diff --git a/a-tools.h b/a-tools.h new file mode 100644 index 0000000..4a00eda --- /dev/null +++ b/a-tools.h @@ -0,0 +1,22 @@ +/* + * a-tools.h: + */ + + +#ifndef __A_TOOLS_H +#define __A_TOOLS_H + +#define KB(x) x * 1024 +#define MB(x) KB(x) * 1024 + +#define DELETE(x) if (x != NULL) { delete x; x = NULL; } + +typedef unsigned char uchar; + +typedef struct abuffer { + uchar *data; + int length; + int offset; +} abuffer; + +#endif /* __A_TOOLS_H */ diff --git a/audioreceiver.c b/audioreceiver.c new file mode 100644 index 0000000..43c8723 --- /dev/null +++ b/audioreceiver.c @@ -0,0 +1,259 @@ +/* + * audioreceiver.c + */ + +#include "audioreceiver.h" +#include "postproc.h" +#include "audiorecorder.h" + + +/* --- cAudioReceiver ------------------------------------------------------- */ + +cAudioReceiver::cAudioReceiver(const cChannel *_channel) +:cReceiver(_channel->GetChannelID(), -2, _channel->Apid(0)), cThread() +{ + channel = _channel; + + active = false; + + pes_sync = false; + device_number = -1; + recstat = recWait; + + buffer = NULL; + rds = NULL; + postdata = NULL; + + dsyslog("[audiorecorder]: receiver for channel <%s> created on pid %d " + "(%s, %s())", channel->Name(), channel->Apid(0), __FILE__, + __func__); +} + + +cAudioReceiver::~cAudioReceiver(void) +{ + Activate(false); + + DELETE(buffer); + DELETE(rds); + DELETE(postdata); +} + + +void cAudioReceiver::Receive(uchar *data, int length) +{ + int ts_header_len, header_len, data_len, rb_len; + + if (! active || data[0] != 0x47) + return; + + /* check adaptation field */ + switch (data[3] & 0x30) { + case 0x10: + /* no adaptation field, payload only */ + ts_header_len = 4; + break; + case 0x30: + /* adaptation field followed by payload */ + ts_header_len = data[4] + 5; + break; + default: + return; + } + + header_len = ts_header_len; + + /* check payload unit start indicator */ + if ((data[1] & 0x40) == 0x40) { + /* ts-header is followed by a pes-header */ + header_len += data[ts_header_len + 8] + 9; + pes_sync = true; + } + + if (! pes_sync) + return; + + /* put mp2-data into the ringbuffer */ + data_len = length - header_len; + rb_len = buffer->Put(data + header_len, data_len); + + if (data_len != rb_len) { + buffer->ReportOverflow(data_len - rb_len); + dsyslog("[audiorecorder]: buffer overflow (%s, %s())", __FILE__, + __func__); + buffer->Clear(); + } +} + + +void cAudioReceiver::Activate(bool on) +{ + if (on) { + if (! active) { + active = true; + Start(); + } + } + else if (active) { + active = false; + Cancel(3); + } +} + + +void cAudioReceiver::Action(void) +{ + abuffer input_buf; + + dsyslog("[audiorecorder]: receiving from channel <%s> (%s, %s())", + channel->Name(), __FILE__, __func__); + + buffer = new cRingBufferLinear(KB(100), KB(5), true, NULL); + postdata = new cPostData(channel); + rds = new cRds(postdata); + + while (active) { + input_buf.data = buffer->Get(input_buf.length); + input_buf.offset = 0; + + if (! input_buf.data) { + usleep(10000); + continue; + } + + + get_mpa_frame(&input_buf, &mpa_frame, channel->Name()); + + while (active && mpa_frame.data) { + set_recstat_rds(); + + if (outfile.is_open()) + outfile.write((char *)mpa_frame.data, + mpa_frame.length); + + input_buf.offset += mpa_frame.length; + get_mpa_frame(&input_buf, &mpa_frame, channel->Name()); + } + + buffer->Del(input_buf.offset); + } + + if (outfile.is_open()) { + outfile.close(); + + if (! postdata->get_recpath().empty()) + remove(postdata->get_recpath().c_str()); + } + + + pes_sync = false; + device_number = -1; + recstat = recWait; + + DELETE(rds); + DELETE(postdata); + DELETE(buffer); + + dsyslog("[audiorecorder]: stopped receiving from channel <%s> " + "(%s, %s())", channel->Name(), __FILE__, __func__); +} + + +bool cAudioReceiver::is_attached(int attached_device_number) +{ + if (device_number < 0) + return false; + + if (attached_device_number != device_number && + attached_device_number != -1) + return false; + + return true; +} + + +bool cAudioReceiver::is_recording(void) +{ + if (recstat != recStart && recstat != recRun) + return false; + + return true; +} + + +void cAudioReceiver::set_recstat_rds(void) +{ + rds->put_data(mpa_frame.data, mpa_frame.length); + + while(rds->set_next_frame()) { + recstat = rds->decode_frame(); + + control_track(); + } +} + + +void cAudioReceiver::control_track(void) +{ +//LT + struct stat dir; + std::string recdir; +// + switch (recstat) { + case recStart: + if (outfile.is_open()) + outfile.close(); + + postdata->start_track(); +//LT + recdir = cPluginAudiorecorder::get_recdir(); + stat(recdir.c_str(), &dir); + + if (! (dir.st_mode & S_IFDIR)) { + dsyslog("[audiorecorder]: %s (given with the parameter " + " --recdir|-r) isn't a directory", recdir.c_str()); + return; + } + + if (access(recdir.c_str(), R_OK | W_OK) != 0) { + dsyslog("[audiorecorder]: can't access %s (given with the " + "parameter --recdir|-r)", recdir.c_str()); + return; + } +// + outfile.open(postdata->get_recpath().c_str()); +//LT + if (!outfile.is_open()) + return; +// + dsyslog("[audiorecorder]: started recording track (%s) on <%s> " + "(%s, %s())", postdata->get_recpath().c_str(), + channel->Name(), __FILE__, __func__); + break; + case recStop: + if (outfile.is_open()) + outfile.close(); + + dsyslog("[audiorecorder]: stopped recording track (%s) on <%s> " + "(%s, %s())", postdata->get_recpath().c_str(), + channel->Name(), __FILE__, __func__); + + postdata->stop_track(mpa_frame.bit_rate, mpa_frame.sample_rate, + mpa_frame.channels, mpa_frame.length); + + if (postdata->get_path().empty()) { + remove(postdata->get_recpath().c_str()); + dsyslog("[audiorecorder]: track (%s) removed" + " (%s, %s())", + postdata->get_recpath().c_str(), + __FILE__, __func__); + } else { + cPostproc::add_track(postdata); + } + postdata->clear(); + + break; + default: + break; + } +} diff --git a/audioreceiver.h b/audioreceiver.h new file mode 100644 index 0000000..33a6776 --- /dev/null +++ b/audioreceiver.h @@ -0,0 +1,63 @@ +/* + * audioreceiver.h + */ + +#ifndef __AUDIORECEIVER_H +#define __AUDIORECEIVER_H + +#include "postdata.h" +#include "rds.h" +#include "mpa-frame.h" +#include "recstat.h" +#include "a-tools.h" + +#include +#include +#include + +#include +#include + + +class cAudioReceiver : public cReceiver, cThread { +private: + bool active; + int device_number; + + std::ofstream outfile; + + cRingBufferLinear *buffer; + const cChannel *channel; + + cPostData *postdata; + cRds *rds; + + eRecStat recstat; + mpeg_audio_frame mpa_frame; + bool pes_sync; + + void set_recstat_rds(void); + void control_track(void); +protected: + virtual void Receive(uchar *data, int length); + + virtual void Action(void); + virtual void Activate(bool on); +public: + cAudioReceiver(const cChannel *_channel); + ~cAudioReceiver(); + + void set_device_number(int _device_number) { device_number = + _device_number; } + int get_device_number(void) { return device_number; } + bool is_attached(int attached_device_number); + /* + * returns true if the receiver is attached to the given device. + * if attached_device_number == -1, true is returned if the + * receiver is attached to a device + */ + bool is_recording(void); + const cChannel *get_channel(void) { return channel; } +}; + +#endif /* __AUDIORECEIVER_H */ diff --git a/audiorecorder.c b/audiorecorder.c new file mode 100644 index 0000000..6dd0fb4 --- /dev/null +++ b/audiorecorder.c @@ -0,0 +1,345 @@ +/* + * audiorecorder.c: A plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + + +#include "audiorecorder.h" +#include "setup.h" +#include "mainmenu.h" +#include "service.h" + +#include + +#include +#include +#include + +#include + +extern "C" { + #include +} + +// extern cChannels Channels; + +using namespace std; + +/* --- cPluginAudiorecorder ------------------------------------------------- */ + +const char *cPluginAudiorecorder::DESCRIPTION = tr("floods your disc with music"); +const char *cPluginAudiorecorder::VERSION = "2.0.0_pre1"; + +string cPluginAudiorecorder::recdir; +int cPluginAudiorecorder::debug = 0; +string cPluginAudiorecorder::cfg; +string cPluginAudiorecorder::pscript; + +cPluginAudiorecorder::cPluginAudiorecorder(void) +{ + dispatcher = NULL; + postproc = NULL; +} + + +cPluginAudiorecorder::~cPluginAudiorecorder() +{ + delete dispatcher; + delete postproc; +} + + +const char *cPluginAudiorecorder::CommandLineHelp(void) +{ + return " -r dir, --recdir=dir set recording-directory to 'dir'.\n" + " -d level, --debug=level set debugging level (default: 0),\n" + " all stuff is written to stdout.\n" + " 0 = off\n" + " 1 = only errors\n" + " 2 = errors and further infos\n" + " -p script, --postproc=script set script (default: none)\n" + " for postprocessing.\n"; + +} + + +bool cPluginAudiorecorder::ProcessArgs(int argc, char *argv[]) +{ + int c, option_index = 0; +// struct stat dir; + + static struct option long_options[] = { + { "recdir", required_argument, NULL, 'r' }, + { "debug", required_argument, NULL, 'd' }, + { "postproc", required_argument, NULL, 'p' }, + { NULL } + }; + + c = getopt_long(argc, argv, "r:v:d:p:", long_options, &option_index); + + while (c != -1) { + switch (c) { + case 'r': + recdir = optarg; + break; + case 'd': + if (atoi(optarg) > 0) + debug = atoi(optarg); + break; + case 'p': + pscript = optarg; + break; + default: + return false; + } + + c = getopt_long(argc, argv, "r:v:d:p:", long_options, + &option_index); + } + + if (recdir.empty()) { + cerr << endl << "audiorecorder: missing parameter --recdir|-r " + << endl; + dsyslog("audiorecorder: missing parameter --recdir|-r"); + return false; + } + + if (recdir[recdir.length() - 1] != '/') + recdir.append("/"); +//LT +/* + stat(recdir.c_str(), &dir); + + if (! (dir.st_mode & S_IFDIR)) { + cerr << endl << "audiorecorder: " << recdir << " (given with " + "the parameter --recdir|-r) isn't a directory" << endl; + dsyslog("[audiorecorder]: %s (given with the parameter " + " --recdir|-r) isn't a directory", recdir.c_str()); + return false; + } + + if (access(recdir.c_str(), R_OK | W_OK) != 0) { + cerr << endl << "audiorecorder: can't access " << recdir << + " (given with the parameter --recdir|-r)" << endl; + dsyslog("[audiorecorder]: can't access %s (given with the " + "parameter --recdir|-r)", recdir.c_str()); + return false; + } +*/ +// + dsyslog("[audiorecorder]: external path-script : %s (%s, %s())", pscript.c_str(), __FILE__, __func__); + + + + return true; +} + + +bool cPluginAudiorecorder::Initialize(void) +{ + /* Initialize any background activities the plugin shall perform. */ + + cfg.append(strdup(ConfigDirectory(PLUGIN_NAME_I18N))); + cfg.append("/audiorecorder.conf"); + + RegisterI18n(Phrases); + + audio_codecs[0] = "mp2"; + audio_codecs[1] = "libmp3lame"; + audio_codecs[2] = "mp3"; + + audio_codecs_translated[0] = tr("mp2"); + audio_codecs_translated[1] = tr("mp3"); + audio_codecs_translated[2] = tr("mp3"); + + fade_types[0] = tr("off"); + fade_types[1] = tr("linear"); + fade_types[2] = tr("exponential"); + + views[0] = tr("all"); + views[1] = tr("by artist"); + views[2] = tr("by channel"); + views[3] = tr("by date"); + + file_patterns[0] = tr("Artist/Title"); + file_patterns[1] = tr("Artist - Title"); + file_patterns[2] = tr("Channel/Artist/Title"); + file_patterns[3] = tr("Channel - Artist - Title"); + + return true; +} + + +bool cPluginAudiorecorder::Start(void) +{ + /* initialize libavcodec */ + avcodec_init(); + avcodec_register_all(); + + probe_audio_codecs(); + + Cache.load(); + + dispatcher = new cDispatcher(); + postproc = new cPostproc(); + + return true; +} + + +void cPluginAudiorecorder::Stop(void) +{ + /* Stop any background activities the plugin shall perform. */ +} + + +void cPluginAudiorecorder::Housekeeping(void) +{ + /* Perform any cleanup or other regular tasks. */ +} + + +const char *cPluginAudiorecorder::MainMenuEntry(void) +{ + main_menu.str(""); + main_menu.clear(); + + main_menu << tr("Audiorecorder") << " (" + << (dispatcher->is_active() ? tr("on") : tr("off")) + << ") " << dispatcher->get_recording_receivers() + << "/" << dispatcher->get_attached_receivers(-1) + << "/" << SetupValues.max_receivers + << ", " << postproc->get_num_queued(); + + return main_menu.str().c_str(); +} + + +cOsdObject *cPluginAudiorecorder::MainMenuAction(void) +{ + return new cMainmenu(dispatcher); +} + + +cString cPluginAudiorecorder::Active(void) +{ + /* + if (postproc->get_num_queued() != 0) + return "active postprocessings"; + */ + return NULL; +} + + +cMenuSetupPage *cPluginAudiorecorder::SetupMenu(void) +{ + return new cAudiorecorderSetup(); +} + + +bool cPluginAudiorecorder::SetupParse(const char *name, const char *value) +{ + if (strcmp(name, "start_type") == 0) + SetupValues.start_type = atoi(value); + else if (strcmp(name, "max_receivers") == 0) + SetupValues.max_receivers = atoi(value); + else if (strcmp(name, "min_free_space") == 0) + SetupValues.min_free_space = atoi(value); + else if (strcmp(name, "pause") == 0) + SetupValues.pause = atoi(value); + else if (strcmp(name, "max_postproc") == 0) + SetupValues.max_postproc = atoi(value); + else if (strcmp(name, "default_view") == 0) + SetupValues.default_view = atoi(value); + else if (strcmp(name, "fade_in") == 0) + SetupValues.fade_in = atoi(value); + else if (strcmp(name, "fade_in_mode") == 0) + SetupValues.fade_in_mode = atoi(value); + else if (strcmp(name, "fade_out") == 0) + SetupValues.fade_out = atoi(value); + else if (strcmp(name, "fade_out_mode") == 0) + SetupValues.fade_out_mode = atoi(value); + else if (strcmp(name, "audio_codec") == 0) { + int c; + + for (c = 0; c < SetupValues.num_audio_codecs; ++c) { + if (strcmp(value, audio_codecs_translated[c]) == 0) { + SetupValues.audio_codec = c; + break; + } + } + } + else if (strcmp(name, "bit_rate") == 0) + SetupValues.bit_rate = atoi(value); + else if (strcmp(name, "file_pattern") == 0) + SetupValues.file_pattern = atoi(value); + else if (strcmp(name, "upper") == 0) + SetupValues.upper = atoi(value); + else if (strcmp(name, "copies") == 0) + SetupValues.copies = atoi(value); + else + return false; + + return true; +} + + +bool cPluginAudiorecorder::Service(const char *id, void *data) +{ + if (! id) + return false; + + if (strcmp(id, "Audiorecorder-StatusRtpChannel-v1.0") == 0) { + if (data) { + struct Audiorecorder_StatusRtpChannel_v1_0 *d; + d = (struct Audiorecorder_StatusRtpChannel_v1_0 *)data; + d->status = + dispatcher->get_recording_status(d->channel); + } + return true; + } + + return false; +} + +const char **cPluginAudiorecorder::SVDRPHelpPages(void) +{ + /* Return help text for SVDRP commands this plugin implements */ + return NULL; +} + + +cString cPluginAudiorecorder::SVDRPCommand(const char *command, + const char *option, int &reply_code) +{ + /* Process SVDRP commands this plugin implements */ + return NULL; +} + + +void cPluginAudiorecorder::probe_audio_codecs() { + int c; + const char *tmp; + AVCodec *codec = NULL; + + for (c = 1; c < SetupValues.num_audio_codecs; ++c) { + codec = avcodec_find_encoder_by_name(audio_codecs[c]); + if (codec) + continue; + + dsyslog("[audiorecorder]: your version of libavcodec (ffmpeg) " + "is not compiled with %s support (%s, %s())", audio_codecs[c], __FILE__, __func__); + + tmp = audio_codecs[c]; + audio_codecs[c] = + audio_codecs[SetupValues.num_audio_codecs - 1]; + audio_codecs[SetupValues.num_audio_codecs - 1] = tmp; + --SetupValues.num_audio_codecs; + } +} + + +VDRPLUGINCREATOR(cPluginAudiorecorder); /* Don't touch this! */ diff --git a/audiorecorder.h b/audiorecorder.h new file mode 100644 index 0000000..ce49c76 --- /dev/null +++ b/audiorecorder.h @@ -0,0 +1,63 @@ +/* + * audiorecorder.h + */ + +#ifndef __AUDIORECORDER_H +#define __AUDIORECORDER_H + +#include "dispatcher.h" +#include "postproc.h" + +#include + +#include +#include + +#include "audiorecorder_i18n.h" + +class cPluginAudiorecorder : public cPlugin { +private: + cDispatcher *dispatcher; + cPostproc *postproc; + + static const char *DESCRIPTION; + static const char *VERSION; + + static std::string recdir; + static int debug; + + static std::string cfg; + static std::string pscript; + + std::stringstream main_menu; + + void probe_audio_codecs(void); +public: + cPluginAudiorecorder(void); + virtual ~cPluginAudiorecorder(); + virtual const char *Version(void) { return VERSION; } + virtual const char *Description(void) { return DESCRIPTION; } + 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 cString Active(void); + virtual cMenuSetupPage *SetupMenu(void); + virtual bool SetupParse(const char *name, const char *value); + virtual bool Service(const char *id, void *data = NULL); + virtual const char **SVDRPHelpPages(void); + virtual cString SVDRPCommand(const char *command, const char *option, + int &reply_code); + + static const char *get_version(void) { return VERSION; } + static std::string get_recdir(void) { return recdir; } + static int get_dbg_level(void) { return debug; } + static std::string get_cfg(void) { return cfg; } + static std::string get_pscript(void) { return pscript; } +}; + +#endif /* __AUDIORECORDER_H */ diff --git a/browse-item.c b/browse-item.c new file mode 100644 index 0000000..5b59923 --- /dev/null +++ b/browse-item.c @@ -0,0 +1,69 @@ +/* + * browse-item.c + */ + +#include "browse-item.h" + +#include + + +using namespace std; + +/* --- cBrowseItem ---------------------------------------------------------- */ + +cBrowseItem::cBrowseItem(cBrowseItem *_main_item, const cTrackInfo *_track, + int _column, eItemType _type) +:cOsdItem() +{ + main_item = _main_item; + if (main_item) + main_item->increase_items(); + + track = _track; + column = _column; + type = _type; + + items = 0; +} + + +void cBrowseItem::increase_items(void) +{ + ++items; + + if (main_item) + main_item->increase_items(); +} + + +void cBrowseItem::delete_items(int del_items) +{ + items -= del_items; + + if (main_item) + main_item->delete_items(del_items); + + if (items < 0) + items = 0; +} + + +void cBrowseItem::toggle_node(void) +{ + string txt = Text(); + + if (type == itemNodeOpen) { + type = itemNodeClose; + string::size_type f = txt.find_first_of("-"); + if (f != string::npos) + txt.replace(f, 1, "+"); + } + else if (type == itemNodeClose) { + type = itemNodeOpen; + string::size_type f = txt.find_first_of("+"); + if (f != string::npos) + txt.replace(f, 1, "-"); + } + + SetText(txt.c_str()); +} diff --git a/browse-item.h b/browse-item.h new file mode 100644 index 0000000..9da3452 --- /dev/null +++ b/browse-item.h @@ -0,0 +1,44 @@ +/* + * browse-item.h + */ + +#ifndef __BROWSE_ITEM_H +#define __BROWSE_ITEM_H + +#include "trackinfo.h" + +#include + + +enum eItemType { + itemTrack, + itemNodeOpen, + itemNodeClose, +}; + +class cBrowseItem : public cOsdItem { +private: + const cTrackInfo *track; + cBrowseItem *main_item; + int column; + eItemType type; + int items; +public: + cBrowseItem(cBrowseItem *_main_item, const cTrackInfo *_track, + int _column, eItemType _type); + + void increase_items(void); + void delete_items(int del_items); + void toggle_node(void); + + const cTrackInfo *get_track(void) const { return track; } + const cBrowseItem *get_main_item(void) const { return main_item; } + int get_column(void) const { return column; } + int get_items(void) const { return items; } + + bool is_node(void) const { return (type != itemTrack); } + bool is_open(void) const { return (type == itemNodeOpen); } + bool is_closed(void) const { return (type == itemNodeClose); } +}; + +#endif /* __BROWSE_ITEM__H */ diff --git a/browse.c b/browse.c new file mode 100644 index 0000000..0e7bac3 --- /dev/null +++ b/browse.c @@ -0,0 +1,638 @@ +/* + * browse.c + */ + +#include "browse.h" +#include "cache.h" +#include "external-player.h" +#include "audiorecorder.h" +#include "setup.h" + +#include +#include +#include + +#include + + +#define MAXCOLS 5 +#define TAB_LEN 3 +#define SERVICE "MP3-Play-v1" + +using namespace std; + +/* --- cBrowse -------------------------------------------------------------- */ + +cBrowse::cBrowse(void) +:cOsdMenu("") +{ + num_columns = MAXCOLS; + columns = NULL; + columns = new cColumn[num_columns + 1]; + + player = cPluginManager::CallFirstService(SERVICE); + + string skin = Skins.Current()->Name(); + width = DisplayMenu()->EditableWidth(); + + if (skin != "curses") + width /= 12; + + dsyslog("[audiorecorder]: skin %s (width %d) detected (%s, %s())", + skin.c_str(), width, __FILE__, __func__); + + set_view(SetupValues.default_view); +} + + +cBrowse::~cBrowse() +{ + delete[] columns; +} + + +eOSState cBrowse::ProcessKey(eKeys key) +{ + eOSState state = cOsdMenu::ProcessKey(key); + + if (HasSubMenu()) { + process_submenu_states(state); + + if (state == osBack || key == kBlue || key == kOk) + CloseSubMenu(); + + return osContinue; + } + + set_status(); + set_help_keys(); + + cBrowseItem *item = get_actual_item(); + + switch (key) { + case kRed: + if (! item) + break; + + if (item->is_node()) + AddSubMenu(new cBrowseAction(true, + get_category(item))); + else + AddSubMenu(new cBrowseAction(false, + get_trackname(item->get_track()))); + + break; + case kGreen: + if (help_green.empty()) + break; + + expand ? expand = false : expand = true; + set_view(view, false); + + break; + case kYellow: + set_view(++view); + + break; + case kBlue: + if (item && ! item->is_node()) + AddSubMenu(new cBrowseInfo(item->get_track())); + + break; + case kOk: + if (! item) + break; + + if (! item->is_node()) + play_file(item->get_track()); + else if (item->is_closed()) { + item->toggle_node(); + insert_items(); + } + else { + item->toggle_node(); + delete_items(); + } + break; + default: + break; + } + + return state; +} + + +void cBrowse::set_view(int _view, bool init) +{ + view = _view; + + if (view == 1) { + /* view by artist */ + num_columns = 2; + + columns[0].set(colArtist, TAB_LEN); + columns[1].set(colTitle, width - TAB_LEN); + columns[2].set(colEnd); + + if (init) + expand = false; + + help_yellow = views[2]; + } + else if (view == 2) { + /* view by channel */ + num_columns = 3; + + columns[0].set(colChannel, TAB_LEN); + columns[1].set(colEvent, TAB_LEN); + columns[2].set(colArtistTitle, width - 2 * TAB_LEN); + columns[3].set(colEnd); + + if (init) + expand = false; + + help_yellow = views[3]; + } + else if (view == 3) { + /* view by date */ + num_columns = 2; + + columns[0].set(colDate, TAB_LEN); + columns[1].set(colTime, 6, true); + columns[2].set(colChannel, 11, true, true); + columns[3].set(colArtistTitle, width - TAB_LEN - 17); + columns[4].set(colEnd); + + if (init) + expand = true; + + help_yellow = views[0]; + } + else { + /* all */ + view = 0; + num_columns = 1; + + columns[0].set(colArtistTitle, width); + columns[1].set(colEnd); + + if (init) + expand = true; + + help_yellow = views[1]; + } + + if (init) { + SetCols(columns[0].get_width(), columns[1].get_width(), + columns[2].get_width(), columns[3].get_width(), + columns[4].get_width()); + + Cache.sort(columns); + } + + Clear(); + insert_items(); + + set_status(); + set_help_keys(); + set_title(); +} + + +void cBrowse::set_help_keys(void) +{ + /* green key */ + if (view == 0) + help_green.erase(); + else if (expand) + help_green = tr("collapse all"); + else + help_green = tr("expand all"); + + cBrowseItem *item = get_actual_item(); + + SetHelp(tr("action"), (help_green.empty() ? NULL : help_green.c_str()) , + help_yellow, (item && ! item->is_node() ? "info" : NULL)); +} + + +void cBrowse::set_status(void) +{ + /* disable status for the skin sttng: */ + if (strcmp(Skins.Current()->Name(), "sttng") == 0) + return; + + string status(views[view]); + + cBrowseItem *item = get_actual_item(); + + if (item && item->get_main_item()) { + status.append(" "); + status.append(get_category(item)); + } + + cut_string(status, width); + SetStatus(status.c_str()); +} + + +void cBrowse::set_title(void) +{ + stringstream title; + title << tr("Audiorecorder") << ", " << tr("Browse tracks") << " (" << Cache.get_num_cached() + << ")"; + + SetTitle(title.str().c_str()); +} + + +void cBrowse::process_submenu_states(eOSState &state) +{ + cBrowseItem *item = get_actual_item(); + + switch(state) { + case osUser1: + play_all_files(item); + state = osBack; + + break; + case osUser6: + if (item) + play_file(item->get_track()); + + state = osBack; + + break; + default: + break; + } +} + + +cBrowseItem *cBrowse::get_actual_item(void) +{ + if (Current() < 0) + return NULL; + + return (cBrowseItem *)Get(Current()); +} + + +void cBrowse::insert_items(void) +{ + int pos = Current(); + + /* clean up old states */ + for (int c = 0; c < num_columns; ++c) { + columns[c].set_main_item(NULL); + columns[c].del_last_entry(); + } + + int column = 0; + cBrowseItem *item = get_actual_item(); + if (item && item->is_node()) { + set_filter(item); + column = item->get_column(); + columns[column].set_main_item(item); + ++column; + } + + int depth; + if (expand) + depth = num_columns; + else + depth = column + 1; + + for (const cTrackInfo *track = Cache.get_next_track(true); track; + track = Cache.get_next_track()) { + + if (filter_track(track)) + continue; + + for (int c = column; c < depth; ++c) { + eItemType type; + + string entry = get_value_of_column(track, c); + + if (c == num_columns - 1) + type = itemTrack; + else if (expand) + type = itemNodeOpen; + else + type = itemNodeClose; + + /* don't show double nodes: */ + if (type != itemTrack && + entry == columns[c].get_last_entry()) + continue; + + columns[c].set_last_entry(entry); + for (int i = c + 1; i < num_columns; ++i) + columns[i].del_last_entry(); + + if (type == itemNodeOpen) + entry.insert(0, "[-] "); + else if (type == itemNodeClose) + entry.insert(0, "[+] "); + + string indent(c, '\t'); + entry.insert(0, indent); + + cBrowseItem *item, *main = NULL; + if (c > 0) + main = columns[c - 1].get_main_item(); + + item = new cBrowseItem(main, track, c, type); + item->SetText(entry.c_str()); + columns[c].set_main_item(item); + + Add(item, true, Get(Current())); + } + } + + SetCurrent(Get(pos)); + Display(); +} + + +void cBrowse::delete_items(void) +{ + cBrowseItem *item = get_actual_item(); + if (! item || ! item->is_node()) + return; + + for (int c = 0; c < item->get_items(); ++c) + Del(Current() + 1); + + item->delete_items(item->get_items()); + + Display(); +} + + +void cBrowse::set_filter(const cBrowseItem *item) +{ + for (int c = 0; c < num_columns; ++c) { + string filter(""); + if (c <= item->get_column()) + filter = get_value_of_column(item->get_track(), c); + + columns[c].set_filter(filter); + } +} + + +bool cBrowse::filter_track(const cTrackInfo *track) +{ + for (int c = 0; c < num_columns; ++c) { + + if (columns[c].get_filter().empty()) + continue; + + if (columns[c].get_filter() != get_value_of_column(track, c)) + return true; + } + + return false; +} + + +string cBrowse::get_category(const cBrowseItem *item) +{ + string category(""); + + if (item) { + int columns = item->get_column(); + + if (item->is_node()) + ++columns; + + for (int c = 0; c < columns; ++c) { + if (c != 0) + category.append("/"); + + category.append(get_value_of_column(item->get_track(), + c)); + } + } + + return category; +} + + +string cBrowse::get_trackname(const cTrackInfo *track) +{ + string trackstr(""); + + if (track) { + trackstr.append(track->get_artist()); + trackstr.append(" - "); + trackstr.append(track->get_title()); + } + + return trackstr; +} + + +string cBrowse::get_value_of_column(const cTrackInfo *track, int c) +{ + string value(""); + bool join = true; + + while (join && columns[c].get_type() != colEnd) { + stringstream col_tmp; + + switch (columns[c].get_type()) { + case colArtist: + col_tmp << track->get_artist(); + break; + case colTitle: + col_tmp << track->get_title(); + break; + case colArtistTitle: + col_tmp << track->get_artist() << " - " + << track->get_title(); + break; + case colAlbum: + col_tmp << track->get_album(); + break; + case colTrack: + col_tmp << track->get_track(); + break; + case colYear: + col_tmp << track->get_year(); + break; + case colGenre: + col_tmp << track->get_genre(); + break; + case colChannel: + col_tmp << track->get_channel(); + break; + case colEvent: + col_tmp << track->get_event(); + break; + case colDate: + col_tmp << track->get_date(); + break; + case colTime: + col_tmp << track->get_time(); + break; + default: + break; + } + + if (col_tmp.str().empty()) + col_tmp << tr("unknown"); + + string col(col_tmp.str()); + + if (columns[c].get_cut()) + cut_string(col, columns[c].get_width() - 1); + + value.append(col); + + join = columns[c].is_joined(); + if (join) + value.append("\t"); + + ++c; + } + + return value; +} + + +void cBrowse::play_all_files(const cBrowseItem *node) +{ + if (! player) { + Skins.Message(mtError, tr("No external player-plugin found"), 2); + return; + } + + if (! node || ! node->is_node()) + return; + + set_filter(node); + + string status = tr("Playing all tracks in "); + status.append(get_category(node)); + cut_string(status, width); + SetStatus(status.c_str()); + + for (const cTrackInfo *track = Cache.get_next_track(true); track; + track = Cache.get_next_track()) { + + if (! filter_track(track)) + play_file(track, false); + } +} + + +void cBrowse::play_file(const cTrackInfo *track, bool set_status) +{ + if (! player) { + Skins.Message(mtError, tr("No external player-plugin found"), 2); + return; + } + + MP3ServiceData data; + data.data.filename = track->get_path().c_str(); + data.result = 0; + +#ifndef AUDIORECORDER_DEVEL + player->Service(SERVICE, &data); + + if (data.result == 0) { + dsyslog("[audiorecorder]: plugin %s could not play the file " + "%s (%s, %s())", player->Name(), + track->get_path().c_str(), __FILE__, __func__); + return; + } +#endif /* AUDIORECORDER_DEVEL */ + + if (set_status) { + string status = tr("Playing "); + status.append(get_trackname(track)); + cut_string(status, width); + SetStatus(status.c_str()); + } +} + + +void cBrowse::cut_string(string &cut, int length) +{ + if ((int)cut.length() > length) { + cut.erase(length - 1); + cut.append("~"); + } +} + + +/* --- cBrowseAction -------------------------------------------------------- */ + +cBrowseAction::cBrowseAction(bool node, const string &text) +:cOsdMenu(tr("Audiorecorder, Action")) +{ + string status(""); + if (node) { + status.append(tr("Category : ")); + status.append(text); + Add(new cOsdItem(tr("Play all tracks"), osUser1)); + Add(new cOsdItem(tr("More to come ..."), osUser7)); + } + else { + status.append(tr("Track: ")); + status.append(text); + Add(new cOsdItem(tr("Play track"), osUser6)); + Add(new cOsdItem(tr("More to come ..."), osUser7)); + } + + SetStatus(status.c_str()); + SetHelp(NULL, NULL, NULL, tr("back")); +} + + +/* --- cBrowseInfo ---------------------------------------------------------- */ + +cBrowseInfo::cBrowseInfo(const cTrackInfo *track) +:cOsdMenu(tr("Audiorecorder, Info"), 11) +{ + string path = track->get_path(); + path.erase(0, cPluginAudiorecorder::get_recdir().length()); + Add(add_item(tr("File"), path)); + + Add(add_item(tr("Artist"), track->get_artist())); + Add(add_item(tr("Title"), track->get_title())); + Add(add_item(tr("Album"), track->get_album())); + Add(add_item(tr("Genre"), track->get_genre())); + + stringstream tmp; + tmp << track->get_track(); + Add(add_item(tr("Track"), tmp.str())); + + tmp.str(""); + tmp.clear(); + tmp << track->get_year(); + Add(add_item(tr("Year"), tmp.str())); + + Add(add_item(tr("Channel"), track->get_channel())); + Add(add_item(tr("Event"), track->get_event())); + Add(add_item(tr("Date"), track->get_date())); + Add(add_item(tr("Time"), track->get_time())); + + SetHelp(NULL, NULL, NULL, tr("back")); +} + + +cOsdItem *cBrowseInfo::add_item(const char *type, const string &text) +{ + string txt = type; + txt.append(":\t"); + + if (text.empty() || text == "0") + txt.append(tr("unknown")); + else + txt.append(text); + + return new cOsdItem(txt.c_str(), osUnknown, false); +} diff --git a/browse.h b/browse.h new file mode 100644 index 0000000..0789a72 --- /dev/null +++ b/browse.h @@ -0,0 +1,71 @@ +/* + * browse.h + */ + +#ifndef __BROWSE_H +#define __BROWSE_H + +#include "column.h" +#include "trackinfo.h" +#include "browse-item.h" + +#include +#include + +#include + + +class cBrowse : public cOsdMenu { +private: + const char *help_yellow; + std::string help_green; + int view, num_columns, width; + bool expand; + cColumn *columns; + cPlugin *player; + + void set_view(int _view, bool init = true); + void set_help_keys(void); + void set_status(void); + void set_title(void); + + void process_submenu_states(eOSState &state); + + cBrowseItem *get_actual_item(void); + void insert_items(void); + void delete_items(void); + + void set_filter(const cBrowseItem *item); + + bool filter_track(const cTrackInfo *track); + + std::string get_category(const cBrowseItem *item); + std::string get_trackname(const cTrackInfo *track); + std::string get_value_of_column(const cTrackInfo *track, int c); + + void play_all_files(const cBrowseItem *node); + void play_file(const cTrackInfo *track, bool set_status = true); + + void cut_string(std::string &cut, int length); +public: + cBrowse(void); + ~cBrowse(); + + virtual eOSState ProcessKey(eKeys key); +}; + + +class cBrowseAction : public cOsdMenu { +public: + cBrowseAction(bool node, const std::string &text); +}; + + +class cBrowseInfo : public cOsdMenu { +private: + cOsdItem *add_item(const char *type, const std::string &text); +public: + cBrowseInfo(const cTrackInfo *track); +}; + +#endif /* __BROWSE__H */ diff --git a/cache.c b/cache.c new file mode 100644 index 0000000..66f1bef --- /dev/null +++ b/cache.c @@ -0,0 +1,232 @@ +/* + * cache.c + */ + +#include "cache.h" +#include "audiorecorder.h" + +#include +#include + + +#define CACHEFILE "cache.xml" + +using namespace std; + +cCache Cache; + +/* --- cCache --------------------------------------------------------------- */ + +const cColumn *cCache::sort_order = NULL; + +cCache::cCache() +:cThread() +{ + what_to_do = 0; + active = false; +} + + +cCache::~cCache() +{ + Activate(false); +} + + +void cCache::Activate(bool on) +{ + if (on) { + if (! active) { + active = true; + Start(); + } + } + else if (active) { + active = false; + Cancel(0); + } +} + + +void cCache::Action(void) +{ + if (what_to_do & REBUILD) { + dsyslog("[audiorecorder]: rebuilding cache thread started (%s, %s())", + __FILE__, __func__); + + Lock(); + tracklist.clear(); + xmlcache.rebuild(); + Unlock(); + + Skins.QueueMessage(mtInfo, tr("Audiorecorder: rebuilding cache finished")); + dsyslog("[audiorecorder]: rebuilding cache thread finished (%s, %s())", + __FILE__, __func__); + + what_to_do ^= REBUILD; + + active = false; + } else if (what_to_do & LOAD) { + string path = cPluginAudiorecorder::get_recdir(); + path.append(CACHEFILE); + + Lock(); + xmlcache.load(path); + Unlock(); + Skins.QueueMessage(mtInfo, tr("Audiorecorder: loading of cache finished")); + dsyslog("[audiorecorder]: loading of cache finished (%s, %s())", + __FILE__, __func__); + + what_to_do ^= LOAD; + active = false; + } +} + +/* +void cCache::load() +{ + string path = cPluginAudiorecorder::get_recdir(); + path.append(CACHEFILE); + + Lock(); + xmlcache.load(path); + Unlock(); +} +*/ + +void cCache::sort(const cColumn *columns) +{ + if (! columns) + return; + + sort_order = columns; + + Lock(); + tracklist.sort(cCache::sort_tracklist); + Unlock(); +} + + +void cCache::add_track(const cTrackInfo &trackinfo, bool add_xmlcache) +{ + if (trackinfo.get_artist().empty() || trackinfo.get_title().empty()) + return; + + Lock(); + tracklist.push_back(trackinfo); + + if (add_xmlcache) + xmlcache.add_track(trackinfo); + Unlock(); +} + + +int cCache::get_num_cached(void) +{ + int num; + Lock(); + num = tracklist.size(); + Unlock(); + + return num; +} + + +const cTrackInfo *cCache::get_next_track(bool reset) +{ + if (reset) + track = tracklist.begin(); + + if (track == tracklist.end()) + return NULL; + + cTrackInfo *trackinfo = &(*track); + ++track; + + return trackinfo; +} + + +bool cCache::sort_tracklist(const cTrackInfo &lhs, const cTrackInfo &rhs) +{ + for (int c = 0; sort_order[c].get_type() != colEnd; ++c) { + switch(sort_order[c].get_type()) { + case colArtist: + if (lhs.get_artist() > rhs.get_artist()) + return false; + if (lhs.get_artist() < rhs.get_artist()) + return true; + break; + case colTitle: + if (lhs.get_title() > rhs.get_title()) + return false; + if (lhs.get_title() < rhs.get_title()) + return true; + break; + case colArtistTitle: + if (lhs.get_artist() > rhs.get_artist()) + return false; + if (lhs.get_artist() < rhs.get_artist()) + return true; + if (lhs.get_title() > rhs.get_title()) + return false; + if (lhs.get_title() < rhs.get_title()) + return true; + break; + case colAlbum: + if (lhs.get_album() > rhs.get_album()) + return false; + if (lhs.get_album() < rhs.get_album()) + return true; + break; + case colTrack: + if (lhs.get_track() > rhs.get_track()) + return false; + if (lhs.get_track() < rhs.get_track()) + return true; + break; + case colYear: + if (lhs.get_year() > rhs.get_year()) + return false; + if (lhs.get_year() < rhs.get_year()) + return true; + break; + case colGenre: + if (lhs.get_genre() > rhs.get_genre()) + return false; + if (lhs.get_genre() < rhs.get_genre()) + return true; + break; + case colChannel: + if (lhs.get_channel() > rhs.get_channel()) + return false; + if (lhs.get_channel() < rhs.get_channel()) + return true; + break; + case colEvent: + if (lhs.get_event() > rhs.get_event()) + return false; + if (lhs.get_event() < rhs.get_event()) + return true; + break; + case colDate: + /* up-to-date date's first */ + if (lhs.get_date() > rhs.get_date()) + return true; + if (lhs.get_date() < rhs.get_date()) + return false; + break; + case colTime: + /* up-to-date time's first */ + if (lhs.get_time() > rhs.get_time()) + return true; + if (lhs.get_time() < rhs.get_time()) + return false; + break; + default: + break; + } + } + + return false; +} diff --git a/cache.h b/cache.h new file mode 100644 index 0000000..75023ea --- /dev/null +++ b/cache.h @@ -0,0 +1,55 @@ +/* + * cache.h + */ + +#ifndef __CACHE_H +#define __CACHE_H + +#include "xml-cache.h" +#include "trackinfo.h" +#include "column.h" + +#include + +#include +#include + +#define NOTHING 0 +#define LOAD 1 +#define REBUILD 2 + + +class cCache : public cThread { +private: + bool active; + int what_to_do; + + std::list tracklist; + std::list::iterator track; + + cXmlCache xmlcache; + + static const cColumn *sort_order; + + static bool sort_tracklist(const cTrackInfo & lhs, + const cTrackInfo &rhs); +protected: + virtual void Action(void); + virtual void Activate(bool on); +public: + cCache(void); + ~cCache(); + void load(void) { what_to_do ^= LOAD; Activate(true); } + void rebuild(void) { what_to_do ^= REBUILD; Activate(true); } + bool is_rebuilding(void) { return active; } + void sort(const cColumn *columns); + + void add_track(const cTrackInfo &trackinfo, bool add_xmlcache = true); + int get_num_cached(void); + + const cTrackInfo *get_next_track(bool reset = false); +}; + +extern cCache Cache; + +#endif /* __CACHE_H */ diff --git a/column.c b/column.c new file mode 100644 index 0000000..cd2ec84 --- /dev/null +++ b/column.c @@ -0,0 +1,29 @@ +/* + * column.c + */ + +#include "column.h" + + +/* --- cColumn -------------------------------------------------------------- */ + +cColumn::cColumn(void) +{ + set(colEnd); +} + + +void cColumn::set(eColumn _type, int _width, bool _join, bool _cut) +{ + type = _type; + width = _width; + if (width < 0) + width = 0; + + join = _join; + cut = _cut; + + filter.erase(); + last_entry.erase(); + item = NULL; +} diff --git a/column.h b/column.h new file mode 100644 index 0000000..4493814 --- /dev/null +++ b/column.h @@ -0,0 +1,58 @@ +/* + * column.h + */ + +#ifndef __COLUMN_H +#define __COLUMN_H + +#include "browse-item.h" + +#include + + +enum eColumn { + colEnd, + colArtist, + colTitle, + colArtistTitle, + colAlbum, + colTrack, + colYear, + colGenre, + colChannel, + colEvent, + colDate, + colTime +}; + + +class cColumn { +private: + eColumn type; + int width; + bool join; + bool cut; + std::string filter, last_entry; + cBrowseItem *item; +public: + cColumn(void); + + void set(eColumn _type, int _width = 0, bool _join = false, + bool _cut = false); + + void set_filter(std::string &_filter) { filter = _filter; } + void set_last_entry(std::string &_last_entry) + { last_entry = _last_entry; } + void del_last_entry(void) { last_entry.erase(); } + void set_main_item(cBrowseItem *_item) { item = _item; } + + eColumn get_type(void) const { return type; } + int get_width(void) const { return width; } + bool is_joined(void) const { return join; } + bool get_cut(void) const { return cut; } + const std::string &get_filter(void) const { return filter; } + const std::string &get_last_entry(void) const { return last_entry; } + cBrowseItem *get_main_item(void) const { return item; } +}; + +#endif /* __COLUMN_H */ diff --git a/contrib/audiorecorder.conf b/contrib/audiorecorder.conf new file mode 100644 index 0000000..548fe26 --- /dev/null +++ b/contrib/audiorecorder.conf @@ -0,0 +1,12 @@ +S19.2E-1-1093-28406 /* DAS MODUL */ +S19.2E-1-1093-28475 /* Eins Live */ +S19.2E-1-1093-28476 /* WDR 2 */ +S19.2E-1-1093-28468 /* SWR 3 */ +S19.2E-1-1093-28423 /* YOU FM */ +S19.2E-1-1093-28450 /* Bremen Vier */ +S19.2E-1-1093-28402 /* Bayern 3 */ +S19.2E-1-1093-28437 /* NDR 2 */ +S19.2E-1-1093-28440 /* N-JOY */ +S19.2E-1-1093-28420 /* hr2 */ +S19.2E-1-1093-28421 /* hr3 */ +S19.2E-1-1093-28419 /* hr1 */ diff --git a/contrib/postproc.pl b/contrib/postproc.pl new file mode 100755 index 0000000..eefbd32 --- /dev/null +++ b/contrib/postproc.pl @@ -0,0 +1,144 @@ +#!/usr/bin/perl +use strict; + +use File::Basename; + +my $LOGGER = "/usr/bin/logger -t " . basename(__FILE__); +my $mydir = dirname(__FILE__); + +# write into logfile for debugging purpose +my $logfile; +$logfile= $mydir . '/' . basename(__FILE__, '.pl') . '.log'; + +# function to write into syslog +sub Log +{ + system("$LOGGER '[audiorecorder]: @_'"); + print LOGFILE "# @_\n" if (defined($logfile)); +} + +if (defined($logfile)) { + # open file for debug + open(LOGFILE, ">> " . $logfile) + or die "Fehler beim Öffnen von '$logfile': $!\n"; + print LOGFILE "=========================== ". localtime() . "\n"; + print LOGFILE 'ARGV[0] = ' . $ARGV[0] . "\n"; + print LOGFILE 'ARGV[1] = ' . $ARGV[1] . "\n"; + print LOGFILE 'ARGV[2] = ' . $ARGV[2] . "\n"; + print LOGFILE 'ARGV[3] = ' . $ARGV[3] . "\n"; + print LOGFILE 'ARGV[4] = ' . $ARGV[4] . "\n"; + print LOGFILE 'ARGV[5] = ' . $ARGV[5] . "\n"; + print LOGFILE 'ARGV[6] = ' . $ARGV[6] . "\n"; + print LOGFILE 'ARGV[7] = ' . $ARGV[7] . "\n"; + print LOGFILE 'ARGV[8] = ' . $ARGV[8] . "\n"; + print LOGFILE 'ARGV[9] = ' . $ARGV[9] . "\n"; +} + +# test number of parameters +if ($#ARGV ne 7) { + Log("Wrong number of parameters"); + exit; +} + +# use telling names for parameters +my $src = $ARGV[0]; +my $src_bitrate = $ARGV[1]; +my $artist = $ARGV[2]; +my $title = $ARGV[3]; +my $channel = $ARGV[4]; +my $event = $ARGV[5]; +my $codec = $ARGV[6]; +my $dst_bitrate = $ARGV[7]; + +# test parameters +if ($artist eq "" || $title eq "") { + Log("Without data better remove this file"); + exit; +} + +# Create canonical filename +my $dst; +my $tmp = lc($artist . "_-_" . $title); +# clear nearly all non-characters +$tmp =~ s/[ \*\?!:"\']/_/g; +# converts slashes to commas +$tmp =~ s/\//,/g; +# convert special characters to ASCII +$tmp =~ s/[áàå]/a/g; +$tmp =~ s/ä/ae/g; +$tmp =~ s/ç/c/g; +$tmp =~ s/[éÉè]/e/g; +$tmp =~ s/[íï]/i/g; +$tmp =~ s/ñ/n/g; +$tmp =~ s/[óò]/o/g; +$tmp =~ s/ö/oe/g; +$tmp =~ s/ù/u/g; +$tmp =~ s/ü/ue/g; +# remove multiple underscores +$tmp =~ s/___*/_/g; +# remove underscore at file end +$tmp =~ s/_$//g; + +# Name with extension +$dst = $tmp . "." . $codec; + +# open blacklist +my $inname = $mydir . '/blacklist.m3u'; +if (open(BLACKLIST, "< " . $inname)) { + while () { + chomp; + if ($_ eq $dst) { + Log("File $dst blacklisted"); + exit; + } + } + close(BLACKLIST) +} + +# calculate sizes +my $src_size = (-s $src); +print LOGFILE "size($src) = " . $src_size . "\n" if (defined($logfile)); + +# No file smaller 8 secs +if ($src_size < $src_bitrate) { + Log("File $src too small ($src_size)!"); + exit; +} + +# Full qualified name +my $audiodir = dirname($src) . '/'; +my $fqn = $audiodir . $dst; + +# file already exists? +if (-e $fqn) { + # Write duplicate file under curtain circumstances + my $size = (-s $fqn); + print LOGFILE "size($dst) = " . $size . "\n" if (defined($logfile)); + # If file size is smaller than 17000 bytes this files are special + if ($size < 17000) { + Log("File already on CD"); + exit; + } + + my $dst_size = 0; + $dst_size = $src_size * $dst_bitrate / $src_bitrate if (!($src_bitrate == 0)); + print LOGFILE "approx. size($dst) = " . $dst_size . "\n" if (defined($logfile)); + + if ($size > $dst_size) { + Log("Already larger version on disk ($size vs. $dst_size)"); + exit; + } + + # Files larger than existing ones are stored with + # a special numbered extension + my $i = 0; + while (-e $fqn) { + $i++; + $dst = $tmp . ".~" . $i . "~." . $codec; + $fqn = $audiodir . $dst; + } + } + +print LOGFILE '$dst = ' . $dst . "\n" if (defined($logfile)); +# return file name +print $dst; diff --git a/contrib/set_path b/contrib/set_path new file mode 100755 index 0000000..6b8317b --- /dev/null +++ b/contrib/set_path @@ -0,0 +1,35 @@ +#! /bin/bash + +# To active this script, call the audiorecorder-plugin with the +# parameter -p plus the full path to this script, e.g. +# "-p /audio/set_path" if this script is stored in the /audio +# directory, it does not have to be in /usr/bin. Further you have +# to configure the plugin to use "extern" for the file names. + +# The parameters to ths script are +# $1. source file +# (e.g. "/audio/S19.2E-1-1093-28450-2007-04-13.16.15.37.tmp.mp2") +# $2. bitrate of the source file +# $3. artist +# $4. title +# $5. channel +# $6. event +# $7. used codec (e.g. "mp3") +# $8. set bitrate of the target + +# The output of the script has to be the file name of the target +# file. If the file already exists or if there is no output of +# this script, the audiorecorder plugin does not convert the recording +# but deletes it. + +# In this sample file, the file name of the target file is made +# of "_-_.<codec>". Additionally all characters are +# translated from upper to lower case as well as '/' translated to +# ',' and '*', '?' resp. ' ' to '_'. + +# You can do anything in this script: handle special characters, +# double underscores, etc. Test wether a song is already stored on CDs, +# use a blacklist for miserable artists, store multiple versions of +# recordings, ... Have a look at postproc.pl. + +echo -n "$3_-_$4.$7" | tr '[A-Z]/*? ' '[a-z],___' diff --git a/convert.c b/convert.c new file mode 100644 index 0000000..237f4c1 --- /dev/null +++ b/convert.c @@ -0,0 +1,154 @@ +/* + * convert.c + */ + +#include "convert.h" + +#include <vdr/tools.h> + + +/* --- cConvert ------------------------------------------------------------- */ + +cConvert::cConvert(const cPostData &postdata) +{ + encoder_buf.data = NULL; + encoder_buf.length = 0; + encoder_open = -1; + + decoder_buf.data = NULL; + decoder_buf.length = 0; + decoder_open = -1; + + init_decoder(); + init_encoder(audio_codecs[postdata.get_codec()], postdata.get_bit_rate(), + postdata.get_sample_rate(), postdata.get_channels()); +} + + +cConvert::~cConvert() +{ + delete[] encoder_buf.data; + delete[] decoder_buf.data; + + if (decoder_open > -1) { + avcodec_close(decoder_ctx); + av_free(decoder_ctx); + } + + if (encoder_open > -1) { + avcodec_close(encoder_ctx); + av_free(encoder_ctx); + } +} + + +void cConvert::init_decoder(void) +{ + decoder_codec = avcodec_find_decoder_by_name("mp2"); + if (! decoder_codec) { + dsyslog("[audiorecorder]: codec mp2 is not supported (%s, " + "%s())", __FILE__, __func__); + return; + } + + decoder_ctx = avcodec_alloc_context(); + decoder_open = avcodec_open(decoder_ctx, decoder_codec); + + if (decoder_open < 0) { + dsyslog("[audiorecorder]: could not open codec mp2 (%s, " + "%s())", __FILE__, __func__); + return; + } + + decoder_buf.data = new uchar[AVCODEC_MAX_AUDIO_FRAME_SIZE]; + + dsyslog("[audiorecorder]: decoder initialized (%s, %s())", __FILE__, + __func__); +} + + +void cConvert::init_encoder(const char *codec, int bit_rate, int sample_rate, + int channels) +{ + encoder_codec = avcodec_find_encoder_by_name(codec); + if (! encoder_codec) { + dsyslog("[audiorecorder]: codec %s is not supported (%s, " + "%s())", codec, __FILE__, __func__); + return; + } + + encoder_ctx = avcodec_alloc_context(); + + encoder_ctx->bit_rate = bit_rate; + encoder_ctx->sample_rate = sample_rate; + encoder_ctx->channels = channels; + + encoder_open = avcodec_open(encoder_ctx, encoder_codec); + + if (encoder_open < 0) { + dsyslog("[audiorecorder]: could not open codec %s (%s, %s())", codec, __FILE__, __func__); + return; + } + + encoder_buf.length = encoder_ctx->frame_size; + encoder_buf.data = new uchar[encoder_buf.length]; + + dsyslog("[audiorecorder]: encoder for %s-codec (br: %d, sr: %d, %d ch) " + "initialized (%s, %s())", encoder_codec->name, + encoder_ctx->bit_rate, encoder_ctx->sample_rate, + encoder_ctx->channels, __FILE__, __func__); +} + + +void cConvert::decode_mpa_frame(mpeg_audio_frame *mpa_frame) +{ + if (decoder_open < 0) { + decoder_buf.length = 0; + return; + } + +#if LIBAVCODEC_VERSION_INT < ((52<<16)+(0<<8)+0) + avcodec_decode_audio(decoder_ctx, (short *)decoder_buf.data, + &decoder_buf.length, mpa_frame->data, mpa_frame->length); +#else + decoder_buf.length = AVCODEC_MAX_AUDIO_FRAME_SIZE; + avcodec_decode_audio2(decoder_ctx, (short *)decoder_buf.data, + &decoder_buf.length, mpa_frame->data, mpa_frame->length); +#endif +} + + +abuffer *cConvert::reencode_mpa_frame(mpeg_audio_frame *mpa_frame, + float volume) +{ + int c, len; + short *tmp; + + if (decoder_open < 0 || encoder_open < 0) + return &encoder_buf; + + if (strcmp(encoder_codec->name, "mp2") == 0 && volume == 1) { + mpa_frame_buf.data = mpa_frame->data; + mpa_frame_buf.offset = mpa_frame->length; + return &mpa_frame_buf; + } + + decode_mpa_frame(mpa_frame); + + if (volume != 1) { + /* manipulate the volume */ + len = decoder_buf.length / sizeof(short); + tmp = (short *)decoder_buf.data; + + for (c = 0; c < len; ++c) { + *(tmp) = (short)((float)*(tmp) * volume); + ++tmp; + } + } + + encoder_buf.offset = avcodec_encode_audio(encoder_ctx, encoder_buf.data, + encoder_buf.length, (short *)decoder_buf.data); + /* encoder_buf.offset is used to save the size of the encoded frame */ + + return &encoder_buf; +} diff --git a/convert.h b/convert.h new file mode 100644 index 0000000..04fcc4b --- /dev/null +++ b/convert.h @@ -0,0 +1,37 @@ +/* + * convert.h + */ + +#ifndef __CONVERT_H +#define __CONVERT_H + +#include "postdata.h" +#include "mpa-frame.h" +#include "a-tools.h" + +extern "C" { +#include <avcodec.h> +} + + +class cConvert { +private: + AVCodec *decoder_codec, *encoder_codec; + AVCodecContext *decoder_ctx, *encoder_ctx; + int decoder_open, encoder_open; + + abuffer decoder_buf, encoder_buf, mpa_frame_buf; + + void init_decoder(void); + void decode_mpa_frame(mpeg_audio_frame *mpa_frame); + void init_encoder(const char *codec, int bit_rate, int sample_rate, + int channels); +public: + cConvert(const cPostData &postdata); + ~cConvert(); + + abuffer *reencode_mpa_frame(mpeg_audio_frame *mpa_frame, + float volume = 1); +}; + +#endif /* __CONVERT_H */ diff --git a/dispatcher.c b/dispatcher.c new file mode 100644 index 0000000..3b5ff14 --- /dev/null +++ b/dispatcher.c @@ -0,0 +1,435 @@ +/* + * dispatcher.c + */ + +#include "dispatcher.h" +#include "setup.h" +#include "audiorecorder.h" +#include "postproc.h" +#include "a-tools.h" + +#include <vdr/device.h> +#include <vdr/channels.h> +#include <vdr/plugin.h> + +#include <unistd.h> +#include <sys/statfs.h> +#include <unistd.h> + + +#define COUNTER 30 + +cRecorderChannels RecorderChannels; + +using namespace std; + +/* --- cDispatcher----------------------------------------------------------- */ + +cDispatcher::cDispatcher(void) +:cThread(), cStatus() +{ + tChannelID channel_id; + int c; + + active = false; + + /* Load Channels to record */ + + RecorderChannels.Load(cPluginAudiorecorder::get_cfg().c_str()); + + if (RecorderChannels.Count() < 1) { + dsyslog("[audiorecorder]: you must have defined at least one channel for recording in %s (%s, %s()", cPluginAudiorecorder::get_cfg().c_str(), + __FILE__, __func__); + Skins.QueueMessage(mtInfo, tr("Audiorecorder: No Channels defined !")); + return; + } + + /* initialize one receiver for each channel */ + c = 0; + + for (cChannelList *cl = RecorderChannels.First(); cl; cl = RecorderChannels.Next(cl)) { + if (cl->Channel()) { + dsyslog("[audiorecorder]: Channel to be recorded # %d <%s>, <%s> %s (%s, %s())", + c, + cl->ChannelString(), + cl->Comment(), cl->Channel()->Name(), + __FILE__, __func__); + + if (cl->NewAudioReceiver()) + c++; + } + } + + dsyslog("[audiorecorder]: Number of Channels : %d - Number of Receivers : %d (%s, %s))", RecorderChannels.Count(), c, __FILE__, __func__); + + if (SetupValues.start_type == 1) + Activate(true); +} + + +cDispatcher::~cDispatcher() +{ + Activate(false); + + for (cChannelList *cl = RecorderChannels.First(); cl; cl = RecorderChannels.Next(cl)) { + if (cl->AudioReceiver()) { + cl->DeleteAudioReceiver(); + } + } +} + + +void cDispatcher::Activate(bool on) +{ + if (on) { + if (! active) { + active = true; + Start(); + } + } else if (active) { + active = false; + Cancel(3); + } +} + + +void cDispatcher::Action(void) +{ + bool interrupted = false; + counter = COUNTER; + + dsyslog("[audiorecorder]: dispatcher thread started (%s, %s())", + __FILE__, __func__); + + while (active) { + sleep(1); + + int num_queued = cPostproc::get_num_queued(); + + if (interrupted && num_queued > (SetupValues.max_postproc / 4)) + continue; + + interrupted = false; + + if (num_queued >= SetupValues.max_postproc) { + dsyslog("[audiorecorder]: max. postprocessings in queue" + " achieved (%d), detaching all receivers (%s, " + "%s())", num_queued, __FILE__, __func__); + + interrupted = true; + detach_receivers(-1, 0); + + continue; + } + + if (! check_free_disc_space()) { + dsyslog("[audiorecorder]: min. free disc space on %s " + "reached (%d mb), recording will be stopped " + "(%s, %s())", + cPluginAudiorecorder::get_recdir().c_str(), + SetupValues.min_free_space, __FILE__, + __func__); + + active = false; + continue; + } + + int receivers = get_attached_receivers(-1); + if (SetupValues.max_receivers > receivers) { + if (counter > 0) + counter--; + else + attach_receivers(); + } + else if (SetupValues.max_receivers < receivers) + detach_receivers(-1, SetupValues.max_receivers); + } + + detach_receivers(-1, 0); + + dsyslog("[audiorecorder]: dispatcher thread stopped (%s, %s())", + __FILE__, __func__); +} + + +void cDispatcher::ChannelSwitch(const cDevice *device, int channel_number) +{ + /* + * workaround to detach active audioreceivers if the attached device + * is switched to another transponder. this should be done inside of + * vdr, but there are situations which lead to emergency exits of vdr + * without this peace of code (for example if a recording starts). + * + */ + + int device_number; + + if (channel_number == 0) { + counter = COUNTER; + return; + } + + device_number = device->DeviceNumber(); + + if (get_attached_receivers(device_number) == 0) + return; + + if (Channels.GetByNumber(channel_number)->Transponder() == + get_transponder_of_first_receiver(device_number)) + return; + + detach_receivers(device_number, 0); +} + + +int cDispatcher::get_attached_receivers(int device_number) +{ + int count = 0; + + for (cChannelList *cl = RecorderChannels.First(); cl; cl = RecorderChannels.Next(cl)) { + if (cl->AudioReceiver()) { + if (cl->AudioReceiver()->is_attached(device_number)) { + ++count; + } + } + } + return count; +} + + +int cDispatcher::get_recording_receivers(void) +{ + int count = 0; + + for (cChannelList *cl = RecorderChannels.First(); cl; cl = RecorderChannels.Next(cl)) { + if (cl->AudioReceiver()) { + if (cl->AudioReceiver()->is_recording()) { + ++count; + } + } + } + return count; +} + + +int cDispatcher::get_recording_status(const cChannel *channel) +{ + for (cChannelList *cl = RecorderChannels.First(); cl; cl = RecorderChannels.Next(cl)) { + if (cl->AudioReceiver() && cl->AudioReceiver()->get_channel() == channel) { + if (cl->AudioReceiver()->is_recording()) + return 3; + else if (cl->AudioReceiver()->is_attached(-1)) + return 2; + else + return 1; + } + } + + return 0; +} + +int cDispatcher::get_no_of_channels(void) { + return RecorderChannels.Count(); +} + +void cDispatcher::detach_receivers(int detach_device_number, + int devices_remaining) +{ + int device_number; + cDevice *device = NULL; + + Lock(); + + for (cChannelList *cl = RecorderChannels.First(); cl; cl = RecorderChannels.Next(cl)) { + if (get_attached_receivers(-1) <= devices_remaining) + break; + + if (!cl->AudioReceiver()) + continue; + + device_number = cl->AudioReceiver()->get_device_number(); + + if (device_number < 0) + continue; + + if (detach_device_number != device_number && detach_device_number > 0) + continue; + + device = cDevice::GetDevice(device_number); + + if (! device) + continue; + + device->Detach(cl->AudioReceiver()); + } + Unlock(); +} + + +void cDispatcher::attach_receivers(void) +{ + cAudioReceiver *receiver; + cDevice *device; + bool needs_detach = false; + + Lock(); + + for (cChannelList *cl = RecorderChannels.First(); cl; cl = RecorderChannels.Next(cl)) { + if (get_attached_receivers(-1) >= SetupValues.max_receivers) + break; + + receiver = cl->AudioReceiver(); + + if (! receiver || receiver->is_attached(-1)) + continue; + + device = cDevice::GetDevice(receiver->get_channel(), 0, needs_detach); + + if (! device || (device && needs_detach)) + continue; + +#ifndef AUDIORCORDER_DEVEL + if (device == cDevice::ActualDevice() && + ! device->IsTunedToTransponder(receiver->get_channel())) + continue; +#endif + if (! device->IsTunedToTransponder(receiver->get_channel()) && + ! device->SwitchChannel(receiver->get_channel(), false)) + continue; + + receiver->set_device_number(device->DeviceNumber()); + device->AttachReceiver(receiver); + } + + Unlock(); +} + + +int cDispatcher::get_transponder_of_first_receiver(int device_number) +{ + int transponder = 0; + + for (cChannelList *cl = RecorderChannels.First(); cl; cl = RecorderChannels.Next(cl)) { + if (! cl->AudioReceiver()) + continue; + + if (cl->AudioReceiver()->is_attached(device_number)) { + transponder = cl->AudioReceiver()->get_channel()->Transponder(); + break; + } + } + + return transponder; +} + + +bool cDispatcher::check_free_disc_space(void) +{ + struct statfs stat_fs; + + if (statfs(cPluginAudiorecorder::get_recdir().c_str(), &stat_fs) != 0) + return false; + + if ((uint64_t)stat_fs.f_bavail * (uint64_t)stat_fs.f_bsize < + (uint64_t)SetupValues.min_free_space * (uint64_t)1024 * + (uint64_t)1024) + return false; + + return true; +} + +// -- cRecorderChannels -------------------------------------------------------------- + +bool cRecorderChannels::Load(const char *filename, bool dummy) +{ + dsyslog("[audiorecorder]: <%s> (%s, %s()", filename, __FILE__, __func__); + + if(cConfig < cChannelList >::Load(filename, true)) { + SetSource(First()); + return true; + } + return false; +} + +// -- cChannelList -------------------------------------------------------------- + +cChannelList::cChannelList(void) +{ + audioreceiver = NULL; +} + +cChannelList::cChannelList(const char *_channelstring, const char *_comment) +{ + Set(_channelstring, _comment); +} + +cChannelList::~cChannelList() +{ + if (audioreceiver) { + delete audioreceiver; + } + if (channelstring) { + free(channelstring); + } + if (comment) { + free(comment); + } +} + +void cChannelList::Set(const char *_channelstring, const char *_comment) +{ + channelstring = strdup(_channelstring); + comment = strdup(_comment); + channel_id = tChannelID::FromString(channelstring); + + if (! channel_id.Valid()) { + dsyslog("[audiorecorder]: channel %s is not valid (%s, %s())", channelstring, __FILE__, __func__); + } else { + channel = Channels.GetByChannelID(channel_id, true, true); + if (channel) { + dsyslog("[audiorecorder]: channel %s set (%s, %s())", channel->Name(), __FILE__, __func__); + } else { + dsyslog("[audiorecorder]: channel not set : %s %d, %d, %d, %d, %d (%s, %s())", channelstring, + channel_id.Source(), + channel_id.Nid(), + channel_id.Tid(), + channel_id.Sid(), + channel_id.Rid(), + __FILE__, __func__); + } + } +} + +cAudioReceiver *cChannelList::NewAudioReceiver() +{ + if (channel) { + audioreceiver = new cAudioReceiver(channel); + return audioreceiver; + } + return NULL; +} + +void cChannelList::DeleteAudioReceiver(void) { + if (audioreceiver) + delete audioreceiver; +} + +bool cChannelList::Parse(char *s) +{ + char cs[256], comment[256], *p1; + + int fields = sscanf(s, "%s", cs); + if (fields >= 1) { + p1 = s; + p1 += strlen(cs) + 1; + + strncpy(comment, p1, 255); + comment[255] = 0; + + Set(cs, comment); + return true; + } else { + return false; + } +} + diff --git a/dispatcher.h b/dispatcher.h new file mode 100644 index 0000000..0dde64d --- /dev/null +++ b/dispatcher.h @@ -0,0 +1,93 @@ +/* + * dispatcher.h + */ + +#ifndef __DISPATCHER_H +#define __DISPATCHER_H + +#include "audioreceiver.h" + +#include <vdr/device.h> +#include <vdr/thread.h> +#include <vdr/status.h> + +class cChannelList : public cListObject { + char *channelstring; + char *comment; + + tChannelID channel_id; + cChannel *channel; + + cAudioReceiver *audioreceiver; +public: + cChannelList(void); + cChannelList(const char *_channelstring, const char *_comment); + void Set(const char *_channelstring, const char *_comment); + cAudioReceiver *NewAudioReceiver(); + void DeleteAudioReceiver(void); + bool Parse(char *s); + const char *ChannelString(void) { return channelstring; } + const char *Comment(void) { return comment; } + cAudioReceiver *AudioReceiver(void) { return audioreceiver; } + cChannel *Channel(void) { return channel; } + ~cChannelList(); +}; + + +class cRecorderChannels : public cConfig<cChannelList> { + cChannelList *current; +public: + virtual bool Load(const char *filename, bool dummy=false); + void SetSource(cChannelList *source) { current=source; } + cChannelList *GetChannel(void) { return current; } +}; + +class cDispatcher : public cThread, cStatus { +private: + bool active; + int counter; + // cAudioReceiver **audioreceivers; + + void attach_receivers(void); + + int get_transponder_of_first_receiver(int device_number); + /* + * returns the transponder of the first attached audioreceiver on the + * given device + */ + + void detach_receivers(int detach_device_number, int devices_remaining); + /* + * detaches all audioreceivers on the given device_number, until + * devices_remaining are remaining. + * if detach_device_number == -1, all audioreceivers are + * detached. + */ + + bool check_free_disc_space(void); +protected: + virtual void Activate(bool on); + virtual void Action(void); + + virtual void ChannelSwitch(const cDevice *device, int channel_number); +public: + cDispatcher(void); + ~cDispatcher(); + + void stop(void) { Activate(false); } + void start(void) { Activate(true); } + bool is_active(void) { return active; } + + int get_recording_receivers(void); + int get_recording_status(const cChannel *channel); + int get_attached_receivers(int device_number); + int get_no_of_channels(void); + + /* + * returns the number of the attached audioreceivers on the given + * device. if device_number == -1, the number of all attached + * audioreceivers is returned. + */ +}; + +#endif /* __DISPATCHER_H */ diff --git a/external-player.h b/external-player.h new file mode 100644 index 0000000..84dce1c --- /dev/null +++ b/external-player.h @@ -0,0 +1,15 @@ +/* + * external-player.h + */ + +#ifndef __EXTERNAL_PLAYER_H +#define __EXTERNAL_PLAYER_H + +struct MP3ServiceData { + int result; + union { + const char *filename; + } data; + }; + +#endif //__EXTERNAL_PLAYER_H diff --git a/mainmenu.c b/mainmenu.c new file mode 100644 index 0000000..469686d --- /dev/null +++ b/mainmenu.c @@ -0,0 +1,90 @@ +/* + * mainmenu.c + */ + +#include "mainmenu.h" +#include "browse.h" +#include "dispatcher.h" +#include "audiorecorder.h" + +#include <sstream> + + +using namespace std; + +/* --- cMainmenu ------------------------------------------------------------ */ + +cMainmenu::cMainmenu(cDispatcher *_dispatcher) +:cOsdMenu("Audiorecorder, Mainmenu") +{ + dispatcher = _dispatcher; + active = dispatcher->is_active(); + + Clear(); + + Add(new cOsdItem(tr("Browse tracks"), osUser1)); + Add(new cOsdItem(active ? tr("Stop receiving") : tr("Start receiving"), + osUser2)); + Add(new cOsdItem(tr("Start rebuilding cache"), osUser3)); + + Display(); +} + + +eOSState cMainmenu::ProcessKey(eKeys key) +{ + eOSState state = cOsdMenu::ProcessKey(key); + + if (HasSubMenu()) + return osContinue; + + switch (state) { + case osBack: + state = osEnd; + break; + case osUser1: + if (Cache.is_rebuilding()) { + Skins.Message(mtStatus, tr("Rebuilding cache, try later")); + break; + } + + AddSubMenu(new cBrowse()); + + break; + case osUser2: + if (active) { + Add(new cOsdItem(tr("Start receiving"), osUser2), false, + Get(Current())); + dispatcher->stop(); + active = false; + } + else { + // Message if there are no channels to record + if (dispatcher->get_no_of_channels() < 1) { + char* tmp = NULL; + asprintf(&tmp, tr("no channel in %s"), + cPluginAudiorecorder::get_cfg().c_str()); + Skins.Message(mtStatus, tmp); + free(tmp); + return state; + } + Add(new cOsdItem(tr("Stop receiving"), osUser2), false, + Get(Current())); + dispatcher->start(); + active = true; + } + Del(Current()); + Display(); + + break; + case osUser3: + Cache.rebuild(); + Skins.Message(mtStatus, tr("Rebuilding cache")); + + break; + default: + break; + } + + return state; +} diff --git a/mainmenu.h b/mainmenu.h new file mode 100644 index 0000000..4482296 --- /dev/null +++ b/mainmenu.h @@ -0,0 +1,29 @@ +/* + * mainmenu.h + */ + +#ifndef __MAINMENU_H +#define __MAINMENU_H + +#include "dispatcher.h" +#include "cache.h" + +#include <vdr/osdbase.h> +#include <vdr/plugin.h> + +#include <string> + +#include "audiorecorder_i18n.h" + + +class cMainmenu : public cOsdMenu { +private: + bool active; + cDispatcher *dispatcher; +public: + cMainmenu(cDispatcher *_dispatcher); + virtual eOSState ProcessKey(eKeys key); +}; + + +#endif /* __MAINMENU_H */ diff --git a/mpa-frame.c b/mpa-frame.c new file mode 100644 index 0000000..1d7f03f --- /dev/null +++ b/mpa-frame.c @@ -0,0 +1,55 @@ +/* + * mpa-frame.c + */ + + +#include "mpa-frame.h" +#include "audiorecorder.h" + +#include <vdr/tools.h> + +#include <iostream> + + +using namespace std; + +const int sample_rates[] = { 44100, 48000, 32000, 0 }; +const int bit_rates[] = { 0, 32000, 48000, 56000, 64000, 80000, 96000, 112000, + 128000, 160000, 192000, 224000, 256000, 320000, 384000, 0 }; + +void get_mpa_frame(abuffer *buf, mpeg_audio_frame *mpa_frame, + const char *description) +{ + int c; + + mpa_frame->data = NULL; + + for (c = buf->offset; c < buf->length - buf->offset - 2; ++c) { + if ((buf->data[c] == 0xff) && (buf->data[c + 1] & 0xe0) && + (buf->data[c + 1] & 0x1e) == 0x1c) { + /* mpeg v1, layer II header found */ + mpa_frame->bit_rate = bit_rates[buf->data[c + 2] >> 4]; + mpa_frame->sample_rate = sample_rates[(buf->data[c + 2] + >> 2) & 0x03]; + if (mpa_frame->sample_rate < 1) + continue; + mpa_frame->channels = (buf->data[c + 3] >> 6) == 3 ? + 1 : 2; + int p = buf->data[c + 2] & 0x01; /* padding bit */ + mpa_frame->length = 144 * mpa_frame->bit_rate / + mpa_frame->sample_rate + p; + + if (c + mpa_frame->length <= buf->length) + mpa_frame->data = buf->data + c; + + break; + } + } + + if (c > buf->offset && cPluginAudiorecorder::get_dbg_level() > 0) + cout << "skipped " << c - buf->offset << " byte(s) " + << description << "> (" << __FILE__ << ", " << __func__ + << "())" << endl; + + buf->offset = c; +} diff --git a/mpa-frame.h b/mpa-frame.h new file mode 100644 index 0000000..7f4c4ef --- /dev/null +++ b/mpa-frame.h @@ -0,0 +1,24 @@ +/* + * mpa-frame.h + */ + +#ifndef __MPA_FRAME__H +#define __MPA_FRAME__H + +#include "a-tools.h" + + +typedef struct mpeg_audio_frame { + uchar *data; + int length; + int bit_rate; + int sample_rate; + int channels; +} mpeg_audio_frame; + +extern const int bit_rates[]; + +void get_mpa_frame(abuffer *buf, mpeg_audio_frame *mpa_frame, + const char *description); + +#endif /* __MPA_FRAME_H */ diff --git a/po/de_DE.po b/po/de_DE.po new file mode 100644 index 0000000..bc231d0 --- /dev/null +++ b/po/de_DE.po @@ -0,0 +1,240 @@ +# VDR plugin language source file. +# Copyright (C) 2007 Klaus Schmidinger <kls@cadsoft.de> +# This file is distributed under the same license as the VDR plugin package. +# Klaus Schmidinger <kls@cadsoft.de>, 2000 +# +msgid "" +msgstr "" +"Project-Id-Version: vdr-audiorecorder-2.0.0\n" +"Report-Msgid-Bugs-To: <cwieninger@gmx.de>\n" +"POT-Creation-Date: 2009-03-09 19:14+0100\n" +"PO-Revision-Date: 2007-08-19 08:20+0200\n" +"Last-Translator: Joerg Bornkessel <hd_brummy@gentoo.org>\n" +"Language-Team: <vdr@linuxtv.org>\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "floods your disc with music" +msgstr "" + +msgid "mp2" +msgstr "mp2" + +msgid "mp3" +msgstr "mp3" + +msgid "off" +msgstr "aus" + +msgid "linear" +msgstr "linear" + +msgid "exponential" +msgstr "exponentiell" + +msgid "all" +msgstr "alle" + +msgid "by artist" +msgstr "nach Künstler" + +msgid "by channel" +msgstr "nach Kanal" + +msgid "by date" +msgstr "nach Datum" + +msgid "Artist/Title" +msgstr "Künstler/Titel" + +msgid "Artist - Title" +msgstr "Künstler - Titel" + +msgid "Channel/Artist/Title" +msgstr "Kanal/Künstler/Titel" + +msgid "Channel - Artist - Title" +msgstr "Kanal - Künstler - Titel" + +msgid "Audiorecorder" +msgstr "Audiorecorder" + +msgid "on" +msgstr "an" + +msgid "collapse all" +msgstr "Alles zuklappen" + +msgid "expand all" +msgstr "Alles aufklappen" + +msgid "action" +msgstr "Aktion" + +msgid "Browse tracks" +msgstr "Titel durchsuchen" + +msgid "unknown" +msgstr "unbekannt" + +msgid "No external player-plugin found" +msgstr "Kein externes Player-Plugin gefunden" + +msgid "Playing all tracks in " +msgstr "Alle Titel spielen in " + +msgid "Playing " +msgstr "Spielt " + +msgid "Audiorecorder, Action" +msgstr "Audiorecorder, Aktion" + +msgid "Category : " +msgstr "Kategorie : " + +msgid "Play all tracks" +msgstr "Alle Titel spielen" + +msgid "More to come ..." +msgstr "Es kommt noch mehr ..." + +msgid "Track: " +msgstr "Titel: " + +msgid "Play track" +msgstr "Titel spielen" + +msgid "back" +msgstr "zurück" + +msgid "Audiorecorder, Info" +msgstr "Audiorecorder, Info" + +msgid "File" +msgstr "Datei" + +msgid "Artist" +msgstr "Künstler" + +msgid "Title" +msgstr "Titel" + +msgid "Album" +msgstr "Album" + +msgid "Genre" +msgstr "Genre" + +msgid "Track" +msgstr "Titel" + +msgid "Year" +msgstr "Jahr" + +msgid "Channel" +msgstr "Kanal" + +msgid "Event" +msgstr "Ereignis" + +msgid "Date" +msgstr "Datum" + +msgid "Time" +msgstr "Uhrzeit" + +msgid "Audiorecorder: rebuilding cache finished" +msgstr "Audiorecorder: Cache Neuaufbau fertig" + +msgid "Audiorecorder: loading of cache finished" +msgstr "Audiorecorder: Cache-Laden fertig" + +msgid "Audiorecorder: No Channels defined !" +msgstr "Audiorecorder: Keine Kanäle definiert !" + +msgid "Start receiving" +msgstr "Aufnahmemodus starten" + +msgid "Stop receiving" +msgstr "Aufnahmemodus beenden" + +msgid "Start rebuilding cache" +msgstr "Cache neu aufbauen" + +msgid "Rebuilding cache, try later" +msgstr "Cache wird neu aufgebaut - später nocheinmal versuchen" + +msgid "no channel in %s" +msgstr "Kein Kanal in %s" + +msgid "Rebuilding cache" +msgstr "Cache wird neu aufgebaut" + +#, fuzzy +msgid "Channel/Artist - Title" +msgstr "Kanal/Künstler - Titel" + +msgid "External" +msgstr "Extern" + +msgid "--- Receiver: ---" +msgstr "--- Aufnahme: ---" + +msgid "Receiving mode on start" +msgstr "Aufnahmemodus beim Start" + +msgid "Max. number of receivers" +msgstr "Max. Anzahl paralleler Aufnahmen" + +msgid "Min. free disc space (in mb)" +msgstr "Min. freier Plattenplatz (in MB)" + +msgid "--- Browser: ---" +msgstr "--- Browser: ---" + +msgid "Default view" +msgstr "Default Anzeige" + +msgid "--- Postprocessing: ---" +msgstr "--- Nachbearbeitung: ---" + +msgid "Pause if osd is opened" +msgstr "Pausieren wenn OSD offen ist" + +msgid "Max. tracks in queue" +msgstr "Max. Titel in Queue" + +msgid "Fade in mode" +msgstr "Fade-In Modus" + +msgid "Fade in seconds" +msgstr "Fade-In Sekunden" + +msgid "Fade out mode" +msgstr "Fade-Out Modus" + +msgid "Fade out seconds" +msgstr "Fade-Out Sekunden" + +msgid "Audio codec" +msgstr "Audiokodierung" + +msgid "Bitrate (if audio codec isn't <original>)" +msgstr "Bitrate (wenn Audiokodierung nicht original ist)" + +msgid "File Pattern" +msgstr "Dateimuster" + +msgid "Upper Case" +msgstr "Großschreibung" + +msgid "No" +msgstr "Nein" + +msgid "Yes" +msgstr "Ja" + +msgid "Copies" +msgstr "Kopien" diff --git a/postdata.c b/postdata.c new file mode 100644 index 0000000..0c7eef6 --- /dev/null +++ b/postdata.c @@ -0,0 +1,135 @@ +/* + * postdata.c + */ + +#include "postdata.h" +#include "setup.h" +#include "mpa-frame.h" +#include "audiorecorder.h" + +#include <vdr/epg.h> +#include <vdr/plugin.h> + +#include <iostream> +#include <sstream> +#include <time.h> + + +using namespace std; + +/* --- cPostData ------------------------------------------------------------ */ + +cPostData::cPostData(const cChannel *_channel) +:cTrackInfo() +{ + channel = _channel; + set_channel(channel->Name()); +} + + +cPostData::~cPostData() +{ +} + + +void cPostData::start_track(void) +{ + set_recpath_date_time(); + set_event(); + + file_pattern = SetupValues.file_pattern; + upper = SetupValues.upper; + copies = SetupValues.copies; + dsyslog("[audiorecorder]: (file_pattern : %d) (%s, %s())", file_pattern, __FILE__, __func__); +} + + +void cPostData::stop_track(int _bit_rate, int _sample_rate, int _channels, + int _len_mpa_frame) +{ + sample_rate = _sample_rate; + channels = _channels; + len_mpa_frame = _len_mpa_frame; + + set_codec(SetupValues.audio_codec); + + if (SetupValues.audio_codec == 0) { + bit_rate = _bit_rate; + } + else { + bit_rate = bit_rates[SetupValues.bit_rate + 1]; + } + + /* TODO: adjust this for other codecs then mp2/mp3 */ + frame_len = 144 * bit_rate / sample_rate; + + set_bit_rates(_bit_rate, bit_rate); + set_path(file_pattern, upper); + set_fade_values(); +} + + +void cPostData::set_recpath_date_time(void) +{ + time_t now; + struct tm *tm_now, tm_store; + char path_str[20], date_str[11], time_str[6]; + stringstream tmp; + + time(&now); + tm_now = localtime_r(&now, &tm_store); + + strftime(path_str, 20, "%Y-%m-%d.%H.%M.%S", tm_now); + strftime(date_str, 11, "%Y-%m-%d", tm_now); + strftime(time_str, 6, "%H.%M", tm_now); + +/* + tmp << cPluginAudiorecorder::get_recdir() + << *channel->GetChannelID().ToString() << "-" << path_str + << ".tmp.mp2"; +*/ + tmp << cPluginAudiorecorder::get_recdir() + << channel->Name() << "-" << path_str + << ".tmp.mp2"; + + set_recpath(tmp.str()); + set_date(date_str); + set_time(time_str); +} + + +void cPostData::set_event(void) +{ + cSchedulesLock sched_lock; + const cSchedules *schedules = cSchedules::Schedules(sched_lock); + if (! schedules) + return; + + const cSchedule *sched = schedules->GetSchedule(channel); + if (! sched) + return; + + const cEvent *event = sched->GetPresentEvent(); + if (! event) + return; + + cTrackInfo::set_event(event->Title()); +} + + +void cPostData::set_fade_values() +{ + fade_in_mode = SetupValues.fade_in_mode; + if (fade_in_mode == 0) + frames_fade_in = 0; + else + frames_fade_in = SetupValues.fade_in * (bit_rate / 8 / + frame_len); + + fade_out_mode = SetupValues.fade_out_mode; + if (fade_out_mode == 0) + frames_fade_out = 0; + else + frames_fade_out = SetupValues.fade_out * (bit_rate / 8 / + frame_len); +} diff --git a/postdata.h b/postdata.h new file mode 100644 index 0000000..6631e6f --- /dev/null +++ b/postdata.h @@ -0,0 +1,62 @@ +/* + * postdata.h + */ + +#ifndef __POSTDATA_H +#define __POSTDATA_H + +#include "trackinfo.h" + +#include <vdr/channels.h> + +#include <string> + + +class cPostData : public cTrackInfo { +private: + const cChannel *channel; + int bit_rate; + int sample_rate; + int channels; + int frame_len; + + int len_mpa_frame; + + int fade_in_mode; + int frames_fade_in; + int fade_out_mode; + int frames_fade_out; + int file_pattern; + int upper; + int copies; + + void set_recpath_date_time(void); + void set_event(void); + + void set_fade_values(void); +public: + cPostData(const cChannel *_channel); + ~cPostData(); + + void start_track(void); + void stop_track(int _bit_rate, int _sample_rate, int _channels, + int _len_mpa_frame); + + int get_bit_rate(void) const { return bit_rate; } + int get_sample_rate(void) const { return sample_rate; } + int get_channels(void) const { return channels; } + + int get_frame_len(void) const { return frame_len; } + + int get_len_mpa_frame(void) const { return len_mpa_frame; } + + int get_fade_in_mode(void) const { return fade_in_mode; } + int get_frames_fade_in(void) const { return frames_fade_in; } + int get_fade_out_mode(void) const { return fade_out_mode; } + int get_frames_fade_out(void) const { return frames_fade_out; } + int get_file_pattern(void) const { return file_pattern; } + int get_upper(void) const { return upper; } + int get_copies(void) const { return copies; } +}; + +#endif /* __POSTDATA_H */ diff --git a/postproc.c b/postproc.c new file mode 100644 index 0000000..45f69d1 --- /dev/null +++ b/postproc.c @@ -0,0 +1,380 @@ +/* + * postproc.c + */ + + +#include "postproc.h" +#include "postdata.h" +#include "cache.h" +#include "setup.h" +#include "audiorecorder.h" + +#include <vdr/osd.h> +#include <vdr/tools.h> + +#include <taglib/mpegfile.h> +#include <taglib/tag.h> + +#include <fstream> +#include <sstream> +#include <cstdio> +/* this include is needed for gcc 2.95: */ +#include <vector> + + +using namespace std; + +/* --- cPostproc -------------------------------------------------------------*/ + +list<cPostData> cPostproc::postlist; +cMutex cPostproc::mutex; + +cPostproc::cPostproc() +:cThread() +{ + active = false; + + convert = NULL; + + file_buf.length = KB(10); + file_buf.data = new uchar[file_buf.length]; + + Activate(true); +} + + +cPostproc::~cPostproc() +{ + Activate(false); + + mutex.Lock(); + while (! postlist.empty()) { + postdata = postlist.begin(); + remove(postdata->get_recpath().c_str()); + + postlist.pop_front(); + } + mutex.Unlock(); + + DELETE(convert); + delete[] file_buf.data; +} + + +void cPostproc::Activate(bool on) +{ + if (on) { + if (! active) { + active = true; + Start(); + } + } + else if (active) { + active = false; + Cancel(3); + } +} + + +void cPostproc::Action(void) +{ + dsyslog("[audiorecorder]: postprocessing thread started (%s, %s())", + __FILE__, __func__); + + while (active) { + if (postlist.empty()) { + usleep(10000); + continue; + } + + bool tag = true; + + postdata = postlist.begin(); + + if (postdata->get_frames_fade_in() > 0 || + postdata->get_frames_fade_out() > 0 || + strcmp(audio_codecs[postdata->get_codec()], "mp2") != 0) { + convert = new cConvert(*postdata); + tag = reencode(); + } + else + rename_file(); + + if (tag) { + set_tag(); + Cache.add_track(*postdata); + } + + mutex.Lock(); + postlist.pop_front(); + mutex.Unlock(); + + DELETE(convert); + } + + dsyslog("[audiorecorder]: postprocessing thread stopped (%s, %s())", + __FILE__, __func__); +} + + +void cPostproc::add_track(const cPostData *track) +{ + if (track->get_path().empty() || track->get_recpath().empty()) + return; + + if (access(track->get_path().c_str(), F_OK) != -1) { + remove(track->get_recpath().c_str()); + dsyslog("[audiorecorder]: track (%s) already exists (%s, " + "%s())", track->get_path().c_str(), __FILE__, + __func__); + return; + } + + mutex.Lock(); + postlist.push_back(*track); + mutex.Unlock(); +} + + +int cPostproc::get_num_queued(void) +{ + int num; + + mutex.Lock(); + num = postlist.size(); + mutex.Unlock(); + + return num; +} + + +bool cPostproc::reencode(void) +{ + ifstream infile; + infile.open(postdata->get_recpath().c_str()); + if (! infile) { + dsyslog("[audiorecorder]: can't open input file (%s) (%s, " + "%s())", postdata->get_recpath().c_str(), __FILE__, + __func__); + return false; + } + + dsyslog("[audiorecorder]: (recpath : %s) (%s, %s())", postdata->get_recpath().c_str(), __FILE__, __func__); + dsyslog("[audiorecorder]: (path : %s) (%s, %s())", postdata->get_path().c_str(), __FILE__, __func__); + dsyslog("[audiorecorder]: (recdir : %s) (%s, %s())", postdata->get_recdir().c_str(), __FILE__, __func__); + dsyslog("[audiorecorder]: (artist : %s) (%s, %s())", postdata->get_artist().c_str(), __FILE__, __func__); + dsyslog("[audiorecorder]: (title : %s) (%s, %s())", postdata->get_title().c_str(), __FILE__, __func__); + dsyslog("[audiorecorder]: (file_pattern : %d) (%s, %s())", postdata->get_file_pattern(), __FILE__, __func__); + dsyslog("[audiorecorder]: (upper : %d) (%s, %s())", postdata->get_upper(), __FILE__, __func__); +//dsyslog("[audiorecorder]: (channel : %s) (%s, %s())", postdata->get_channel(), __FILE__, __func__); + + stringstream tmp; + std::string path; + ofstream outfile; + + int pos, res = 0; + std::string dir, pfad, dirpfad; + + dirpfad = postdata->get_recdir(); + pfad = postdata->get_path(); + pos = pfad.find(dirpfad); + + if (pos >= 0) { + dir = pfad; + dir.erase(0, dirpfad.length()); + dsyslog("[audiorecorder]: dir : %s (%s, %s())", dir.c_str(), __FILE__, + __func__); + + pos = dir.find("/"); + + while (pos >= 0 && res == 0) { + if (pos == 0) { + dir.erase(0, 1); + } else { + dirpfad.append("/"); + dirpfad.append(dir.substr(0, pos)); + dir.erase(0, pos + 1); + + res = test_and_create(dirpfad.c_str()); + + dsyslog("[audiorecorder]: 5 dirpfad : %d %s (%s, %s())", res, dirpfad.c_str(), __FILE__, + __func__); + } + dsyslog("[audiorecorder]: 6 dir : %s (%s, %s())", dir.c_str(), __FILE__, + __func__); + pos = dir.find("/"); + } + } + + outfile.open(postdata->get_path().c_str()); + + if (! outfile) { + dsyslog("[audiorecorder]: can't open output file (%s) (%s, " + "%s())", postdata->get_path().c_str(), __FILE__, + __func__); + return false; + } + + infile.seekg(0, ios::end); + int file_pos = infile.tellg(); + infile.seekg(0, ios::beg); + + int frames = file_pos / postdata->get_len_mpa_frame(); + + dsyslog("[audiorecorder]: start reencoding (%s into %s) (%s, %s())", + postdata->get_recpath().c_str(), postdata->get_path().c_str(), + __FILE__, __func__); + + int frame = 0; + file_pos = 0; + file_buf.offset = 0; + + while (active && ! infile.eof()) { + infile.seekg(file_pos + file_buf.offset, ios::beg); + file_pos = infile.tellg(); + file_buf.offset = 0; + + infile.read((char *)file_buf.data, file_buf.length); + + get_mpa_frame(&file_buf, &mpa_frame, + postdata->get_recpath().c_str()); + + while(active && mpa_frame.data) { + file_buf.offset += mpa_frame.length; + + /* pause postprocessing if osd is opened */ + while (active && SetupValues.pause == 1 && + cOsd::IsOpen()) + usleep(10000); + + float volume = get_volume(frame, frames); + abuffer *encoder_buf = + convert->reencode_mpa_frame(&mpa_frame, volume); + + if (encoder_buf->data) { + outfile.write((char *)encoder_buf->data, + encoder_buf->offset); + /* + * encoder_buf->offset is used to save the + * size of the encoded frame ... + */ + ++frame; + } + + get_mpa_frame(&file_buf, &mpa_frame, + postdata->get_recpath().c_str()); + } + } + + infile.close(); + outfile.close(); + + remove(postdata->get_recpath().c_str()); + + if (! active) { + remove(postdata->get_path().c_str()); + return false; + } + + dsyslog("[audiorecorder]: stop reencoding (%s into %s) (%s, %s())", + postdata->get_recpath().c_str(), postdata->get_path().c_str(), + __FILE__, __func__); + + return true; +} + + +float cPostproc::get_volume(int _frame, int _frames) +{ + int fade; + + if (_frame < postdata->get_frames_fade_in()) { + fade = postdata->get_frames_fade_in(); + + if (postdata->get_fade_in_mode() == 1) + return (float)_frame / (float)fade; + else if (postdata->get_fade_in_mode() == 2) + return (float)(_frame * _frame) / (float)(fade * fade); + } + else if (_frame > _frames - postdata->get_frames_fade_out()) { + fade = postdata->get_frames_fade_out(); + _frame = _frames - _frame; + + if (postdata->get_fade_out_mode() == 1) + return (float)_frame / (float)fade; + else if (postdata->get_fade_out_mode() == 2) + return (float)(_frame * _frame) / (float)(fade * fade); + } + + return 1; +} + + +void cPostproc::set_tag() +{ + TagLib::MPEG::File file(postdata->get_path().c_str()); + TagLib::Tag *tag = file.tag(); + + if (! tag) + return; + + tag->setArtist(postdata->get_artist()); + tag->setTitle(postdata->get_title()); + + if (! postdata->get_album().empty()) + tag->setAlbum(postdata->get_album()); + + if (! postdata->get_genre().empty()) + tag->setGenre(postdata->get_genre()); + + if (! postdata->get_channel().empty()) { + cPlugin *plugin = cPluginManager::GetPlugin("audiorecorder"); + + stringstream tmp; + + tmp << "recorded on \"" << (postdata->get_event().empty() ? + "" : postdata->get_event()) << "@" + << postdata->get_channel() << "\" (vdr-audiorecorder " + << plugin->Version() << ")"; + + tag->setComment(tmp.str().c_str()); + } + + file.save(); + + dsyslog("[audiorecorder]: tag written (%s) (%s, %s())", + postdata->get_path().c_str(), __FILE__, __func__); +} + + +void cPostproc::rename_file() +{ + rename(postdata->get_recpath().c_str(), postdata->get_path().c_str()); + + dsyslog("[audiorecorder]: renamed %s to %s (%s, %s())", + postdata->get_recpath().c_str(), postdata->get_path().c_str(), + __FILE__, __func__); +} + +int cPostproc::test_and_create(const char *dirname) +{ + struct stat statbuf; + + int res; + + res = stat(dirname, &statbuf); + + if (res != 0) { + if (mkdir(dirname, S_IRWXU | S_IRWXG | S_IXOTH | S_IROTH)) + return 0; + else + return 1; + } else { + if (statbuf.st_mode & S_IFDIR) { + return 0; + } else { + return 1; + } + } +} + diff --git a/postproc.h b/postproc.h new file mode 100644 index 0000000..44d9242 --- /dev/null +++ b/postproc.h @@ -0,0 +1,48 @@ +/* + * postproc.h + */ + +#ifndef __POSTPROC_H +#define __POSTPROC_H + +#include "postdata.h" +#include "convert.h" +#include "mpa-frame.h" +#include "a-tools.h" + +#include <vdr/thread.h> + +#include <list> + + +class cPostproc : public cThread { +private: + bool active; + + static std::list<cPostData> postlist; + static cMutex mutex; + std::list<cPostData>::iterator postdata; + + abuffer file_buf; + mpeg_audio_frame mpa_frame; + cConvert *convert; + + bool reencode(void); + void fade_in(void); + void fade_out(void); + float get_volume(int _frame, int _frames); + void set_tag(void); + void rename_file(void); + int test_and_create(const char *dirname); +protected: + virtual void Action(void); + virtual void Activate(bool on); +public: + cPostproc(void); + ~cPostproc(); + + static void add_track(const cPostData *track); + static int get_num_queued(void); +}; + +#endif /* __POSTPROC_H */ diff --git a/push-test b/push-test deleted file mode 100644 index e69de29..0000000 diff --git a/rds.c b/rds.c new file mode 100644 index 0000000..b204b0d --- /dev/null +++ b/rds.c @@ -0,0 +1,386 @@ +/* + * rds.c + */ + +#include "rds.h" +#include "audiorecorder.h" + +#include <vdr/tools.h> + +#include <iostream> +#include <string.h> + + +using namespace std; + +/* + * rt-translation: 0x80..0xff + * this table is taken from the vdr-radio-plugin, thx a lot ... + */ +const uchar rt_trans[128] = { + 0xe1, 0xe0, 0xe9, 0xe8, 0xed, 0xec, 0xf3, 0xf2, + 0xfa, 0xf9, 0xd1, 0xc7, 0x8c, 0xdf, 0x8e, 0x8f, + 0xe2, 0xe4, 0xea, 0xeb, 0xee, 0xef, 0xf4, 0xf6, + 0xfb, 0xfc, 0xf1, 0xe7, 0x9c, 0x9d, 0x9e, 0x9f, + 0xaa, 0xa1, 0xa9, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, + 0xa8, 0xa9, 0xa3, 0xab, 0xac, 0xad, 0xae, 0xaf, + 0xba, 0xb9, 0xb2, 0xb3, 0xb1, 0xa1, 0xb6, 0xb7, + 0xb5, 0xbf, 0xf7, 0xb0, 0xbc, 0xbd, 0xbe, 0xa7, + 0xc1, 0xc0, 0xc9, 0xc8, 0xcd, 0xcc, 0xd3, 0xd2, + 0xda, 0xd9, 0xca, 0xcb, 0xcc, 0xcd, 0xd0, 0xcf, + 0xc2, 0xc4, 0xca, 0xcb, 0xce, 0xcf, 0xd4, 0xd6, + 0xdb, 0xdc, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, + 0xc3, 0xc5, 0xc6, 0xe3, 0xe4, 0xdd, 0xd5, 0xd8, + 0xde, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xf0, + 0xe3, 0xe5, 0xe6, 0xf3, 0xf4, 0xfd, 0xf5, 0xf8, + 0xfe, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff }; + + +/* map rds pty-codes 10-15 and 24-28 to id3v1 tags */ +const char *ptys[29] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, "Pop", "Rock", "Easy Listening", "Classical", + "Classical", "Other", NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, "Jazz", "Country", "Folklore", "Oldies", "Folk" }; + +/* --- cRds ----------------------------------------------------------------- */ + + +cRds::cRds(cPostData *_postdata) +{ + postdata = _postdata; + recstat = recWait; + + buf.data = new uchar[RDS_BUF_SIZE]; + buf.length = 0; + buf.offset = 0; + + rt_length = 0; + lb0xfd = false; + + last_tb = -1; + last_rb = -1; +} + + +cRds::~cRds() +{ + delete[] buf.data; +} + + +void cRds::put_data(uchar *data, int length) +{ + int c, pos, len; + + pos = length - 1; + len = data[pos - 1]; /* length of rds data */ + + if (data[pos] != 0xfd || len == 0) + return; + + if (buf.length + len >= RDS_BUF_SIZE) { + dsyslog("[audiorecorder]: buffer overflow <%s> (%s, %s())", + postdata->get_channel().c_str(), __FILE__, __func__); + buf.length = 0; + buf.offset = 0; + } + + /* reverse rds data */ + for (c = 2; c < len + 2; ++c) { + + /* byte stuffing */ + int bs = data[pos - c]; + + if (bs == 0xfd) { + lb0xfd = true; + continue; + } + + if (lb0xfd) { + switch (bs) { + case 0x00: + bs = 0xfd; + break; + case 0x01: + bs = 0xfe; + break; + default: + bs = 0xff; + } + + lb0xfd = false; + } + + /* copy rds value on the buffer */ + buf.data[buf.length] = bs; + ++buf.length; + } +} + + +bool cRds::set_next_frame(void) +{ + int offset; + + offset = buf.offset; + rds_frame.data = NULL; + rds_frame.length = 0; + + for (; buf.offset < buf.length - 4; ++buf.offset) { + if (buf.data[buf.offset] == 0xfe) { + /* rds start marker found */ + rds_frame.length = buf.data[buf.offset + 4] + 8; + + if (buf.offset + rds_frame.length > buf.length) + break; + + rds_frame.data = buf.data + buf.offset; + + /* check rds end marker */ + if (rds_frame.data[rds_frame.length - 1] != 0xff) + dsyslog("[audiorecorder]: no rds end marker " + "found <%s> (%s, %s())", + postdata->get_channel().c_str(), + __FILE__, __func__); + + break; + } + } + + if (buf.offset != offset && cPluginAudiorecorder::get_dbg_level() > 0) + cout << "skipped " << (buf.offset - offset) << " byte(s) <" + << postdata->get_channel() << "> (" << __FILE__ + << ", " << __func__ << "())" << endl; + + if (! rds_frame.data) { + delete_data(buf.offset); + return false; + } + + buf.offset += rds_frame.length; + + return true; +} + + +void cRds::delete_data(int length) +{ + /* clear the buffer */ + + if (length < 1) + return; + + buf.length -= length; + buf.offset -= length; + memmove(buf.data, buf.data + length, buf.length); +} + + +eRecStat cRds::decode_frame(void) +{ + /* reset recording status */ + switch (recstat) { + case recStart: + recstat = recRun; + break; + case recStop: + recstat = recOff; + break; + default: + break; + } + + + if (rds_frame.data[5] == mecRT) + decode_radiotext(); + else if (rds_frame.data[5] == mecODA && rds_frame.data[7] == 0x4b && + rds_frame.data[8] == 0xd7) + decode_rtp(); + else if (rds_frame.data[5] == mecPTY) { + int pty = rds_frame.data[8]; + + if (recstat == recRun && postdata->get_genre().empty()) { + if ((pty > 9 && pty < 16) || (pty > 23 && pty < 29)) + postdata->set_genre(ptys[pty]); + } + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "pty-code <" << postdata->get_channel() << + ">: " << pty << endl; + } + + return recstat; +} + + +void cRds::decode_radiotext(void) +{ + int c, rt_ab_flag; + + rt_length = rds_frame.data[8] - 1; + rt_ab_flag = rds_frame.data[9] & 0x01; + + for (c = 0; c < rt_length; ++c) { + if (rds_frame.data[c + 10] >= 0x80) + rds_frame.data[c + 10] = + rt_trans[(rds_frame.data[c + 10] - 0x80)]; + + radiotext[c] = rds_frame.data[c + 10]; + } + + radiotext[rt_length] = '\0'; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "radiotext (" << rt_ab_flag << ") <" << + postdata->get_channel() << ">: " << radiotext << endl; +} + + +void cRds::decode_rtp(void) +{ + int rb, tb; + + bool toggle_tb = false; + bool toggle_rb = false; + + tb = (rds_frame.data[10] >> 4) & 0x01; + if (last_tb == -1) + last_tb = tb; + + rb = (rds_frame.data[10] >> 3) & 0x01; + if (last_rb == -1) + last_rb = rb; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "rtp-data <" << postdata->get_channel() << ">: toggle " + "bit: " << tb << ", running bit: " << rb << endl; + + if (last_tb != tb) + toggle_tb = true; + + if (last_rb != rb) + toggle_rb = true; + + last_tb = tb; + last_rb = rb; + + if (recstat == recWait) { + if (! toggle_tb && ! toggle_rb) + return; + + /* ready to record */ + recstat = recOff; + } + + if (rb == 1) { + /* running bit is on */ + if (recstat == recOff) { + recstat = recStart; + rt_length = 0; + } + else if (toggle_tb) { + recstat = recStop; + return; + } + } + else { + /* running bit is off */ + if (recstat == recRun) + recstat = recStop; + + return; + } + + if (recstat == recRun && rt_length > 0) + decode_rtp_items(); +} + + +void cRds::decode_rtp_items(void) +{ + int c, t[2], s[2], l[2]; + + /* tag 1 */ + t[0] = ((rds_frame.data[10] << 3) & 0x38) | (rds_frame.data[11] >> 5); + s[0] = ((rds_frame.data[11] << 1) & 0x3e) | (rds_frame.data[12] >> 7); + l[0] = ((rds_frame.data[12] >> 1) & 0x3f) + 1; + + /* tag 2*/ + t[1] = ((rds_frame.data[12] << 5) & 0x20) | (rds_frame.data[13] >> 3); + s[1] = ((rds_frame.data[13] << 3) & 0x38) | (rds_frame.data[14] >> 5); + l[1] = (rds_frame.data[14] & 0x1f) + 1; + + for (c = 0; c < 2; ++c) { + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "rtp-data <" << postdata->get_channel() << ">: " + "type: " << t[c] << ", start:" << s[c] << + ", length: " << l[c] << endl; + + if (t[c] < 1 || t[c] > 10) + continue; + + if (t[c] == ItemTitle) { + if (correct_rtp_tag(t[c], s[c], l[c])) + postdata->set_title(radiotext + s[c]); + } + else if (t[c] == ItemArtist) { + if (correct_rtp_tag(t[c], s[c], l[c])) + postdata->set_artist(radiotext + s[c]); + } + else if (t[c] == ItemAlbum) { + if (correct_rtp_tag(t[c], s[c], l[c])) + postdata->set_album(radiotext + s[c]); + } + else if (t[c] == ItemTrack) + postdata->set_track(atoi(radiotext + s[c])); + else if (t[c] == ItemGenre) { + if (correct_rtp_tag(t[c], s[c], l[c])) + postdata->set_genre(radiotext + s[c]); + } + } +} + + +bool cRds::correct_rtp_tag(int &type, int &start, int &length) +{ + int original_length = length; + + if (start + length > rt_length) { + length = rt_length - start; + + if (original_length - length > 1 || length < 0) { + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "rtp-data <" << postdata->get_channel() + << ">: got buggy tag-infos, could not " + "correct the length value" << endl; + return false; + } + } + + /* remove ' ', '"' or "-" at the beginning of the tag */ + int end = start + length; + for (; start < end; ++start) { + if (radiotext[start] != ' ' && radiotext[start] != '"' && + radiotext[start] != '-') + break; + + --length; + } + + /* remove ' ' or '"' at the end of the tag */ + for (; length > 0; length--) { + if (radiotext[start + length - 1] != ' ' && radiotext[start + + length - 1] != '"') + break; + } + + if (length <= 1 && cPluginAudiorecorder::get_dbg_level() > 1) { + cout << "rtp-data <" << postdata->get_channel() << ">: got " + "buggy tag-infos, length is too short !" << endl; + return false; + } + + radiotext[start + length] = '\0'; + + return true; +} diff --git a/rds.h b/rds.h new file mode 100644 index 0000000..0dad138 --- /dev/null +++ b/rds.h @@ -0,0 +1,60 @@ +/* + * rds.h + */ + +#ifndef __RDS_H +#define __RDS_H + +#include "recstat.h" +#include "postdata.h" +#include "a-tools.h" + + +#define RDS_BUF_SIZE 263*2 + +enum eMec { + mecNone, + mecPTY = 0x07, + mecRT = 0x0a, + mecODA = 0x46 +}; + +enum { + ItemTitle = 1, + ItemAlbum, + ItemTrack, + ItemArtist, + ItemComposition, /* not supported */ + ItemMovement, /* not supported */ + ItemComposer, /* not supported */ + ItemBand, /* not supported */ + ItemComment, + ItemGenre /* not supported */ +}; + +class cRds { +private: + abuffer buf, rds_frame; + char radiotext[65]; + int rt_length; + bool lb0xfd; + int last_tb, last_rb; + eRecStat recstat; + + cPostData *postdata; + + void decode_radiotext(void); + void decode_rtp(void); + void decode_rtp_items(void); + bool correct_rtp_tag(int &type, int &start, int &length); + void delete_data(int length); +public: + cRds(cPostData *_postdata); + ~cRds(); + + void put_data(uchar *data, int length); + bool set_next_frame(void); + eRecStat decode_frame(void); +}; + +#endif /* __RDS_H */ diff --git a/recstat.h b/recstat.h new file mode 100644 index 0000000..556070f --- /dev/null +++ b/recstat.h @@ -0,0 +1,17 @@ +/* + * recstat.h + */ + +#ifndef __RECSTAT_H +#define __RECSTAT_H + + +enum eRecStat { + recWait, /* wait for the first toggle */ + recOff, /* no active recording */ + recStart, /* start recording */ + recRun, /* active recording */ + recStop, /* stop recording */ +}; + +#endif /* __RECSTAT_H */ diff --git a/service.h b/service.h new file mode 100644 index 0000000..2fd4b14 --- /dev/null +++ b/service.h @@ -0,0 +1,27 @@ +/* + * service.h + */ + +#ifndef __SERVICE_H +#define __SERVICE_H + +#include <vdr/channels.h> + + +/* + * service-id: "Audiorecorder-StatusRtpChannel-v1.0" + * give me the channel, and i will set the actual status. + */ + +struct Audiorecorder_StatusRtpChannel_v1_0 { + const cChannel *channel; + int status; + /* + * 0 = channel is unknown + * 1 = channel is known, but no receiver is attached to this channel + * 2 = receiver is attached, but there is no actual recording + * 3 = actual recording + */ +}; + +#endif /* __SERVICE_H */ diff --git a/setup.c b/setup.c new file mode 100644 index 0000000..db5ee78 --- /dev/null +++ b/setup.c @@ -0,0 +1,104 @@ +/* + * setup.c + */ + +#include "setup.h" +#include "mpa-frame.h" + + +/* default setup values: */ +struct setup_values SetupValues = { + 1, /* receiving mode on start is 'on' */ + 5, /* max. receivers */ + 50, /* min. free space (in mb) */ + 3, /* default view 'by date' */ + 1, /* pause postprocessing if osd is opened */ + 8, /* max. tracks in postprocessing queue */ + 1, /* fade in mode */ + 9, /* fade in seconds */ + 2, /* fade out mode */ + 12, /* fade out seconds */ + 0, /* audio codec */ + NUM_CODECS, /* number of audio codecs */ + 9, /* bitrate */ + 0 /* file_pattern */ +}; + + +const char *audio_codecs[NUM_CODECS] = { "mp2", "libmp3lame", "mp3" }; + +const char *audio_codecs_translated[NUM_CODECS] = { tr("mp2"), tr("mp3"), tr("mp3") }; + +const char *fade_types[NUM_FADE_TYPES] = { tr("off"), tr("linear"), tr("exponential") }; + +const char *_bit_rates[] = { "0", "32000", "48000", "56000", "64000", "80000", "96000", "112000", "128000", "160000", "192000", "224000", "256000", "320000", "384000", "0" }; + +const char *views[NUM_VIEWS] = { tr("all"), tr("by artist"), tr("by channel"), tr("by date") }; + +const char *file_patterns[NUM_FILE_PATTERNS] = { tr("Artist/Title"), tr("Artist - Title"), tr("Channel/Artist/Title"), tr("Channel - Artist - Title"), tr("Channel/Artist - Title"), tr("External") }; + +/* --- cAudiorecorderSetup -------------------------------------------------- */ + +cAudiorecorderSetup::cAudiorecorderSetup(void) +:cMenuSetupPage() +{ + setupvalues = SetupValues; + + Add(new cOsdItem(tr("--- Receiver: ---"), osUnknown, false)); + Add(new cMenuEditBoolItem(tr("Receiving mode on start"), + &setupvalues.start_type, tr("off"), tr("on"))); + Add(new cMenuEditIntItem(tr("Max. number of receivers"), + &setupvalues.max_receivers, 0, 12)); + Add(new cMenuEditIntItem(tr("Min. free disc space (in mb)"), + &setupvalues.min_free_space, 50, 999999)); + + Add(new cOsdItem(tr("--- Browser: ---"), osUnknown, false)); + Add(new cMenuEditStraItem(tr("Default view"), &setupvalues.default_view, + NUM_VIEWS, views)); + + Add(new cOsdItem(tr("--- Postprocessing: ---"), osUnknown, false)); + Add(new cMenuEditBoolItem(tr("Pause if osd is opened"), + &setupvalues.pause)); + Add(new cMenuEditIntItem(tr("Max. tracks in queue"), + &setupvalues.max_postproc, 1, 20)); + Add(new cMenuEditStraItem(tr("Fade in mode"), &setupvalues.fade_in_mode, + NUM_FADE_TYPES, fade_types)); + Add(new cMenuEditIntItem(tr("Fade in seconds"), &setupvalues.fade_in, + 0, 20)); + Add(new cMenuEditStraItem(tr("Fade out mode"), &setupvalues.fade_out_mode, + NUM_FADE_TYPES, fade_types)); + Add(new cMenuEditIntItem(tr("Fade out seconds"), &setupvalues.fade_out, + 0, 20)); + Add(new cMenuEditStraItem(tr("Audio codec"), &setupvalues.audio_codec, + setupvalues.num_audio_codecs, audio_codecs_translated)); + Add(new cMenuEditStraItem(tr("Bitrate (if audio codec isn't <original>)"), + &setupvalues.bit_rate, 14, &_bit_rates[1])); + Add(new cMenuEditStraItem(tr("File Pattern"), + &setupvalues.file_pattern, NUM_FILE_PATTERNS, file_patterns)); + Add(new cMenuEditBoolItem(tr("Upper Case"), + &setupvalues.upper, tr("No"), tr("Yes"))); + Add(new cMenuEditIntItem(tr("Copies"), &setupvalues.copies, + 0, 20)); +} + + +void cAudiorecorderSetup::Store(void) +{ + SetupValues = setupvalues; + + SetupStore("default_view", SetupValues.default_view); + SetupStore("start_type", SetupValues.start_type); + SetupStore("max_receivers", SetupValues.max_receivers); + SetupStore("min_free_space", SetupValues.min_free_space); + SetupStore("pause", SetupValues.pause); + SetupStore("max_postproc", SetupValues.max_postproc); + SetupStore("fade_in_mode", SetupValues.fade_in_mode); + SetupStore("fade_in", SetupValues.fade_in); + SetupStore("fade_out_mode", SetupValues.fade_out_mode); + SetupStore("fade_out", SetupValues.fade_out); + SetupStore("audio_codec", audio_codecs_translated[SetupValues.audio_codec]); + SetupStore("bit_rate", SetupValues.bit_rate); + SetupStore("file_pattern", SetupValues.file_pattern); + SetupStore("upper", SetupValues.upper); + SetupStore("copies", SetupValues.copies); +} diff --git a/setup.h b/setup.h new file mode 100644 index 0000000..7194632 --- /dev/null +++ b/setup.h @@ -0,0 +1,56 @@ +/* + * setup.h + */ + +#ifndef __SETUP_H +#define __SETUP_H + +#include <vdr/menuitems.h> + + +struct setup_values { + int start_type; + int max_receivers; + int min_free_space; + + int default_view; + + int pause; + int max_postproc; + int fade_in_mode; + int fade_in; + int fade_out_mode; + int fade_out; + int audio_codec; + int num_audio_codecs; + int bit_rate; + int file_pattern; + int upper; + int copies; +}; + +extern struct setup_values SetupValues; + +#define NUM_CODECS 3 +extern const char *audio_codecs[NUM_CODECS]; +extern const char *audio_codecs_translated[NUM_CODECS]; + +#define NUM_FADE_TYPES 3 +extern const char *fade_types[NUM_FADE_TYPES]; + +#define NUM_VIEWS 4 +extern const char *views[NUM_VIEWS]; + +#define NUM_FILE_PATTERNS 6 +extern const char *file_patterns[NUM_FILE_PATTERNS]; + +class cAudiorecorderSetup : public cMenuSetupPage { +private: + struct setup_values setupvalues; +protected: + virtual void Store(void); +public: + cAudiorecorderSetup(void); +}; + +#endif /* __SETUP_H */ diff --git a/trackinfo.c b/trackinfo.c new file mode 100644 index 0000000..91b895e --- /dev/null +++ b/trackinfo.c @@ -0,0 +1,360 @@ +/* + * trackinfo.c + */ + +#include "trackinfo.h" +#include "audiorecorder.h" + +#include <vdr/tools.h> + +#include <iostream> + + +using namespace std; + +/* --- cTrackInfo ----------------------------------------------- */ + + +cTrackInfo::cTrackInfo(void) +{ + track = 0; + year = 0; +} + + +void cTrackInfo::clear(void) +{ + recpath.erase(); + path.erase(); + artist.erase(); + title.erase(); + album.erase(); + track = 0; + year = 0; + genre.erase(); + comment.erase(); + event.erase(); + recdate.erase(); + rectime.erase(); + codec = 0; + recdir.erase(); +} + + +void cTrackInfo::set_recpath(const string &_recpath) +{ + if (_recpath.empty() || ! recpath.empty()) + return; + + recpath = _recpath; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- recpath set to: " << recpath << " ---" << endl; +} + + +void cTrackInfo::set_path(int _file_pattern, int _upper) +{ + if (artist.empty() || title.empty()) + return; + + stringstream tmp; + + patch_chars(artist); + patch_chars(title); + + if (_upper) { + string_toupper(artist); + string_toupper(title); + string_toupper(album); + string_toupper(channel); + string_toupper(event); + } + + switch(_file_pattern) { + case 0 : // Artist/Title + tmp << artist << "/" << title << "." << audio_codecs_translated[codec]; + break; + case 1 : // Artist - Title + tmp << artist << "-" << title << "." << audio_codecs_translated[codec]; + break; + case 2 : // Station/Artist/Title + tmp << channel << "/" << artist << "/" << title << "." << audio_codecs_translated[codec]; + break; + case 3 : // Station - Artist - Title + tmp << channel << "-" << artist << "-" << title << "." << audio_codecs_translated[codec]; + break; + case 4 : // Station/Artist - Title + tmp << channel << "/" << artist << "-" << title << "." << audio_codecs_translated[codec]; + break; + case 5 : // External + tmp << path_external(); + break; + } + + path = tmp.str(); + partial_path = tmp.str(); + + recdir.insert(0, cPluginAudiorecorder::get_recdir()); + + if (!path.empty()) + path.insert(0, cPluginAudiorecorder::get_recdir()); + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- path set to: " << path << " ---" << endl; +} + +void cTrackInfo::set_path(const string &_path) +{ + if (_path.empty()) { + path = ""; + partial_path = ""; + } else { + path = _path; + partial_path = path; + recdir = cPluginAudiorecorder::get_recdir(); + partial_path.erase(0, recdir.length()); + } + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- path set to: " << path << " ---" << endl; +} + + +void cTrackInfo::set_date(const char *_date) +{ + if (! _date || ! recdate.empty()) + return; + + recdate = _date; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- date set to: " << recdate << " ---" << endl; +} + + +void cTrackInfo::set_time(const char *_time) +{ + if (! _time || ! rectime.empty()) + return; + + rectime = _time; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- time set to: " << rectime << " ---" << endl; +} + +void cTrackInfo::set_artist(const char *_artist) +{ + if (! _artist || ! artist.empty()) + return; + + artist = _artist; + // string_toupper(artist); + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- artist set to: " << artist << " ---" << endl; +} + + +void cTrackInfo::set_title(const char *_title) +{ + if (! _title || ! title.empty()) + return; + + title = _title; + // string_toupper(title); + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- title set to: " << title << " ---" << endl; +} + + +void cTrackInfo::set_album(const char *_album) +{ + if (! _album || ! album.empty()) + return; + + album = _album; + // string_toupper(album); + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- album set to: " << album << " ---" << endl; +} + + +void cTrackInfo::set_track(int _track) +{ + if (_track == 0 || track != 0) + return; + + track = _track; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- track set to: " << track << " ---" << endl; +} + + +void cTrackInfo::set_year(int _year) +{ + if (_year == 0 || year != 0) + return; + + year = _year; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- year set to: " << year << " ---" << endl; +} + + +void cTrackInfo::set_genre(const char *_genre) +{ + if (! _genre || ! genre.empty()) + return; + + genre = _genre; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- genre set to: " << genre << " ---" << endl; +} + + +void cTrackInfo::set_comment(const char *_comment) +{ + if (! _comment || ! comment.empty()) + return; + + comment = _comment; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- comment set to: " << comment << " ---" << endl; +} + + +void cTrackInfo::set_channel(const string &_channel) +{ + if (_channel.empty() || ! channel.empty()) + return; + + channel = _channel; + // string_toupper(channel); + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- channel set to: " << channel << " ---" << endl; +} + + +void cTrackInfo::set_event(const string &_event) +{ + if (_event.empty() || ! event.empty()) + return; + + event = _event; + // string_toupper(event); + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- event set to: " << event << " ---" << endl; +} + + +/* +void cTrackInfo::set_codec(const char *_codec) +{ + if (! _codec || ! codec.empty()) + return; + + codec = _codec; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- codec set to: " << codec << " ---" << endl; +} +*/ + +void cTrackInfo::set_codec(int _codec) +{ + if (_codec < 0 || _codec >= NUM_CODECS) + return; + + codec = _codec; + + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "--- codec set to: " << codec << " ---" << endl; +} + + +void cTrackInfo::set_bit_rates(int _mpa_frame_bit_rate, int _bit_rate) +{ + mpa_frame_bit_rate = _mpa_frame_bit_rate; + bit_rate = _bit_rate; +} + +void cTrackInfo::string_toupper(string &str) +{ + for (string::iterator i = str.begin(); i != str.end(); ++i) + *i = toupper((unsigned char)*i); +} + +void cTrackInfo::patch_chars(string &_string) +{ + for (string::iterator i = _string.begin(); i != _string.end(); ++i) { + if ((char)*i == '*' || (char)*i == '?') + *i = '_'; + else if ((char)*i == '/') + *i = ','; + } +} + +std::string cTrackInfo::path_external(void) +{ + + char *cmdbuf = NULL; + char *result = NULL; + + dsyslog("[audiorecorder]: LT 1 (%s, %s())", + __FILE__, __func__); + + asprintf(&cmdbuf, "\"%s\" \"%s\" \"%d\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%d\"", + cPluginAudiorecorder::get_pscript().c_str(), + recpath.c_str(), + mpa_frame_bit_rate, + artist.c_str(), + title.c_str(), + channel.c_str(), + event.c_str(), + audio_codecs_translated[codec], + bit_rate + ); + + dsyslog("[audiorecorder]: LT 2 (%s, %s())", + __FILE__, __func__); + + dsyslog("[audiorecorder]: executing command '%s' (%s, %s())", + cmdbuf, + __FILE__, __func__); + + cReadLine pipe; + + dsyslog("[audiorecorder]: LT 3 (%s, %s())", + __FILE__, __func__); + + FILE *p = popen(cmdbuf, "r"); + dsyslog("[audiorecorder]: LT 4 (%s, %s())", + __FILE__, __func__); + + if (p != (FILE *)NULL) { + result = pipe.Read(p); + dsyslog("[audiorecorder]: received '%s' (%s, %s())", + result, + __FILE__, __func__); + pclose(p); + } else { + dsyslog("[audiorecorder]: ERROR: can't open pipe for command '%s' (%s, %s())", + cmdbuf, + __FILE__, __func__); + } + free(cmdbuf); + + dsyslog("[audiorecorder]: LT 5 (%s, %s())", + __FILE__, __func__); + return (result == NULL) ? "" : result; +} diff --git a/trackinfo.h b/trackinfo.h new file mode 100644 index 0000000..9361598 --- /dev/null +++ b/trackinfo.h @@ -0,0 +1,69 @@ +/* + * trackinfo.h + */ + +#ifndef __TRACKINFO_H +#define __TRACKINFO_H + +#include <string> +#include "setup.h" + + +class cTrackInfo { +private: + std::string recpath, path, artist, title, album, genre, comment, + channel, event, recdate, rectime, recdir, partial_path; + + int track, year; + int mpa_frame_bit_rate, bit_rate; + int codec; + + void string_toupper(std::string &str); + void patch_chars(std::string &str); + std::string path_external(void); + +public: + cTrackInfo(void); + + void clear(void); + + void set_recpath(const std::string &_recpath); + void set_path(int _file_pattern, int _upper); + void set_path(const std::string &_path); + void set_date(const char *_date); + void set_time(const char *_time); + void set_artist(const char *_artist); + void set_title(const char *_title); + void set_album(const char *_album); + void set_track(int _track); + void set_year(int _year); + void set_genre(const char *_genre); + void set_comment(const char *_comment); + void set_channel(const std::string &_channel); + void set_event(const std::string &_event); + void set_codec(int _codec); + void set_bit_rates(int _mpa_frame_bit_rate, int _bit_rate); + + std::string get_recpath(void) const { return recpath; } + std::string get_path(void) const { return path; } + std::string get_partial_path(void) const { return partial_path ; } + std::string get_date(void) const { return recdate; } + std::string get_time(void) const { return rectime; } + std::string get_artist(void) const { return artist; } + std::string get_title(void) const { return title; } + std::string get_album(void) const { return album; } + int get_track(void) const { return track; } + int get_year(void) const { return year; } + std::string get_genre(void) const { return genre; } + std::string get_comment(void) const { return comment; } + std::string get_channel(void) const { return channel; } + std::string get_event(void) const { return event; } + + int get_codec(void) const { return codec; } + +/* std::string get_codec(void) const { return codec; } + std::string get_codec_translated(void) const { return codec_translated; } +*/ + std::string get_recdir(void) const { return recdir; } +}; +#endif /* __TRACKINFO_H */ diff --git a/xml-base.c b/xml-base.c new file mode 100644 index 0000000..9900551 --- /dev/null +++ b/xml-base.c @@ -0,0 +1,104 @@ +/* + * xml-base.c + */ + +#include "xml-base.h" + +#include <vdr/tools.h> + +#include <unistd.h> + + +using namespace std; +using namespace a_land; + +/* --- cXmlBase ------------------------------------------------------------- */ + +cXmlBase::cXmlBase(const char *_root_element) +{ + root_element = _root_element; + document = NULL; + root = NULL; +} + + +cXmlBase::~cXmlBase() +{ + if (document) { + document->SaveFile(); + } + + delete document; +} + + +bool cXmlBase::load(const string &_path) +{ + if (document) + return true; + + path = _path; + + dsyslog("[audiorecorder]: loading xml-file (%s) (%s ,%s())", + path.c_str(), __FILE__, __func__); + + document = new TiXmlDocument(path); + + if (access(path.c_str(), F_OK) == -1) { + dsyslog("[audiorecorder]: creating empty xml-file (%s) (%s ," + "%s())", path.c_str(), __FILE__, __func__); + TiXmlElement new_root(root_element); + document->InsertEndChild(new_root); + document->SaveFile(); + } + + if (! document->LoadFile()) { + dsyslog("[audiorecorder]: error while parsing xml-file (%s) " + "(%s, %s())", path.c_str(), __FILE__, __func__); + dsyslog("[audiorecorder]: %s, row: %d, column: %d (%s, %s())", + document->ErrorDesc(), document->ErrorRow(), + document->ErrorCol(), __FILE__, __func__); + return false; + } + + set_root(); + + if (! root || root->ValueStr() != root_element) + return false; + + copy_to_objects(); + + return true; +} + + +void cXmlBase::clear(void) +{ + remove(path.c_str()); + + delete document; + document = new TiXmlDocument(path); + + root = new TiXmlElement(root_element); +} + + +void cXmlBase::add_subelement(TiXmlElement &main_element, const char *name, + const string &text) +{ + if (! name || text.empty()) + return; + + TiXmlElement element(name); + TiXmlText txt(text); + element.InsertEndChild(txt); + + main_element.InsertEndChild(element); +} + + +void cXmlBase::set_root(void) +{ + if (document) + root = document->RootElement(); +} diff --git a/xml-base.h b/xml-base.h new file mode 100644 index 0000000..4c63426 --- /dev/null +++ b/xml-base.h @@ -0,0 +1,37 @@ +/* + * xml-base.h + */ + +#ifndef __XML_BASE_H +#define __XML_BASE_H + +#include "tinyxml/tinyxml.h" + +#include <string> + + +class cXmlBase { +private: + std::string path, root_element; + + a_land::TiXmlDocument *document; + a_land::TiXmlElement *root; +protected: + virtual ~cXmlBase(); + + virtual void copy_to_objects(void) {} +public: + cXmlBase(const char *_root_element); + + bool load(const std::string &_path); + void clear(void); + + void add_subelement(a_land::TiXmlElement &main_element, + const char *name, const std::string &text); + + a_land::TiXmlDocument *get_document(void) { return document; } + a_land::TiXmlElement *get_root(void) { return root; } + void set_root(void); +}; + +#endif /* __XML_BASE_H */ diff --git a/xml-cache.c b/xml-cache.c new file mode 100644 index 0000000..14204b9 --- /dev/null +++ b/xml-cache.c @@ -0,0 +1,280 @@ +/* + * xml-cache.c + */ + +#include "xml-cache.h" +#include "cache.h" +#include "audiorecorder.h" + +#include <taglib/mpegfile.h> +#include <taglib/tag.h> + +#include <dirent.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <iostream> + +#include "setup.h" + + +using namespace std; +using namespace a_land; + +/* --- cXmlCache ------------------------------------------------------------ */ + + +cXmlCache::cXmlCache(void) +:cXmlBase("cache") {} + + +void cXmlCache::ReadDir(int level, std::string path) +{ + DIR *dir; + struct dirent entry, *result; + struct stat attribute; + string fullpath; + string file; + + dsyslog("[audiorecorder]: level : %d - %s (%s, %s())", level, path.c_str(), __FILE__, __func__); + + + dir = opendir(path.c_str()); + if (! dir) + return; + + + for (readdir_r(dir, &entry, &result); result; readdir_r(dir, &entry, &result)) { + file = result->d_name; + + int len = file.length() - 8; + if (len < 0) + len = 0; + + if (file == "." || file == ".." || + file.substr(len, 8) == ".tmp.mp2") + continue; + + fullpath = path; + + string::reverse_iterator i = fullpath.rbegin(); + + if (*i != '/') { + fullpath.append("/"); + } + fullpath.append(file); + + stat(fullpath.c_str(), &attribute); + + if (! attribute.st_mode & S_IFREG) + continue; + + if ( attribute.st_mode & S_IFDIR) { + ReadDir(level + 1, fullpath); + } else { + for (int i1 = 0; i1 < NUM_CODECS; i1++) { + std::string tmp1; + tmp1 = "."; + tmp1.append(audio_codecs[i1]); + if (file.substr(file.length() - tmp1.length(), tmp1.length()) == tmp1) { + char date_str[11], time_str[6]; + struct tm *tm_now, tm_store; + tm_now = localtime_r(&attribute.st_mtime, &tm_store); + strftime(date_str, 11, "%Y-%m-%d", tm_now); + strftime(time_str, 6, "%H.%M", tm_now); + rebuild_track(fullpath, date_str, time_str); + break; + } + } + } + } + closedir(dir); +} + +void cXmlCache::rebuild(void) +{ + clear(); + + ReadDir(0, cPluginAudiorecorder::get_recdir().c_str()); + + get_document()->InsertEndChild(*get_root()); + + get_document()->SaveFile(); + + set_root(); +} + + +void cXmlCache::add_track(const cTrackInfo &trackinfo, bool save) +{ + if (! get_root() || trackinfo.get_artist().empty() || + trackinfo.get_title().empty()) + return; + + a_land::TiXmlElement track("track"); + track.SetAttribute("path", trackinfo.get_partial_path()); + track.SetAttribute("date", trackinfo.get_date()); + track.SetAttribute("time", trackinfo.get_time()); + + add_subelement(track, "artist", trackinfo.get_artist()); + add_subelement(track, "title", trackinfo.get_title()); + add_subelement(track, "album", trackinfo.get_album()); + + stringstream tmp; + if (trackinfo.get_track() != 0) + tmp << trackinfo.get_track(); + + add_subelement(track, "tracknr", tmp.str()); + + tmp.str(""); + tmp.clear(); + if (trackinfo.get_year() != 0) + tmp << trackinfo.get_year(); + + add_subelement(track, "year", tmp.str()); + add_subelement(track, "genre", trackinfo.get_genre()); + add_subelement(track, "channel", trackinfo.get_channel()); + add_subelement(track, "event", trackinfo.get_event()); + + get_root()->InsertEndChild(track); + + if (save) { + get_document()->SaveFile(); + } +} + + +void cXmlCache::copy_to_objects(void) +{ + a_land::TiXmlElement *xml_track = get_root()->FirstChildElement("track"); + + while (xml_track) { + cTrackInfo trackinfo; + + string path = xml_track->Attribute("path"); + + if (path.empty()) { + /* remove deleted files from the xml-cache */ + a_land::TiXmlElement *tmp = xml_track; + xml_track = xml_track->NextSiblingElement("track"); + get_root()->RemoveChild(tmp); + + continue; + } + + path.insert(0, cPluginAudiorecorder::get_recdir()); + + trackinfo.set_path(path); + + if (access(path.c_str(), F_OK) == -1) { + dsyslog("[audiorecorder]: copy %s : (%s, %s())", path.c_str(), __FILE__, __func__); + /* remove deleted files from the xml-cache */ + a_land::TiXmlElement *tmp = xml_track; + xml_track = xml_track->NextSiblingElement("track"); + get_root()->RemoveChild(tmp); + + continue; + } + + if (xml_track->Attribute("date")) + trackinfo.set_date(xml_track->Attribute("date")); + if (xml_track->Attribute("time")) + trackinfo.set_time(xml_track->Attribute("time")); + + for (a_land::TiXmlElement *element = xml_track->FirstChildElement(); + element; element = element->NextSiblingElement()) { + if (element->FirstChild() == NULL) + continue; + else if (element->ValueStr() == "artist") + trackinfo.set_artist( + element->FirstChild()->Value()); + else if (element->ValueStr() == "title") + trackinfo.set_title( + element->FirstChild()->Value()); + else if (element->ValueStr() == "album") + trackinfo.set_album( + element->FirstChild()->Value()); + else if (element->ValueStr() == "tracknr") + trackinfo.set_track( + atoi(element->FirstChild()->Value())); + else if (element->ValueStr() == "year") + trackinfo.set_year( + atoi(element->FirstChild()->Value())); + else if (element->ValueStr() == "genre") + trackinfo.set_genre( + element->FirstChild()->Value()); + else if (element->ValueStr() == "channel") + trackinfo.set_channel( + element->FirstChild()->Value()); + else if (element->ValueStr() == "event") + trackinfo.set_event( + element->FirstChild()->Value()); + } + + if (! trackinfo.get_title().empty() && + ! trackinfo.get_artist().empty()) + Cache.add_track(trackinfo, false); + + xml_track = xml_track->NextSiblingElement("track"); + } + get_document()->SaveFile(); +} + + +void cXmlCache::rebuild_track(const string &path, const char *date, + const char *time) +{ + if (cPluginAudiorecorder::get_dbg_level() > 1) + cout << "rebuilding track: " << path << endl; + + TagLib::MPEG::File file(path.c_str()); + TagLib::Tag *tag = file.tag(); + + if (! tag) + return; + + cTrackInfo trackinfo; + + trackinfo.set_path(path); + trackinfo.set_date(date); + trackinfo.set_time(time); + + if (! tag->artist().isEmpty()) + trackinfo.set_artist(tag->artist().toCString()); + + if (! tag->title().isEmpty()) + trackinfo.set_title(tag->title().toCString()); + + if (! tag->album().isEmpty()) + trackinfo.set_album(tag->album().toCString()); + + if (tag->track() != 0) + trackinfo.set_track(tag->track()); + + if (tag->year() != 0) + trackinfo.set_year(tag->year()); + + if (! tag->genre().isEmpty()) + trackinfo.set_genre(tag->genre().toCString()); + + if (! tag->comment().isEmpty()) { + string com = tag->comment().toCString(); + trackinfo.set_comment(com.c_str()); + string::size_type f1 = com.find("recorded on \""); + string::size_type f2 = com.find("(vdr-audiorecorder "); + if (f1 != string::npos && f2 != string::npos) { + com.erase(0, 13); + string::size_type p1 = com.find_last_of('@'); + string::size_type p2 = com.find_last_of('"'); + if (p1 != string::npos) { + trackinfo.set_event(com.substr(0, p1)); + if (p2 != string::npos) + trackinfo.set_channel(com.substr(p1 + 1, + p2 - p1 - 1)); + } + } + } + + Cache.add_track(trackinfo, false); + add_track(trackinfo, false); +} diff --git a/xml-cache.h b/xml-cache.h new file mode 100644 index 0000000..9f364bf --- /dev/null +++ b/xml-cache.h @@ -0,0 +1,33 @@ +/* + * xml-cache.h + */ + +#ifndef __XML_CACHE_H +#define __XML_CACHE_H + +#include "xml-base.h" +#include "trackinfo.h" + +#include <unistd.h> + +#include <string> + +#include <iostream> + + + +class cXmlCache: public cXmlBase { +private: + void rebuild_track(const std::string &path, const char *date, + const char *time); +protected: + virtual void copy_to_objects(void); +public: + cXmlCache(void); + + void ReadDir(int level, std::string path); + void rebuild(void); + void add_track(const cTrackInfo &trackinfo, bool save = true); +}; + +#endif /* __XML_CACHE_H */ -- cgit v1.2.3