diff options
author | Joerg Bornkessel <hd_brummy@gentoo.org> | 2014-04-26 19:15:40 +0200 |
---|---|---|
committer | Joerg Bornkessel <hd_brummy@gentoo.org> | 2014-04-26 19:15:40 +0200 |
commit | d45bc4bed686d1d0c2daff22a66dfa0394fde4fb (patch) | |
tree | 75b7a38c993c0c5761764a375fa822190729a402 | |
parent | 99628e3cec79c4b5888056ce4be3b92a38eddf47 (diff) | |
download | vdr-plugin-audiorecorder-d45bc4bed686d1d0c2daff22a66dfa0394fde4fb.tar.gz vdr-plugin-audiorecorder-d45bc4bed686d1d0c2daff22a66dfa0394fde4fb.tar.bz2 |
initial commit
-rw-r--r-- | COPYING | 340 | ||||
-rw-r--r-- | HISTORY | 90 | ||||
-rw-r--r-- | METAGS.TAG (renamed from push-test) | 0 | ||||
-rw-r--r-- | Makefile.obsolete | 136 | ||||
-rw-r--r-- | README | 58 | ||||
-rw-r--r-- | a-tools.h | 22 | ||||
-rw-r--r-- | audioreceiver.c | 259 | ||||
-rw-r--r-- | audioreceiver.h | 63 | ||||
-rw-r--r-- | audiorecorder.c | 345 | ||||
-rw-r--r-- | audiorecorder.h | 63 | ||||
-rw-r--r-- | browse-item.c | 69 | ||||
-rw-r--r-- | browse-item.h | 44 | ||||
-rw-r--r-- | browse.c | 638 | ||||
-rw-r--r-- | browse.h | 71 | ||||
-rw-r--r-- | cache.c | 232 | ||||
-rw-r--r-- | cache.h | 55 | ||||
-rw-r--r-- | column.c | 29 | ||||
-rw-r--r-- | column.h | 58 | ||||
-rw-r--r-- | contrib/audiorecorder.conf | 12 | ||||
-rwxr-xr-x | contrib/postproc.pl | 144 | ||||
-rwxr-xr-x | contrib/set_path | 35 | ||||
-rw-r--r-- | convert.c | 154 | ||||
-rw-r--r-- | convert.h | 37 | ||||
-rw-r--r-- | dispatcher.c | 435 | ||||
-rw-r--r-- | dispatcher.h | 93 | ||||
-rw-r--r-- | external-player.h | 15 | ||||
-rw-r--r-- | mainmenu.c | 90 | ||||
-rw-r--r-- | mainmenu.h | 29 | ||||
-rw-r--r-- | mpa-frame.c | 55 | ||||
-rw-r--r-- | mpa-frame.h | 24 | ||||
-rw-r--r-- | po/de_DE.po | 240 | ||||
-rw-r--r-- | postdata.c | 135 | ||||
-rw-r--r-- | postdata.h | 62 | ||||
-rw-r--r-- | postproc.c | 380 | ||||
-rw-r--r-- | postproc.h | 48 | ||||
-rw-r--r-- | rds.c | 386 | ||||
-rw-r--r-- | rds.h | 60 | ||||
-rw-r--r-- | recstat.h | 17 | ||||
-rw-r--r-- | service.h | 27 | ||||
-rw-r--r-- | setup.c | 104 | ||||
-rw-r--r-- | setup.h | 56 | ||||
-rw-r--r-- | trackinfo.c | 360 | ||||
-rw-r--r-- | trackinfo.h | 69 | ||||
-rw-r--r-- | xml-base.c | 104 | ||||
-rw-r--r-- | xml-base.h | 37 | ||||
-rw-r--r-- | xml-cache.c | 280 | ||||
-rw-r--r-- | xml-cache.h | 33 |
47 files changed, 6093 insertions, 0 deletions
@@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. @@ -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/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='<cwieninger@gmx.de>' -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* *~ @@ -0,0 +1,58 @@ +This is a "plugin" for the Video Disk Recorder (VDR). + +Written by: herbert attenberger <herbsl at a-land.de> + +Project's homepage: <http://www.a-land.de> + +Latest version available at: <http://www.a-land.de> + +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 <vdr/receiver.h> +#include <vdr/ringbuffer.h> +#include <vdr/channels.h> + +#include <string> +#include <fstream> + + +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 <vdr/plugin.h> + +#include <getopt.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <iostream> + +extern "C" { + #include <avcodec.h> +} + +// 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 <vdr/plugin.h> + +#include <sstream> +#include <string> + +#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 <string> + + +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 <vdr/osdbase.h> + + +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 <vdr/skins.h> +#include <vdr/interface.h> +#include <vdr/tools.h> + +#include <sstream> + + +#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 <vdr/osdbase.h> +#include <vdr/plugin.h> + +#include <string> + + +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 */ @@ -0,0 +1,232 @@ +/* + * cache.c + */ + +#include "cache.h" +#include "audiorecorder.h" + +#include <vdr/skins.h> +#include <vdr/tools.h> + + +#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; +} @@ -0,0 +1,55 @@ +/* + * cache.h + */ + +#ifndef __CACHE_H +#define __CACHE_H + +#include "xml-cache.h" +#include "trackinfo.h" +#include "column.h" + +#include <vdr/thread.h> + +#include <list> +#include <string> + +#define NOTHING 0 +#define LOAD 1 +#define REBUILD 2 + + +class cCache : public cThread { +private: + bool active; + int what_to_do; + + std::list<cTrackInfo> tracklist; + std::list<cTrackInfo>::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 <string> + + +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 (<BLACKLIST>) { + 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 "<artist>_-_<title>.<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 */ @@ -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; +} @@ -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 */ @@ -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); +} @@ -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 */ |